import { ethers, Signer } from 'ethers' import Decimal from 'decimal.js' import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' import ERC20TemplateEnterprise from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20TemplateEnterprise.sol/ERC20TemplateEnterprise.json' import { amountToUnits, sendTx, ZERO_ADDRESS } from '../utils' import { AbiItem, ConsumeMarketFee, FreOrderParams, FreCreationParams, ProviderFees, PublishingMarketFee, DispenserParams, OrderParams, DatatokenRoles, ReceiptOrEstimate } from '../@types' import { Nft } from './NFT' import { Config } from '../config' import { SmartContract } from './SmartContract' export class Datatoken extends SmartContract { public abiEnterprise: AbiItem[] public nft: Nft getDefaultAbi() { return ERC20Template.abi as AbiItem[] } /** * Instantiate Datatoken class * @param {Signer} signer The signer object. * @param {string | number} [network] Network id or name * @param {Config} [config] The configuration object. * @param {AbiItem[]} [abi] ABI array of the smart contract * @param {AbiItem[]} abiEnterprise Enterprise ABI array of the smart contract */ constructor( signer: Signer, network?: string | number, config?: Config, abi?: AbiItem[], abiEnterprise?: AbiItem[] ) { super(signer, network, config, abi) this.abiEnterprise = abiEnterprise || (ERC20TemplateEnterprise.abi as AbiItem[]) this.nft = new Nft(this.signer) } /** * Approves a spender to spend a certain amount of datatokens. * @param {String} dtAddress Datatoken address * @param {String} spender Spender address * @param {string} amount Number of datatokens, as number. Will be converted to wei * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} */ public async approve( dtAddress: string, spender: string, amount: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) const estGas = await dtContract.estimateGas.approve( spender, amountToUnits(null, null, amount, 18) ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.approve, spender, amountToUnits(null, null, amount, 18) ) return >trxReceipt } /** * Creates a new FixedRateExchange setup. * @param {String} dtAddress Datatoken address * @param {String} address Caller address * @param {FixedRateParams} fixedRateParams The parameters required to create a fixed-rate exchange contract. * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} */ public async createFixedRate( dtAddress: string, address: string, fixedRateParams: FreCreationParams, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if (!(await this.isDatatokenDeployer(dtAddress, address))) { throw new Error(`User is not Datatoken Deployer`) } if (!fixedRateParams.allowedConsumer) fixedRateParams.allowedConsumer = ZERO_ADDRESS const withMint = fixedRateParams.withMint === false ? 0 : 1 // should check DatatokenDeployer role using NFT level .. const estGas = await dtContract.estimateGas.createFixedRate( fixedRateParams.fixedRateAddress, [ fixedRateParams.baseTokenAddress, fixedRateParams.owner, fixedRateParams.marketFeeCollector, fixedRateParams.allowedConsumer ], [ fixedRateParams.baseTokenDecimals, fixedRateParams.datatokenDecimals, fixedRateParams.fixedRate, fixedRateParams.marketFee, withMint ] ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.createFixedRate, fixedRateParams.fixedRateAddress, [ fixedRateParams.baseTokenAddress, fixedRateParams.owner, fixedRateParams.marketFeeCollector, fixedRateParams.allowedConsumer ], [ fixedRateParams.baseTokenDecimals, fixedRateParams.datatokenDecimals, fixedRateParams.fixedRate, fixedRateParams.marketFee, withMint ] ) return >trxReceipt } /** * Creates a new Dispenser * @param {String} dtAddress Datatoken address * @param {String} address Caller address * @param {String} dispenserAddress Dispenser contract address * @param {DispenserParams} dispenserParams The parameters required to create a dispenser contract. * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async createDispenser( dtAddress: string, address: string, dispenserAddress: string, dispenserParams: DispenserParams, estimateGas?: G ): Promise> { if (!(await this.isDatatokenDeployer(dtAddress, address))) { throw new Error(`User is not Datatoken Deployer`) } const dtContract = this.getContract(dtAddress) if (!dispenserParams.allowedSwapper) dispenserParams.allowedSwapper = ZERO_ADDRESS dispenserParams.withMint = dispenserParams.withMint !== false // should check DatatokenDeployer role using NFT level .. const estGas = await dtContract.estimateGas.createDispenser( dispenserAddress, dispenserParams.maxTokens, dispenserParams.maxBalance, dispenserParams.withMint, dispenserParams.allowedSwapper ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.createDispenser, dispenserAddress, dispenserParams.maxTokens, dispenserParams.maxBalance, dispenserParams.withMint, dispenserParams.allowedSwapper ) return >trxReceipt } /** * Mints datatokens * @param {String} dtAddress Datatoken address * @param {String} address Minter address * @param {String} amount Number of datatokens, as number. Will be converted to wei * @param {String} toAddress only if toAddress is different from the minter * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async mint( dtAddress: string, address: string, amount: string, toAddress?: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if ((await this.getPermissions(dtAddress, address)).minter !== true) { throw new Error(`Caller is not Minter`) } const capAvailble = await this.getCap(dtAddress) if (new Decimal(capAvailble).gte(amount)) { const estGas = await dtContract.estimateGas.mint( toAddress || address, amountToUnits(null, null, amount, 18) ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.mint, toAddress || address, amountToUnits(null, null, amount, 18) ) return >trxReceipt } else { throw new Error(`Mint amount exceeds cap available`) } } /** * Add Minter for an ERC20 Datatoken * only DatatokenDeployer can succeed * @param {String} dtAddress Datatoken address * @param {String} address caller address * @param {String} minter address which is going to be a Minter * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async addMinter( dtAddress: string, address: string, minter: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if ((await this.isDatatokenDeployer(dtAddress, address)) !== true) { throw new Error(`Caller is not DatatokenDeployer`) } // Estimate gas cost for addMinter method const estGas = await dtContract.estimateGas.addMinter(minter) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.addMinter, minter ) return >trxReceipt } /** * Revoke Minter permission for an ERC20 Datatoken * only DatatokenDeployer can succeed * @param {String} dtAddress Datatoken address * @param {String} address caller address * @param {String} minter address which will have removed the Minter permission * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} */ public async removeMinter( dtAddress: string, address: string, minter: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if ((await this.isDatatokenDeployer(dtAddress, address)) !== true) { throw new Error(`Caller is not DatatokenDeployer`) } const estGas = await dtContract.estimateGas.removeMinter(minter) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.removeMinter, minter ) return >trxReceipt } /** * Adds a payment manager on a datatoken to a desired address.(can set who's going to collect fee when consuming orders) * only DatatokenDeployer can succeed * @param {String} dtAddress Datatoken address * @param {String} address Caller address * @param {String} paymentManager The address of the payment manager * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async addPaymentManager( dtAddress: string, address: string, paymentManager: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if ((await this.isDatatokenDeployer(dtAddress, address)) !== true) { throw new Error(`Caller is not DatatokenDeployer`) } const estGas = await dtContract.estimateGas.addPaymentManager(paymentManager) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.addPaymentManager, paymentManager ) return >trxReceipt } /** * Revoke paymentManager permission for an ERC20 Datatoken * only DatatokenDeployer can succeed * @param {String} dtAddress Datatoken address * @param {String} address User address * @param {String} paymentManager User which will be removed from paymentManager permission * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} trxReceipt */ public async removePaymentManager( dtAddress: string, address: string, paymentManager: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if ((await this.isDatatokenDeployer(dtAddress, address)) !== true) { throw new Error(`Caller is not DatatokenDeployer`) } const estGas = await dtContract.estimateGas.removePaymentManager(paymentManager) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.removePaymentManager, paymentManager ) return >trxReceipt } /** * This function allows to set a new PaymentCollector (receives DT when consuming) * If not set the paymentCollector is the NFT Owner * only NFT owner can call * @param dtAddress Datatoken address * @param address Caller address * @param paymentCollector User to be set as new payment collector * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} trxReceipt */ public async setPaymentCollector( dtAddress: string, address: string, paymentCollector: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) const isPaymentManager = (await this.getPermissions(dtAddress, address)) .paymentManager const nftAddress = !isPaymentManager && (await this.getNFTAddress(dtAddress)) const isNftOwner = nftAddress && (await this.nft.getNftOwner(nftAddress)) === address const nftPermissions = nftAddress && !isNftOwner && (await this.nft.getNftPermissions(nftAddress, address)) const isDatatokenDeployer = nftPermissions?.deployERC20 if (!isPaymentManager && !isNftOwner && !isDatatokenDeployer) { throw new Error(`Caller is not Fee Manager, owner or Datatoken Deployer`) } const estGas = await dtContract.estimateGas.setPaymentCollector(paymentCollector) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.setPaymentCollector, paymentCollector ) return >trxReceipt } /** * getPaymentCollector - It returns the current paymentCollector * @param dtAddress datatoken address * @return {Promise} */ public async getPaymentCollector(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const paymentCollector = await dtContract.getPaymentCollector() return paymentCollector } /** * Transfer tokens(as number) from address to toAddress * @param {String} dtAddress Datatoken address * @param {String} toAddress Receiver address * @param {String} amount Number of datatokens, as number. Will be converted to wei. * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async transfer( dtAddress: string, toAddress: string, amount: string, estimateGas?: G ): Promise> { return this.transferWei( dtAddress, toAddress, await amountToUnits(null, null, amount, 18), estimateGas ) } /** * Transfer in wei from address to toAddress * @param {String} dtAddress Datatoken address * @param {String} toAddress Receiver address * @param {String} amount Number of datatokens (number) expressed as wei * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async transferWei( dtAddress: string, toAddress: string, amount: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) const estGas = await dtContract.estimateGas.transfer(toAddress, amount) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.transfer, toAddress, amount ) return >trxReceipt } /** * Start Order: called by payer or consumer prior ordering a service consume on a marketplace. * @param {String} dtAddress Datatoken address * @param {String} consumer Consumer Address * @param {Number} serviceIndex Service index in the metadata * @param {providerFees} providerFees provider fees * @param {consumeMarketFee} ConsumeMarketFee consume market fees * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} string */ public async startOrder( dtAddress: string, consumer: string, serviceIndex: number, providerFees: ProviderFees, consumeMarketFee?: ConsumeMarketFee, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) if (!consumeMarketFee) { consumeMarketFee = { consumeMarketFeeAddress: ZERO_ADDRESS, consumeMarketFeeToken: ZERO_ADDRESS, consumeMarketFeeAmount: '0' } } const estGas = await dtContract.estimateGas.startOrder( consumer, serviceIndex, providerFees, consumeMarketFee ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.startOrder, consumer, serviceIndex, providerFees, consumeMarketFee ) return >trxReceipt } /** * Reuse Order: called by payer or consumer having a valid order, but with expired provider access. * Pays the provider fee again, but it will not require a new datatoken payment * Requires previous approval of provider fee. * @param {String} dtAddress Datatoken address * @param {String} orderTxId previous valid order * @param {providerFees} providerFees provider fees * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} string */ public async reuseOrder( dtAddress: string, orderTxId: string, providerFees: ProviderFees, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress) const estGas = await dtContract.estimateGas.reuseOrder(orderTxId, providerFees) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.reuseOrder, orderTxId, providerFees ) return >trxReceipt } /** * Buys 1 DT from the FRE and then startsOrder, while burning that DT * @param {String} dtAddress Datatoken address * @param {OrderParams} orderParams The parameters required to place an order. * @param {FreParams} freParams The parameters required to buy from a fixed-rate exchange. * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} */ public async buyFromFreAndOrder( dtAddress: string, orderParams: OrderParams, freParams: FreOrderParams, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress, this.abiEnterprise) const freContractParams = await this.getFreOrderParams(freParams) const estGas = await dtContract.estimateGas.buyFromFreAndOrder( orderParams, freContractParams ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.buyFromFreAndOrder, orderParams, freContractParams ) return >trxReceipt } /** * Gets 1 DT from dispenser and then startsOrder, while burning that DT * @param {String} dtAddress Datatoken address * @param {OrderParams} orderParams - The parameters required to place an order. * @param {String} dispenserContract dispenser address * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} */ public async buyFromDispenserAndOrder( dtAddress: string, orderParams: OrderParams, dispenserContract: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(dtAddress, this.abiEnterprise) const estGas = await dtContract.estimateGas.buyFromDispenserAndOrder( orderParams, dispenserContract ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.buyFromDispenserAndOrder, orderParams, dispenserContract ) return >trxReceipt } /** setData * This function allows to store data with a preset key (keccak256(dtAddress)) into NFT 725 Store * only DatatokenDeployer can succeed * @param {String} dtAddress Datatoken address * @param {String} address User address * @param {String} value Data to be stored into 725Y standard * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async setData( dtAddress: string, address: string, value: string, estimateGas?: G ): Promise> { if (!(await this.isDatatokenDeployer(dtAddress, address))) { throw new Error(`User is not Datatoken Deployer`) } const dtContract = this.getContract(dtAddress) const valueHex = ethers.utils.hexlify(ethers.utils.toUtf8Bytes(value)) const estGas = await dtContract.estimateGas.setData(valueHex) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.setData, valueHex ) return >trxReceipt } /** * Clean Datatoken level Permissions (minters, paymentManager and reset the paymentCollector) for an ERC20 Datatoken * Only NFT Owner (at 721 level) can call it. * @param {string} dtAddress Datatoken address where we want to clean permissions * @param {string} address User adress * @param {Boolean} estimateGas if True, return gas estimate * @return {Promise} transactionId */ public async cleanPermissions( dtAddress: string, address: string, estimateGas?: G ): Promise> { if ((await this.nft.getNftOwner(await this.getNFTAddress(dtAddress))) !== address) { throw new Error('Caller is NOT Nft Owner') } const dtContract = this.getContract(dtAddress) const estGas = await dtContract.estimateGas.cleanPermissions() if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.cleanPermissions ) return >trxReceipt } /** * Returns ERC20 Datatoken user's permissions for a datatoken * @param {String} dtAddress Datatoken adress * @param {String} address user adress * @return {Promise} */ public async getPermissions( dtAddress: string, address: string ): Promise { const dtContract = this.getContract(dtAddress) const roles = await dtContract.permissions(address) return roles } /** * Returns the Datatoken cap * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getCap(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const cap = await dtContract.cap() return await this.unitsToAmount(null, cap, 18) } /** * It returns the token decimals, how many supported decimal points * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getDecimals(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const decimals = await dtContract.decimals() return decimals } /** * It returns the token template index. * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getId(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const id = await dtContract.getId() return id } /** * It returns the token symbol * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getSymbol(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const symbol = await dtContract.symbol() return symbol } /** * It returns the name of the token * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getName(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const name = await dtContract.name() return name } /** * It returns the token decimals, how many supported decimal points * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getNFTAddress(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const nftAddress = await dtContract.getERC721Address() return nftAddress } /** * It returns the list of fixedRateExchanges created for this datatoken. * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getFixedRates(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const fixedRates = await dtContract.getFixedRates() return fixedRates } /** * It returns the list of dispensers created for this datatoken. * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async getDispensers(dtAddress: string): Promise { const dtContract = this.getContract(dtAddress) const dispensers = await dtContract.getDispensers() return dispensers } /** * Returns true if address has deployERC20 role * @param {String} dtAddress Datatoken adress * @param {String} dtAddress Datatoken adress * @return {Promise} */ public async isDatatokenDeployer(dtAddress: string, address: string): Promise { const dtContract = this.getContract(dtAddress) const isDatatokenDeployer = await dtContract.isERC20Deployer(address) return isDatatokenDeployer } /** * Get Address Balance for datatoken * @param {String} dtAddress Datatoken adress * @param {String} address user adress * @return {Promise} balance Number of datatokens. Will be converted from wei */ public async balance(datatokenAddress: string, address: string): Promise { const dtContract = this.getContract(datatokenAddress) const balance = await dtContract.balanceOf(address) return await this.unitsToAmount(null, balance, 18) } /** * Allows to set the fee required by the publisherMarket * only publishMarketFeeAddress can call it * @param {string} datatokenAddress Datatoken adress * @param {string} publishMarketFeeAddress new publish Market Fee Address * @param {string} publishMarketFeeToken new publish Market Fee Token * @param {string} publishMarketFeeAmount new fee amount * @param {String} address user adress * @param {Boolean} estimateGas if True, return gas estimate */ public async setPublishingMarketFee( datatokenAddress: string, publishMarketFeeAddress: string, publishMarketFeeToken: string, publishMarketFeeAmount: string, address: string, estimateGas?: G ): Promise> { const dtContract = this.getContract(datatokenAddress) const mktFeeAddress = (await dtContract.getPublishingMarketFee())[0] if (mktFeeAddress !== address) { throw new Error(`Caller is not the Publishing Market Fee Address`) } const estGas = await dtContract.estimateGas.setPublishingMarketFee( publishMarketFeeAddress, publishMarketFeeToken, publishMarketFeeAmount ) if (estimateGas) return >estGas const trxReceipt = await sendTx( estGas, this.signer, this.config?.gasFeeMultiplier, dtContract.setPublishingMarketFee, publishMarketFeeAddress, publishMarketFeeToken, publishMarketFeeAmount ) return >trxReceipt } /** * Returns the current fee set by the publishing market * @param {String} datatokenAddress Datatoken adress * @return {Promise} Current fee set by the publishing market */ public async getPublishingMarketFee( datatokenAddress: string ): Promise { const dtContract = this.getContract(datatokenAddress) const publishingMarketFee = await dtContract.getPublishingMarketFee() const returnValues = { publishMarketFeeAddress: publishingMarketFee[0], publishMarketFeeToken: publishingMarketFee[1], publishMarketFeeAmount: publishingMarketFee[2].toString() } return returnValues } private async getFreOrderParams(freParams: FreOrderParams): Promise { return { exchangeContract: freParams.exchangeContract, exchangeId: freParams.exchangeId, maxBaseTokenAmount: await amountToUnits( this.signer, freParams.baseTokenAddress, freParams.maxBaseTokenAmount, freParams.baseTokenDecimals ), swapMarketFee: await amountToUnits( this.signer, freParams.baseTokenAddress, freParams.swapMarketFee, freParams.baseTokenDecimals ), marketFeeAddress: freParams.marketFeeAddress } } }