mirror of
https://github.com/oceanprotocol/ocean.js.git
synced 2024-11-26 20:39:05 +01:00
Feature/dispenser (#790)
* add dispenser support * bump contracts to 0.6.2
This commit is contained in:
parent
f7a91c7b3c
commit
92b4be0dce
6
package-lock.json
generated
6
package-lock.json
generated
@ -2198,9 +2198,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@oceanprotocol/contracts": {
|
"@oceanprotocol/contracts": {
|
||||||
"version": "0.5.16",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.6.2.tgz",
|
||||||
"integrity": "sha512-p7aFIUT8RVoMzdPP7ML8G08BnQ09syywKjOT16hqJm0GmofunEuVffUXbryG4EkQ+qRbf/zeoxSmesi79kQXlA=="
|
"integrity": "sha512-J6amHsmVbdc2rAwbUYOaY7inLV13GxPIiqbsLF78nmdIvhhGDhT2LYMyfQtxkMwQzYDP6EzD4albCgOXlWM15g=="
|
||||||
},
|
},
|
||||||
"@octokit/auth-token": {
|
"@octokit/auth-token": {
|
||||||
"version": "2.4.5",
|
"version": "2.4.5",
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethereum-navigator/navigator": "^0.5.2",
|
"@ethereum-navigator/navigator": "^0.5.2",
|
||||||
"@oceanprotocol/contracts": "0.5.16",
|
"@oceanprotocol/contracts": "^0.6.2",
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.0.1",
|
||||||
"cross-fetch": "^3.1.2",
|
"cross-fetch": "^3.1.2",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
|
@ -521,7 +521,7 @@ export class DataTokens {
|
|||||||
dataTokenAddress: string,
|
dataTokenAddress: string,
|
||||||
newMinterAddress: string,
|
newMinterAddress: string,
|
||||||
address: string
|
address: string
|
||||||
): Promise<string> {
|
): Promise<TransactionReceipt> {
|
||||||
const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, {
|
const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, {
|
||||||
from: address
|
from: address
|
||||||
})
|
})
|
||||||
@ -542,6 +542,7 @@ export class DataTokens {
|
|||||||
})
|
})
|
||||||
return trxReceipt
|
return trxReceipt
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.logger.error('ERROR: Propose minter failed')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -552,7 +553,10 @@ export class DataTokens {
|
|||||||
* @param {String} address - only proposad minter can call this
|
* @param {String} address - only proposad minter can call this
|
||||||
* @return {Promise<string>} transactionId
|
* @return {Promise<string>} transactionId
|
||||||
*/
|
*/
|
||||||
public async approveMinter(dataTokenAddress: string, address: string): Promise<string> {
|
public async approveMinter(
|
||||||
|
dataTokenAddress: string,
|
||||||
|
address: string
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, {
|
const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, {
|
||||||
from: address
|
from: address
|
||||||
})
|
})
|
||||||
|
350
src/dispenser/Dispenser.ts
Normal file
350
src/dispenser/Dispenser.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import defaultDispenserABI from '@oceanprotocol/contracts/artifacts/Dispenser.json'
|
||||||
|
import { TransactionReceipt } from 'web3-core'
|
||||||
|
import { Contract } from 'web3-eth-contract'
|
||||||
|
import { AbiItem } from 'web3-utils/types'
|
||||||
|
import Web3 from 'web3'
|
||||||
|
import { SubscribablePromise, Logger, getFairGasPrice } from '../utils'
|
||||||
|
import { DataTokens } from '../datatokens/Datatokens'
|
||||||
|
import Decimal from 'decimal.js'
|
||||||
|
|
||||||
|
export interface DispenserToken {
|
||||||
|
active: boolean
|
||||||
|
owner: string
|
||||||
|
minterApproved: boolean
|
||||||
|
isTrueMinter: boolean
|
||||||
|
maxTokens: string
|
||||||
|
maxBalance: string
|
||||||
|
balance: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DispenserMakeMinterProgressStep {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MakeDispenserMinter,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
AcceptingNewMinter
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DispenserCancelMinterProgressStep {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MakeOwnerMinter,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
AcceptingNewMinter
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OceanDispenser {
|
||||||
|
public GASLIMIT_DEFAULT = 1000000
|
||||||
|
/** Ocean related functions */
|
||||||
|
public dispenserAddress: string
|
||||||
|
public dispenserABI: AbiItem | AbiItem[]
|
||||||
|
public web3: Web3
|
||||||
|
public contract: Contract = null
|
||||||
|
private logger: Logger
|
||||||
|
public datatokens: DataTokens
|
||||||
|
public startBlock: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate Dispenser
|
||||||
|
* @param {any} web3
|
||||||
|
* @param {String} dispenserAddress
|
||||||
|
* @param {any} dispenserABI
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
web3: Web3,
|
||||||
|
logger: Logger,
|
||||||
|
dispenserAddress: string = null,
|
||||||
|
dispenserABI: AbiItem | AbiItem[] = null,
|
||||||
|
datatokens: DataTokens,
|
||||||
|
startBlock?: number
|
||||||
|
) {
|
||||||
|
this.web3 = web3
|
||||||
|
this.dispenserAddress = dispenserAddress
|
||||||
|
if (startBlock) this.startBlock = startBlock
|
||||||
|
else this.startBlock = 0
|
||||||
|
this.dispenserABI = dispenserABI || (defaultDispenserABI.abi as AbiItem[])
|
||||||
|
this.datatokens = datatokens
|
||||||
|
if (web3)
|
||||||
|
this.contract = new this.web3.eth.Contract(this.dispenserABI, this.dispenserAddress)
|
||||||
|
this.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dispenser status for a datatoken
|
||||||
|
* @param {String} dataTokenAddress
|
||||||
|
* @return {Promise<FixedPricedExchange>} Exchange details
|
||||||
|
*/
|
||||||
|
public async status(dataTokenAddress: string): Promise<DispenserToken> {
|
||||||
|
try {
|
||||||
|
const result: DispenserToken = await this.contract.methods
|
||||||
|
.status(dataTokenAddress)
|
||||||
|
.call()
|
||||||
|
result.maxTokens = this.web3.utils.fromWei(result.maxTokens)
|
||||||
|
result.maxBalance = this.web3.utils.fromWei(result.maxBalance)
|
||||||
|
result.balance = this.web3.utils.fromWei(result.balance)
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`No dispenser available for data token: ${dataTokenAddress}`)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates a new dispener.
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {Number} maxTokens max amount of tokens to dispense
|
||||||
|
* @param {Number} maxBalance max balance of user. If user balance is >, then dispense will be rejected
|
||||||
|
* @param {String} address User address (must be owner of the dataToken)
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public async activate(
|
||||||
|
dataToken: string,
|
||||||
|
maxTokens: string,
|
||||||
|
maxBalance: string,
|
||||||
|
address: string
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.activate(
|
||||||
|
dataToken,
|
||||||
|
this.web3.utils.toWei(maxTokens),
|
||||||
|
this.web3.utils.toWei(maxBalance)
|
||||||
|
)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods
|
||||||
|
.activate(
|
||||||
|
dataToken,
|
||||||
|
this.web3.utils.toWei(maxTokens),
|
||||||
|
this.web3.utils.toWei(maxBalance)
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to activate dispenser: ${e.message}`)
|
||||||
|
}
|
||||||
|
return trxReceipt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates a dispener.
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} address User address (must be owner of the dispenser)
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public async deactivate(
|
||||||
|
dataToken: string,
|
||||||
|
address: string
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.deactivate(dataToken)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods.deactivate(dataToken).send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to deactivate dispenser: ${e.message}`)
|
||||||
|
}
|
||||||
|
return trxReceipt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the dispenser minter of the datatoken
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} address User address (must be owner of the datatoken)
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public makeMinter(
|
||||||
|
dataToken: string,
|
||||||
|
address: string
|
||||||
|
): SubscribablePromise<DispenserMakeMinterProgressStep, TransactionReceipt> {
|
||||||
|
return new SubscribablePromise(async (observer) => {
|
||||||
|
observer.next(DispenserMakeMinterProgressStep.MakeDispenserMinter)
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
const minterTx = await this.datatokens.proposeMinter(
|
||||||
|
dataToken,
|
||||||
|
this.dispenserAddress,
|
||||||
|
address
|
||||||
|
)
|
||||||
|
if (!minterTx) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
observer.next(DispenserMakeMinterProgressStep.AcceptingNewMinter)
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.acceptMinter(dataToken)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) =>
|
||||||
|
err ? gasLimitDefault : estGas
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods.acceptMinter(dataToken).send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to accept minter role: ${e.message}`)
|
||||||
|
}
|
||||||
|
return trxReceipt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel minter role of dispenser and make the owner minter of the datatoken
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} address User address (must be owner of the dispenser)
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public cancelMinter(
|
||||||
|
dataToken: string,
|
||||||
|
address: string
|
||||||
|
): SubscribablePromise<DispenserCancelMinterProgressStep, TransactionReceipt> {
|
||||||
|
return new SubscribablePromise(async (observer) => {
|
||||||
|
observer.next(DispenserCancelMinterProgressStep.MakeOwnerMinter)
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.removeMinter(dataToken)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) =>
|
||||||
|
err ? gasLimitDefault : estGas
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods.removeMinter(dataToken).send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to remove minter role: ${e.message}`)
|
||||||
|
}
|
||||||
|
if (!trxReceipt) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
observer.next(DispenserCancelMinterProgressStep.AcceptingNewMinter)
|
||||||
|
const minterTx = await this.datatokens.approveMinter(dataToken, address)
|
||||||
|
return minterTx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request tokens from dispenser
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} amount
|
||||||
|
* @param {String} address User address
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public async dispense(
|
||||||
|
dataToken: string,
|
||||||
|
address: string,
|
||||||
|
amount: string = '1'
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.dispense(dataToken, this.web3.utils.toWei(amount))
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods
|
||||||
|
.dispense(dataToken, this.web3.utils.toWei(amount))
|
||||||
|
.send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to dispense tokens: ${e.message}`)
|
||||||
|
}
|
||||||
|
return trxReceipt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw all tokens from the dispenser (if any)
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} address User address (must be owner of the dispenser)
|
||||||
|
* @return {Promise<TransactionReceipt>} TransactionReceipt
|
||||||
|
*/
|
||||||
|
public async ownerWithdraw(
|
||||||
|
dataToken: string,
|
||||||
|
address: string
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
|
let estGas
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
try {
|
||||||
|
estGas = await this.contract.methods
|
||||||
|
.ownerWithdraw(dataToken)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
let trxReceipt = null
|
||||||
|
try {
|
||||||
|
trxReceipt = await this.contract.methods.ownerWithdraw(dataToken).send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to withdraw tokens: ${e.message}`)
|
||||||
|
}
|
||||||
|
return trxReceipt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tokens can be dispensed
|
||||||
|
* @param {String} dataToken
|
||||||
|
* @param {String} address User address that will receive datatokens
|
||||||
|
* @return {Promise<Boolean>}
|
||||||
|
*/
|
||||||
|
public async isDispensable(
|
||||||
|
dataToken: string,
|
||||||
|
address: string,
|
||||||
|
amount: string = '1'
|
||||||
|
): Promise<Boolean> {
|
||||||
|
const status = await this.status(dataToken)
|
||||||
|
if (!status) return false
|
||||||
|
// check active
|
||||||
|
if (status.active === false) return false
|
||||||
|
// check maxBalance
|
||||||
|
const userBalance = new Decimal(await this.datatokens.balance(dataToken, address))
|
||||||
|
if (userBalance.greaterThanOrEqualTo(status.maxBalance)) return false
|
||||||
|
// check maxAmount
|
||||||
|
if (new Decimal(String(amount)).greaterThan(status.maxTokens)) return false
|
||||||
|
// check dispenser balance
|
||||||
|
const contractBalance = new Decimal(status.balance)
|
||||||
|
if (contractBalance.greaterThanOrEqualTo(amount) || status.isTrueMinter === true)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,19 @@ export class Config {
|
|||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
public fixedRateExchangeAddressABI?: AbiItem | AbiItem[]
|
public fixedRateExchangeAddressABI?: AbiItem | AbiItem[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DispenserAddress
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public dispenserAddress?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DispenserABI
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
public dispenserABI?: AbiItem | AbiItem[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DDOContractAddress
|
* DDOContractAddress
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
import { Compute } from './Compute'
|
import { Compute } from './Compute'
|
||||||
import { OceanPool } from '../balancer/OceanPool'
|
import { OceanPool } from '../balancer/OceanPool'
|
||||||
import { OceanFixedRateExchange } from '../exchange/FixedRateExchange'
|
import { OceanFixedRateExchange } from '../exchange/FixedRateExchange'
|
||||||
|
import { OceanDispenser } from '../dispenser/Dispenser'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main interface for Ocean Protocol.
|
* Main interface for Ocean Protocol.
|
||||||
@ -72,6 +73,15 @@ export class Ocean extends Instantiable {
|
|||||||
instance.datatokens,
|
instance.datatokens,
|
||||||
instanceConfig.config.startBlock
|
instanceConfig.config.startBlock
|
||||||
)
|
)
|
||||||
|
instance.OceanDispenser = new OceanDispenser(
|
||||||
|
instanceConfig.config.web3Provider,
|
||||||
|
instanceConfig.logger,
|
||||||
|
instanceConfig.config.dispenserAddress,
|
||||||
|
instanceConfig.config.dispenserABI,
|
||||||
|
instance.datatokens,
|
||||||
|
instanceConfig.config.startBlock
|
||||||
|
)
|
||||||
|
|
||||||
instance.onChainMetadata = new OnChainMetadata(
|
instance.onChainMetadata = new OnChainMetadata(
|
||||||
instanceConfig.config.web3Provider,
|
instanceConfig.config.web3Provider,
|
||||||
instanceConfig.logger,
|
instanceConfig.logger,
|
||||||
@ -155,6 +165,12 @@ export class Ocean extends Instantiable {
|
|||||||
*/
|
*/
|
||||||
public fixedRateExchange: OceanFixedRateExchange
|
public fixedRateExchange: OceanFixedRateExchange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ocean Dispenser submodule
|
||||||
|
* @type {OceanDispenser}
|
||||||
|
*/
|
||||||
|
public OceanDispenser: OceanDispenser
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ocean tokens submodule
|
* Ocean tokens submodule
|
||||||
* @type {OceanTokens}
|
* @type {OceanTokens}
|
||||||
|
@ -35,6 +35,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: '0x1234',
|
factoryAddress: '0x1234',
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 0
|
startBlock: 0
|
||||||
},
|
},
|
||||||
@ -52,6 +53,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 0
|
startBlock: 0
|
||||||
},
|
},
|
||||||
@ -68,6 +70,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 9227563
|
startBlock: 9227563
|
||||||
},
|
},
|
||||||
@ -84,6 +87,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 7294090
|
startBlock: 7294090
|
||||||
},
|
},
|
||||||
@ -100,6 +104,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 11105459
|
startBlock: 11105459
|
||||||
},
|
},
|
||||||
@ -116,6 +121,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 11005222
|
startBlock: 11005222
|
||||||
},
|
},
|
||||||
@ -132,6 +138,7 @@ const configs: ConfigHelperConfig[] = [
|
|||||||
factoryAddress: null,
|
factoryAddress: null,
|
||||||
poolFactoryAddress: null,
|
poolFactoryAddress: null,
|
||||||
fixedRateExchangeAddress: null,
|
fixedRateExchangeAddress: null,
|
||||||
|
dispenserAddress: null,
|
||||||
metadataContractAddress: null,
|
metadataContractAddress: null,
|
||||||
startBlock: 90707
|
startBlock: 90707
|
||||||
}
|
}
|
||||||
@ -147,6 +154,7 @@ export class ConfigHelper {
|
|||||||
DTFactory,
|
DTFactory,
|
||||||
BFactory,
|
BFactory,
|
||||||
FixedRateExchange,
|
FixedRateExchange,
|
||||||
|
Dispenser,
|
||||||
Metadata,
|
Metadata,
|
||||||
Ocean
|
Ocean
|
||||||
} = DefaultContractsAddresses[network]
|
} = DefaultContractsAddresses[network]
|
||||||
@ -154,6 +162,7 @@ export class ConfigHelper {
|
|||||||
factoryAddress: DTFactory,
|
factoryAddress: DTFactory,
|
||||||
poolFactoryAddress: BFactory,
|
poolFactoryAddress: BFactory,
|
||||||
fixedRateExchangeAddress: FixedRateExchange,
|
fixedRateExchangeAddress: FixedRateExchange,
|
||||||
|
dispenserAddress: Dispenser,
|
||||||
metadataContractAddress: Metadata,
|
metadataContractAddress: Metadata,
|
||||||
oceanTokenAddress: Ocean,
|
oceanTokenAddress: Ocean,
|
||||||
...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI })
|
...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI })
|
||||||
@ -169,11 +178,19 @@ export class ConfigHelper {
|
|||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const { DTFactory, BFactory, FixedRateExchange, Metadata, Ocean } = data[network]
|
const {
|
||||||
|
DTFactory,
|
||||||
|
BFactory,
|
||||||
|
FixedRateExchange,
|
||||||
|
Dispenser,
|
||||||
|
Metadata,
|
||||||
|
Ocean
|
||||||
|
} = data[network]
|
||||||
configAddresses = {
|
configAddresses = {
|
||||||
factoryAddress: DTFactory,
|
factoryAddress: DTFactory,
|
||||||
poolFactoryAddress: BFactory,
|
poolFactoryAddress: BFactory,
|
||||||
fixedRateExchangeAddress: FixedRateExchange,
|
fixedRateExchangeAddress: FixedRateExchange,
|
||||||
|
dispenserAddress: Dispenser,
|
||||||
metadataContractAddress: Metadata,
|
metadataContractAddress: Metadata,
|
||||||
oceanTokenAddress: Ocean,
|
oceanTokenAddress: Ocean,
|
||||||
...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI })
|
...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI })
|
||||||
|
50
test/DispenserContractHandler.ts
Normal file
50
test/DispenserContractHandler.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import Web3 from 'web3'
|
||||||
|
import { Contract } from 'web3-eth-contract'
|
||||||
|
import { AbiItem } from 'web3-utils/types'
|
||||||
|
|
||||||
|
export class DispenserContractHandler {
|
||||||
|
public contract: Contract
|
||||||
|
public accounts: string[]
|
||||||
|
public contractBytecode: string
|
||||||
|
public contractAddress: string
|
||||||
|
public web3: Web3
|
||||||
|
|
||||||
|
constructor(contractABI: AbiItem[], contractBytecode: string, web3: Web3) {
|
||||||
|
this.web3 = web3
|
||||||
|
this.contract = new this.web3.eth.Contract(contractABI)
|
||||||
|
this.contractBytecode = contractBytecode
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAccounts(): Promise<string[]> {
|
||||||
|
this.accounts = await this.web3.eth.getAccounts()
|
||||||
|
return this.accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deployContracts() {
|
||||||
|
await this.getAccounts()
|
||||||
|
// get est gascost
|
||||||
|
const estGas = await this.contract
|
||||||
|
.deploy({
|
||||||
|
data: this.contractBytecode,
|
||||||
|
arguments: []
|
||||||
|
})
|
||||||
|
.estimateGas(function (err, estGas) {
|
||||||
|
if (err) console.log('DeployContracts: ' + err)
|
||||||
|
return estGas
|
||||||
|
})
|
||||||
|
// deploy the contract and get it's address
|
||||||
|
this.contractAddress = await this.contract
|
||||||
|
.deploy({
|
||||||
|
data: this.contractBytecode,
|
||||||
|
arguments: []
|
||||||
|
})
|
||||||
|
.send({
|
||||||
|
from: this.accounts[0],
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: '3000000000'
|
||||||
|
})
|
||||||
|
.then(function (contract) {
|
||||||
|
return contract.options.address
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
239
test/unit/dispenser/Dispenser.test.ts
Normal file
239
test/unit/dispenser/Dispenser.test.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import { AbiItem } from 'web3-utils/types'
|
||||||
|
import { TestContractHandler } from '../../TestContractHandler'
|
||||||
|
import { DispenserContractHandler } from '../../DispenserContractHandler'
|
||||||
|
import { DataTokens } from '../../../src/datatokens/Datatokens'
|
||||||
|
import { LoggerInstance } from '../../../src/utils'
|
||||||
|
import Web3 from 'web3'
|
||||||
|
import factory from '@oceanprotocol/contracts/artifacts/DTFactory.json'
|
||||||
|
import datatokensTemplate from '@oceanprotocol/contracts/artifacts/DataTokenTemplate.json'
|
||||||
|
import { OceanDispenser } from '../../../src/dispenser/Dispenser'
|
||||||
|
|
||||||
|
import DispenserContract = require('@oceanprotocol/contracts/artifacts/Dispenser.json')
|
||||||
|
const web3 = new Web3('http://127.0.0.1:8545')
|
||||||
|
|
||||||
|
describe('Dispenser flow', () => {
|
||||||
|
let DispenserAddress: string
|
||||||
|
let DispenserClass
|
||||||
|
let bob: string
|
||||||
|
let alice: string
|
||||||
|
let charlie: string
|
||||||
|
let datatoken: DataTokens
|
||||||
|
let tokenAddress: string
|
||||||
|
let tokenAddress2: string
|
||||||
|
let tokenAddress3: string
|
||||||
|
|
||||||
|
let owner: string
|
||||||
|
let contracts: TestContractHandler
|
||||||
|
|
||||||
|
const consoleDebug = false
|
||||||
|
const tokenAmount = '1000'
|
||||||
|
const blob = 'http://localhost:8030/api/v1/services/consume'
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
// deploy SFactory
|
||||||
|
const Contracts = new DispenserContractHandler(
|
||||||
|
DispenserContract.abi as AbiItem[],
|
||||||
|
DispenserContract.bytecode,
|
||||||
|
web3
|
||||||
|
)
|
||||||
|
await Contracts.getAccounts()
|
||||||
|
owner = Contracts.accounts[0]
|
||||||
|
|
||||||
|
await Contracts.deployContracts()
|
||||||
|
DispenserAddress = Contracts.contractAddress
|
||||||
|
assert(DispenserAddress !== null)
|
||||||
|
|
||||||
|
// deploy DT Factory
|
||||||
|
contracts = new TestContractHandler(
|
||||||
|
factory.abi as AbiItem[],
|
||||||
|
datatokensTemplate.abi as AbiItem[],
|
||||||
|
datatokensTemplate.bytecode,
|
||||||
|
factory.bytecode,
|
||||||
|
web3
|
||||||
|
)
|
||||||
|
await contracts.getAccounts()
|
||||||
|
owner = contracts.accounts[0]
|
||||||
|
alice = contracts.accounts[1]
|
||||||
|
bob = contracts.accounts[2]
|
||||||
|
charlie = contracts.accounts[3]
|
||||||
|
await contracts.deployContracts(owner)
|
||||||
|
|
||||||
|
// initialize DataTokens
|
||||||
|
datatoken = new DataTokens(
|
||||||
|
contracts.factoryAddress,
|
||||||
|
factory.abi as AbiItem[],
|
||||||
|
datatokensTemplate.abi as AbiItem[],
|
||||||
|
web3,
|
||||||
|
LoggerInstance
|
||||||
|
)
|
||||||
|
assert(datatoken !== null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create some datatoken2', async () => {
|
||||||
|
tokenAddress = await datatoken.create(
|
||||||
|
blob,
|
||||||
|
alice,
|
||||||
|
'1000000000000000',
|
||||||
|
'AliceDT',
|
||||||
|
'DTA'
|
||||||
|
)
|
||||||
|
assert(tokenAddress !== null)
|
||||||
|
if (consoleDebug) console.log("Alice's address:" + alice)
|
||||||
|
if (consoleDebug) console.log('data Token address:' + tokenAddress)
|
||||||
|
tokenAddress2 = await datatoken.create(
|
||||||
|
blob,
|
||||||
|
alice,
|
||||||
|
'1000000000000000',
|
||||||
|
'AliceDT2',
|
||||||
|
'DTA2'
|
||||||
|
)
|
||||||
|
assert(tokenAddress2 !== null)
|
||||||
|
tokenAddress3 = await datatoken.create(
|
||||||
|
blob,
|
||||||
|
alice,
|
||||||
|
'1000000000000000',
|
||||||
|
'AliceDT3',
|
||||||
|
'DTA3'
|
||||||
|
)
|
||||||
|
assert(tokenAddress3 !== null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize Dispenser class', async () => {
|
||||||
|
DispenserClass = new OceanDispenser(
|
||||||
|
web3,
|
||||||
|
LoggerInstance,
|
||||||
|
DispenserAddress,
|
||||||
|
DispenserContract.abi as AbiItem[],
|
||||||
|
datatoken
|
||||||
|
)
|
||||||
|
assert(DispenserClass !== null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice mints 1000 tokens', async () => {
|
||||||
|
const txid = await datatoken.mint(tokenAddress, alice, tokenAmount)
|
||||||
|
if (consoleDebug) console.log(txid)
|
||||||
|
assert(txid !== null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice creates a dispenser', async () => {
|
||||||
|
const tx = await DispenserClass.activate(tokenAddress, '1', '1', alice)
|
||||||
|
assert(tx, 'Cannot activate dispenser')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice should make the dispenser a minter', async () => {
|
||||||
|
const tx = await DispenserClass.makeMinter(tokenAddress, alice)
|
||||||
|
assert(tx, 'Cannot make dispenser a minter')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice gets the dispenser status', async () => {
|
||||||
|
const status = await DispenserClass.status(tokenAddress)
|
||||||
|
assert(status.active === true, 'Dispenser not active')
|
||||||
|
assert(status.owner === alice, 'Dispenser owner is not alice')
|
||||||
|
assert(status.minterApproved === true, 'Dispenser is not a minter')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice should fail to get the dispenser status for an unknown token', async () => {
|
||||||
|
const status = await DispenserClass.status(tokenAddress3)
|
||||||
|
assert(
|
||||||
|
status.owner === '0x0000000000000000000000000000000000000000',
|
||||||
|
'Owner of inexistent dispenser should be 0'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice should fail to get the dispenser status for zero address', async () => {
|
||||||
|
const status = await DispenserClass.status(0x0000000000000000000000000000000000000000)
|
||||||
|
assert(status === null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob requests more datatokens then allowed', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress, bob, '10')
|
||||||
|
assert(check === false, 'isDispensable should return false')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress, bob, '10')
|
||||||
|
assert(tx === null, 'Request should fail')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob requests datatokens', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress, bob, '1')
|
||||||
|
assert(check === true, 'isDispensable should return true')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress, bob, '1')
|
||||||
|
assert(tx, 'Bob failed to get 1DT')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob requests more datatokens but he exceeds maxBalance', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress, bob, '10')
|
||||||
|
assert(check === false, 'isDispensable should return false')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress, bob, '10')
|
||||||
|
assert(tx === null, 'Request should fail')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice deactivates the dispenser', async () => {
|
||||||
|
const tx = await DispenserClass.deactivate(tokenAddress, alice)
|
||||||
|
assert(tx, 'Cannot deactivate dispenser')
|
||||||
|
const status = await DispenserClass.status(tokenAddress)
|
||||||
|
assert(status.active === false, 'Dispenser is still active')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Charlie should fail to get datatokens', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress, charlie, '1')
|
||||||
|
assert(check === false, 'isDispensable should return false')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress, charlie, '1')
|
||||||
|
assert(tx === null, 'Charlie should fail to get 1DT')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice calls removeMinter role and checks if she is the new minter', async () => {
|
||||||
|
const tx = await DispenserClass.cancelMinter(tokenAddress, alice)
|
||||||
|
assert(tx, 'Cannot cancel minter role')
|
||||||
|
const status = await DispenserClass.status(tokenAddress)
|
||||||
|
assert(status.minterApproved === false, 'Dispenser is still a minter')
|
||||||
|
assert(status.owner === alice, 'Dispenser is not owned by Alice')
|
||||||
|
const isMinter = await datatoken.isMinter(tokenAddress, alice)
|
||||||
|
assert(isMinter === true, 'ALice is not the minter')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob should fail to activate a dispenser for a token for he is not a minter', async () => {
|
||||||
|
const tx = await DispenserClass.activate(tokenAddress, '1', '1', bob)
|
||||||
|
assert(tx === null, 'Bob should fail to activate dispenser')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice creates a dispenser without minter role', async () => {
|
||||||
|
const tx = await DispenserClass.activate(tokenAddress2, '1', '1', alice)
|
||||||
|
assert(tx, 'Cannot activate dispenser')
|
||||||
|
})
|
||||||
|
it('Bob requests datatokens but there are none', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress2, bob, '1')
|
||||||
|
assert(check === false, 'isDispensable should return false')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress2, bob, '1')
|
||||||
|
assert(tx === null, 'Request should fail')
|
||||||
|
})
|
||||||
|
it('Alice mints tokens and transfer them to the dispenser.', async () => {
|
||||||
|
const mintTx = await datatoken.mint(
|
||||||
|
tokenAddress2,
|
||||||
|
alice,
|
||||||
|
'10',
|
||||||
|
DispenserClass.dispenserAddress
|
||||||
|
)
|
||||||
|
assert(mintTx, 'Alice cannot mint tokens')
|
||||||
|
const status = await DispenserClass.status(tokenAddress2)
|
||||||
|
assert(status.balance > 0, 'Balances do not match')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob requests datatokens', async () => {
|
||||||
|
const check = await DispenserClass.isDispensable(tokenAddress2, bob, '1')
|
||||||
|
assert(check === true, 'isDispensable should return true')
|
||||||
|
const tx = await DispenserClass.dispense(tokenAddress2, bob, '1')
|
||||||
|
assert(tx, 'Bob failed to get 1DT')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Bob tries to withdraw all datatokens', async () => {
|
||||||
|
const tx = await DispenserClass.ownerWithdraw(tokenAddress2, bob)
|
||||||
|
assert(tx === null, 'Request should fail')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Alice withdraws all datatokens', async () => {
|
||||||
|
const tx = await DispenserClass.ownerWithdraw(tokenAddress2, alice)
|
||||||
|
assert(tx, 'Alice failed to withdraw all her tokens')
|
||||||
|
const status = await DispenserClass.status(tokenAddress2)
|
||||||
|
assert(status.balance === '0', 'Balance > 0')
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user