From 92b4be0dce525cc1016af75f6792a75620a54247 Mon Sep 17 00:00:00 2001 From: Alex Coseru Date: Mon, 10 May 2021 18:42:19 +0300 Subject: [PATCH] Feature/dispenser (#790) * add dispenser support * bump contracts to 0.6.2 --- package-lock.json | 6 +- package.json | 2 +- src/datatokens/Datatokens.ts | 8 +- src/dispenser/Dispenser.ts | 350 ++++++++++++++++++++++++++ src/models/Config.ts | 13 + src/ocean/Ocean.ts | 16 ++ src/utils/ConfigHelper.ts | 19 +- test/DispenserContractHandler.ts | 50 ++++ test/unit/dispenser/Dispenser.test.ts | 239 ++++++++++++++++++ 9 files changed, 696 insertions(+), 7 deletions(-) create mode 100644 src/dispenser/Dispenser.ts create mode 100644 test/DispenserContractHandler.ts create mode 100644 test/unit/dispenser/Dispenser.test.ts diff --git a/package-lock.json b/package-lock.json index fdd8f1ab..662ece11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2198,9 +2198,9 @@ } }, "@oceanprotocol/contracts": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.5.16.tgz", - "integrity": "sha512-p7aFIUT8RVoMzdPP7ML8G08BnQ09syywKjOT16hqJm0GmofunEuVffUXbryG4EkQ+qRbf/zeoxSmesi79kQXlA==" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.6.2.tgz", + "integrity": "sha512-J6amHsmVbdc2rAwbUYOaY7inLV13GxPIiqbsLF78nmdIvhhGDhT2LYMyfQtxkMwQzYDP6EzD4albCgOXlWM15g==" }, "@octokit/auth-token": { "version": "2.4.5", diff --git a/package.json b/package.json index 54589277..9e871310 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@ethereum-navigator/navigator": "^0.5.2", - "@oceanprotocol/contracts": "0.5.16", + "@oceanprotocol/contracts": "^0.6.2", "@types/crypto-js": "^4.0.1", "cross-fetch": "^3.1.2", "crypto-js": "^4.0.0", diff --git a/src/datatokens/Datatokens.ts b/src/datatokens/Datatokens.ts index 00369ecc..265c7064 100644 --- a/src/datatokens/Datatokens.ts +++ b/src/datatokens/Datatokens.ts @@ -521,7 +521,7 @@ export class DataTokens { dataTokenAddress: string, newMinterAddress: string, address: string - ): Promise { + ): Promise { const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, { from: address }) @@ -542,6 +542,7 @@ export class DataTokens { }) return trxReceipt } catch (e) { + this.logger.error('ERROR: Propose minter failed') return null } } @@ -552,7 +553,10 @@ export class DataTokens { * @param {String} address - only proposad minter can call this * @return {Promise} transactionId */ - public async approveMinter(dataTokenAddress: string, address: string): Promise { + public async approveMinter( + dataTokenAddress: string, + address: string + ): Promise { const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, { from: address }) diff --git a/src/dispenser/Dispenser.ts b/src/dispenser/Dispenser.ts new file mode 100644 index 00000000..972910d3 --- /dev/null +++ b/src/dispenser/Dispenser.ts @@ -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} Exchange details + */ + public async status(dataTokenAddress: string): Promise { + 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 + */ + public async activate( + dataToken: string, + maxTokens: string, + maxBalance: string, + address: string + ): Promise { + 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 + */ + public async deactivate( + dataToken: string, + address: string + ): Promise { + 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 + */ + public makeMinter( + dataToken: string, + address: string + ): SubscribablePromise { + 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 + */ + public cancelMinter( + dataToken: string, + address: string + ): SubscribablePromise { + 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 + */ + public async dispense( + dataToken: string, + address: string, + amount: string = '1' + ): Promise { + 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 + */ + public async ownerWithdraw( + dataToken: string, + address: string + ): Promise { + 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} + */ + public async isDispensable( + dataToken: string, + address: string, + amount: string = '1' + ): Promise { + 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 + } +} diff --git a/src/models/Config.ts b/src/models/Config.ts index e7a3e924..be23d525 100644 --- a/src/models/Config.ts +++ b/src/models/Config.ts @@ -85,6 +85,19 @@ export class Config { * @type {any} */ public fixedRateExchangeAddressABI?: AbiItem | AbiItem[] + + /** + * DispenserAddress + * @type {string} + */ + public dispenserAddress?: string + + /** + * DispenserABI + * @type {any} + */ + public dispenserABI?: AbiItem | AbiItem[] + /** * DDOContractAddress * @type {string} diff --git a/src/ocean/Ocean.ts b/src/ocean/Ocean.ts index afec3a5d..95da7e33 100644 --- a/src/ocean/Ocean.ts +++ b/src/ocean/Ocean.ts @@ -15,6 +15,7 @@ import { import { Compute } from './Compute' import { OceanPool } from '../balancer/OceanPool' import { OceanFixedRateExchange } from '../exchange/FixedRateExchange' +import { OceanDispenser } from '../dispenser/Dispenser' /** * Main interface for Ocean Protocol. @@ -72,6 +73,15 @@ export class Ocean extends Instantiable { instance.datatokens, 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( instanceConfig.config.web3Provider, instanceConfig.logger, @@ -155,6 +165,12 @@ export class Ocean extends Instantiable { */ public fixedRateExchange: OceanFixedRateExchange + /** + * Ocean Dispenser submodule + * @type {OceanDispenser} + */ + public OceanDispenser: OceanDispenser + /** * Ocean tokens submodule * @type {OceanTokens} diff --git a/src/utils/ConfigHelper.ts b/src/utils/ConfigHelper.ts index 69ca04e4..05db7929 100644 --- a/src/utils/ConfigHelper.ts +++ b/src/utils/ConfigHelper.ts @@ -35,6 +35,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: '0x1234', poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 0 }, @@ -52,6 +53,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 0 }, @@ -68,6 +70,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 9227563 }, @@ -84,6 +87,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 7294090 }, @@ -100,6 +104,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 11105459 }, @@ -116,6 +121,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 11005222 }, @@ -132,6 +138,7 @@ const configs: ConfigHelperConfig[] = [ factoryAddress: null, poolFactoryAddress: null, fixedRateExchangeAddress: null, + dispenserAddress: null, metadataContractAddress: null, startBlock: 90707 } @@ -147,6 +154,7 @@ export class ConfigHelper { DTFactory, BFactory, FixedRateExchange, + Dispenser, Metadata, Ocean } = DefaultContractsAddresses[network] @@ -154,6 +162,7 @@ export class ConfigHelper { factoryAddress: DTFactory, poolFactoryAddress: BFactory, fixedRateExchangeAddress: FixedRateExchange, + dispenserAddress: Dispenser, metadataContractAddress: Metadata, oceanTokenAddress: Ocean, ...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI }) @@ -169,11 +178,19 @@ export class ConfigHelper { 'utf8' ) ) - const { DTFactory, BFactory, FixedRateExchange, Metadata, Ocean } = data[network] + const { + DTFactory, + BFactory, + FixedRateExchange, + Dispenser, + Metadata, + Ocean + } = data[network] configAddresses = { factoryAddress: DTFactory, poolFactoryAddress: BFactory, fixedRateExchangeAddress: FixedRateExchange, + dispenserAddress: Dispenser, metadataContractAddress: Metadata, oceanTokenAddress: Ocean, ...(process.env.AQUARIUS_URI && { metadataCacheUri: process.env.AQUARIUS_URI }) diff --git a/test/DispenserContractHandler.ts b/test/DispenserContractHandler.ts new file mode 100644 index 00000000..8fdbb84f --- /dev/null +++ b/test/DispenserContractHandler.ts @@ -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 { + 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 + }) + } +} diff --git a/test/unit/dispenser/Dispenser.test.ts b/test/unit/dispenser/Dispenser.test.ts new file mode 100644 index 00000000..9260efd6 --- /dev/null +++ b/test/unit/dispenser/Dispenser.test.ts @@ -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') + }) +})