From 5072bdbb5a756ddc2e55e60015805f9e45076d80 Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Fri, 29 Oct 2021 12:54:35 +0300 Subject: [PATCH] integrated dispenser class, added some unit tests --- src/pools/dispenser/Dispenser.ts | 406 ++++++++++++++++++++++++++++++ test/unit/pools/Dispenser.test.ts | 129 ++++++++++ 2 files changed, 535 insertions(+) create mode 100644 test/unit/pools/Dispenser.test.ts diff --git a/src/pools/dispenser/Dispenser.ts b/src/pools/dispenser/Dispenser.ts index 0c982d3f..094519f6 100644 --- a/src/pools/dispenser/Dispenser.ts +++ b/src/pools/dispenser/Dispenser.ts @@ -1,6 +1,412 @@ import Web3 from 'web3' +import { AbiItem } from 'web3-utils' +import { Contract } from 'web3-eth-contract' +import { TransactionReceipt } from 'web3-eth' +import defaultDispenserABI from '@oceanprotocol/contracts/pools/dispenser/Dispenser.json' +import { LoggerInstance as logger, getFairGasPrice } from '../../utils/' + +export interface DispenserToken { + active: boolean + owner: string + maxTokens: string + maxBalance: string + balance: string + minterApproved: boolean + isTrueMinter: boolean + allowedSwapper: string +} export class Dispenser { public GASLIMIT_DEFAULT = 1000000 public web3: Web3 = null + public dispenserAddress: string + public startBlock: number + public dispenserABI: AbiItem | AbiItem[] + public dispenserContract: Contract + + /** + * Instantiate Dispenser + * @param {any} web3 + * @param {String} dispenserAddress + * @param {any} dispenserABI + */ + constructor( + web3: Web3, + dispenserAddress: string = null, + dispenserABI: AbiItem | AbiItem[] = null, + startBlock?: number + ) { + this.web3 = web3 + this.dispenserAddress = dispenserAddress + if (startBlock) this.startBlock = startBlock + else this.startBlock = 0 + this.dispenserABI = dispenserABI || (defaultDispenserABI.abi as AbiItem[]) + if (web3) + this.dispenserContract = new this.web3.eth.Contract( + this.dispenserABI, + this.dispenserAddress + ) + } + + /** + * Get information about a datatoken dispenser + * @param {String} dtAddress + * @return {Promise} Exchange details + */ + public async status(dtAdress: string): Promise { + try { + const result: DispenserToken = await this.dispenserContract.methods + .status(dtAdress) + .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) { + logger.warn(`No dispenser available for data token: ${dtAdress}`) + } + return null + } + + /** + * Estimate gas cost for create method + * @param {String} dtAddress Datatoken address + * @param {String} address Owner address + * @param {String} maxTokens max tokens to dispense + * @param {String} maxBalance max balance of requester + * @param {String} allowedSwapper if !=0, only this address can request DTs + * @return {Promise} + */ + public async estGasCreate( + dtAddress: string, + address: string, + maxTokens: string, + maxBalance: string, + allowedSwapper: string + ): Promise { + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await this.dispenserContract.methods + .create( + dtAddress, + this.web3.utils.toWei(maxTokens), + this.web3.utils.toWei(maxBalance), + address, + allowedSwapper + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + return estGas + } + + /** + * Creates a new Dispenser + * @param {String} dtAddress Datatoken address + * @param {String} address Owner address + * @param {String} maxTokens max tokens to dispense + * @param {String} maxBalance max balance of requester + * @param {String} allowedSwapper only account that can ask tokens. set address(0) if not required + * @return {Promise} transactionId + */ + public async create( + dtAddress: string, + address: string, + maxTokens: string, + maxBalance: string, + allowedSwapper: string + ): Promise { + const estGas = await this.estGasCreateDispenser( + dtAddress, + address, + maxTokens, + maxBalance, + allowedSwapper + ) + + // Call createFixedRate contract method + const trxReceipt = await this.dispenserContract.methods + .create( + dtAddress, + this.web3.utils.toWei(maxTokens), + this.web3.utils.toWei(maxBalance), + address, + allowedSwapper + ) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } + + /** + * Estimate gas for activate method + * @param {String} dtAddress + * @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} + */ + public async estGasActivate( + dtAddress: string, + maxTokens: string, + maxBalance: string, + address: string + ): Promise { + let estGas + const gasLimitDefault = this.GASLIMIT_DEFAULT + try { + estGas = await this.dispenserContract.methods + .activate( + dtAddress, + this.web3.utils.toWei(maxTokens), + this.web3.utils.toWei(maxBalance) + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Activates a new dispener. + * @param {String} dtAddress refers to datatoken address. + * @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( + dtAddress: string, + maxTokens: string, + maxBalance: string, + address: string + ): Promise { + try { + const estGas = await this.estGasActivate(dtAddress, maxTokens, maxBalance, address) + const trxReceipt = await this.dispenserContract.methods + .activate( + dtAddress, + this.web3.utils.toWei(maxTokens), + this.web3.utils.toWei(maxBalance) + ) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + logger.error(`ERROR: Failed to activate dispenser: ${e.message}`) + } + return null + } + + /** + * Estimate gas for deactivate method + * @param {String} dtAddress + * @param {String} address User address (must be owner of the dataToken) + * @return {Promise} + */ + public async estGasDeactivate(dtAddress: string, address: string): Promise { + let estGas + const gasLimitDefault = this.GASLIMIT_DEFAULT + try { + estGas = await this.dispenserContract.methods + .deactivate(dtAddress) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Deactivate an existing dispenser. + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dataToken) + * @return {Promise} TransactionReceipt + */ + public async deactivate( + dtAddress: string, + address: string + ): Promise { + try { + const estGas = await this.estGasDeactivate(dtAddress, address) + const trxReceipt = await this.dispenserContract.methods.deactivate(dtAddress).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + logger.error(`ERROR: Failed to activate dispenser: ${e.message}`) + } + return null + } + + /** + * Estimate gas for setAllowedSwapper method + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dataToken) + * @param {String} newAllowedSwapper refers to the new allowedSwapper + * @return {Promise} + */ + public async estGasSetAllowedSwapper( + dtAddress: string, + address: string, + newAllowedSwapper: string + ): Promise { + let estGas + const gasLimitDefault = this.GASLIMIT_DEFAULT + try { + estGas = await this.dispenserContract.methods + .setAllowedSwapper(dtAddress, newAllowedSwapper) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Sets a new allowedSwapper. + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dataToken) + * @param {String} newAllowedSwapper refers to the new allowedSwapper + * @return {Promise} TransactionReceipt + */ + public async setAllowedSwapper( + dtAddress: string, + address: string, + newAllowedSwapper: string + ): Promise { + try { + const estGas = await this.estGasSetAllowedSwapper( + dtAddress, + address, + newAllowedSwapper + ) + const trxReceipt = await this.dispenserContract.methods + .setAllowedSwapper(dtAddress, newAllowedSwapper) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + logger.error(`ERROR: Failed to activate dispenser: ${e.message}`) + } + return null + } + + /** + * Estimate gas for dispense method + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dataToken) + * @param {String} newAllowedSwapper refers to the new allowedSwapper + * @return {Promise} + */ + public async estGasDispense( + dtAddress: string, + address: string, + amount: string = '1', + destination: string + ): Promise { + let estGas + const gasLimitDefault = this.GASLIMIT_DEFAULT + try { + estGas = await this.dispenserContract.methods + .dispense(dtAddress, this.web3.utils.toWei(amount), destination) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Dispense datatokens to caller. + * The dispenser must be active, hold enough DT (or be able to mint more) + * and respect maxTokens/maxBalance requirements + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address + * @param {String} amount amount of datatokens required. + * @param {String} destination who will receive the tokens + * @return {Promise} TransactionReceipt + */ + public async dispense( + dtAddress: string, + address: string, + amount: string = '1', + destination: string + ): Promise { + const estGas = await this.estGasDispense(dtAddress, address, amount, destination) + try { + const trxReceipt = await this.dispenserContract.methods + .dispense(dtAddress, this.web3.utils.toWei(amount), destination) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + logger.error(`ERROR: Failed to dispense tokens: ${e.message}`) + } + return null + } + + /** + * Estimate gas for ownerWithdraw method + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dataToken) + * @param {String} newAllowedSwapper refers to the new allowedSwapper + * @return {Promise} + */ + public async estGasOwnerWithdraw(dtAddress: string, address: string): Promise { + let estGas + const gasLimitDefault = this.GASLIMIT_DEFAULT + try { + estGas = await this.dispenserContract.methods + .ownerWithdraw(dtAddress) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Withdraw all tokens from the dispenser + * @param {String} dtAddress refers to datatoken address. + * @param {String} address User address (must be owner of the dispenser) + * @return {Promise} TransactionReceipt + */ + public async ownerWithdraw( + dtAddress: string, + address: string + ): Promise { + const estGas = await this.estGasOwnerWithdraw(dtAddress, address) + try { + const trxReceipt = await this.dispenserContract.methods + .ownerWithdraw(dtAddress) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + logger.error(`ERROR: Failed to withdraw tokens: ${e.message}`) + } + return null + } } diff --git a/test/unit/pools/Dispenser.test.ts b/test/unit/pools/Dispenser.test.ts new file mode 100644 index 00000000..0d3ac239 --- /dev/null +++ b/test/unit/pools/Dispenser.test.ts @@ -0,0 +1,129 @@ +import Web3 from 'web3' +import { AbiItem, AbiInput } from 'web3-utils' +import { assert, expect } from 'chai' +import ERC721Factory from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json' +import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' +import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import FactoryRouter from '@oceanprotocol/contracts/artifacts/contracts/pools/FactoryRouter.sol/FactoryRouter.json' +import DispenserTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/dispenser/Dispenser.sol/Dispenser.json' +import { LoggerInstance } from '.../../../src/utils' +import { Dispenser } from '.../../../src/pools/dispenser/' +import { NFTFactory } from '.../../../src/factories/' +import { Datatoken } from '.../../../src/datatokens/' +import { TestContractHandler } from '../../TestContractHandler' + +const web3 = new Web3('http://127.0.0.1:8545') + +describe('Dispenser flow', () => { + let factoryOwner: string + let nftOwner: string + let user1: string + let user2: string + let user3: string + let contracts: TestContractHandler + let DispenserAddress: string + let DispenserClass: Dispenser + let nftFactory: NFTFactory + let datatoken: Datatoken + let nftAddress: string + let dtAddress: string + + it('should deploy contracts', async () => { + contracts = new TestContractHandler( + web3, + ERC721Template.abi as AbiItem, + ERC20Template.abi as AbiItem, + null, + ERC721Factory.abi as AbiItem, + null, + null, + null, + DispenserTemplate.abi as AbiItem, + ERC721Template.bytecode, + ERC20Template.bytecode, + null, + ERC721Factory.bytecode, + FactoryRouter.bytecode, + null, + null, + DispenserTemplate.bytecode + ) + await contracts.getAccounts() + factoryOwner = contracts.accounts[0] + nftOwner = contracts.accounts[1] + user1 = contracts.accounts[2] + user2 = contracts.accounts[3] + user3 = contracts.accounts[4] + + await contracts.deployContracts(factoryOwner, FactoryRouter.abi as AbiItem[]) + }) + + it('should initialize Dispenser class', async () => { + DispenserClass = new Dispenser( + web3, + DispenserAddress, + DispenserTemplate.abi as AbiItem[] + ) + assert(DispenserClass !== null) + }) + + it('#createNftwithErc - should create an NFT and a Datatoken ', async () => { + nftFactory = new NFTFactory(contracts.factory721Address, web3, LoggerInstance) + + const nftData = { + name: '72120Bundle', + symbol: '72Bundle', + templateIndex: 1, + baseURI: 'https://oceanprotocol.com/nft/' + } + const ercData = { + templateIndex: 1, + strings: ['ERC20B1', 'ERC20DT1Symbol'], + addresses: [ + contracts.accounts[0], + user3, + user2, + '0x0000000000000000000000000000000000000000' + ], + uints: [web3.utils.toWei('10000'), 0], + bytess: [] + } + + const txReceipt = await nftFactory.createNftWithErc( + contracts.accounts[0], + nftData, + ercData + ) + + expect(txReceipt.events.NFTCreated.event === 'NFTCreated') + expect(txReceipt.events.TokenCreated.event === 'TokenCreated') + + nftAddress = txReceipt.events.NFTCreated.returnValues.newTokenAddress + dtAddress = txReceipt.events.TokenCreated.returnValues.newTokenAddress + }) + + it('Make user2 minter', async () => { + datatoken = new Datatoken(web3, ERC20Template.abi as AbiItem) + await datatoken.addMinter(dtAddress, nftOwner, user2) + assert((await datatoken.getDTPermissions(dtAddress, user2)).minter === true) + }) + + it('user2 creates a dispenser', async () => { + const tx = await DispenserClass.activate(dtAddress, '1', '1', user2) + assert(tx, 'Cannot activate dispenser') + }) + + it('user2 gets the dispenser status', async () => { + const status = await DispenserClass.status(dtAddress) + assert(status.active === true, 'Dispenser not active') + assert(status.owner === user2, 'Dispenser owner is not alice') + assert(status.minterApproved === true, 'Dispenser is not a minter') + }) + + it('user2 deactivates the dispenser', async () => { + const tx = await DispenserClass.deactivate(dtAddress, user2) + assert(tx, 'Cannot deactivate dispenser') + const status = await DispenserClass.status(dtAddress) + assert(status.active === false, 'Dispenser is still active') + }) +})