diff --git a/src/datatokens/Datatoken.ts b/src/datatokens/Datatoken.ts new file mode 100644 index 00000000..3d496cb3 --- /dev/null +++ b/src/datatokens/Datatoken.ts @@ -0,0 +1,111 @@ +import Web3 from 'web3' +import { AbiItem } from 'web3-utils' +import { TransactionReceipt } from 'web3-eth' +import defaultDatatokensABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import Decimal from 'decimal.js' +import { Logger, getFairGasPrice } from '../utils' + +/** + * ERC20 ROLES + */ +interface Roles { + minter: boolean + feeManager: boolean +} + +export class Datatoken { + public GASLIMIT_DEFAULT = 1000000 + public factoryAddress: string + public factoryABI: AbiItem | AbiItem[] + public datatokensABI: AbiItem | AbiItem[] + public web3: Web3 + private logger: Logger + public startBlock: number + + /** + * Instantiate ERC20 DataTokens (independently of Ocean). + * @param {AbiItem | AbiItem[]} datatokensABI + * @param {Web3} web3 + */ + constructor( + web3: Web3, + logger: Logger, + datatokensABI?: AbiItem | AbiItem[], + startBlock?: number + ) { + this.web3 = web3 + this.logger = logger + this.datatokensABI = datatokensABI || (defaultDatatokensABI.abi as AbiItem[]) + this.startBlock = startBlock || 0 + } + + /** + * Mint + * @param {String} dataTokenAddress + * @param {String} 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 + * @return {Promise} transactionId + */ + public async mint( + dataTokenAddress: string, + address: string, + amount: string, + toAddress?: string + ): Promise { + const dtContract = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress) + + if ((await this.getDTPermissions(dataTokenAddress, address)).minter != true) { + throw new Error(`Caller is not Minter`) + } + + const capAvailble = await this.getCap(dataTokenAddress) + if (new Decimal(capAvailble).gte(amount)) { + // Estimate gas cost for mint method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await dtContract.methods + .mint(toAddress || address, this.web3.utils.toWei(amount)) + .estimateGas({ from: address }, (err, estGas) => + err ? gasLimitDefault : estGas + ) + } catch (e) { + estGas = gasLimitDefault + } + + // Call mint contract method + const trxReceipt = await dtContract.methods + .mint(toAddress || address, this.web3.utils.toWei(amount)) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } else { + throw new Error(`Mint amount exceeds cap available`) + } + } + + /** Returns ERC20 user's permissions for a datatoken + * @param {String} dtAddress Datatoken adress + * @param {String} address user adress + * @return {Promise} + */ + public async getDTPermissions(dtAddress: string, address: string): Promise { + const dtContract = new this.web3.eth.Contract(this.datatokensABI, dtAddress) + const roles = await dtContract.methods.permissions(address).call() + return roles + } + + /** Returns the DataToken capital + * @param {String} dtAddress Datatoken adress + * @return {Promise} + */ + public async getCap(dtAddress: string): Promise { + const datatoken = new this.web3.eth.Contract(this.datatokensABI, dtAddress) + const cap = await datatoken.methods.cap().call() + return this.web3.utils.fromWei(cap) + } +} diff --git a/src/datatokens/NFTDatatoken.ts b/src/datatokens/NFTDatatoken.ts index ad0199b8..cac770a4 100644 --- a/src/datatokens/NFTDatatoken.ts +++ b/src/datatokens/NFTDatatoken.ts @@ -1,5 +1,6 @@ import Web3 from 'web3' import { AbiItem } from 'web3-utils' +import { TransactionReceipt } from 'web3-eth' import defaultNFTDatatokenABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' import { Logger, getFairGasPrice, generateDtName } from '../utils' @@ -13,7 +14,7 @@ interface Roles { store: boolean } -export class NFTDataToken { +export class NFTDatatoken { public GASLIMIT_DEFAULT = 1000000 public factory721Address: string public factory721ABI: AbiItem | AbiItem[] @@ -104,4 +105,426 @@ export class NFTDataToken { } return tokenAddress } + + /** + * Add Manager for NFT Contract (only NFT Owner can succeed) + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Owner adress + * @param {String} manager User adress which is going to be assing manager + * @return {Promise} trxReceipt + */ + public async addManager(nftAddress: string, address: string, manager: string) { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTOwner(nftAddress)) !== address) { + throw new Error(`Caller is not NFT Owner`) + } + + // Estimate gas for add manager call + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .addManager(manager) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Invoke addManager function of the contract + const trxReceipt = await nftContract.methods.addManager(manager).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Removes a specific manager for NFT Contract (only NFT Owner can succeed) + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Owner adress + * @param {String} manager User adress which is going to be removed as manager + * @return {Promise} trxReceipt + */ + public async removeManager(nftAddress: string, address: string, manager: string) { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTOwner(nftAddress)) !== address) { + throw new Error(`Caller is not NFT Owner`) + } + + // Estimate gas for removeManager method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .removeManager(manager) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Invoke removeManager function of the contract + const trxReceipt = await nftContract.methods.removeManager(manager).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Add ERC20Deployer permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} erc20Deployer User adress which is going to have erc20Deployer permission + * @return {Promise} trxReceipt + */ + public async addERC20Deployer( + nftAddress: string, + address: string, + erc20Deployer: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager !== true) { + throw new Error(`Caller is not Manager`) + } + + // Estimate gas for addToCreateERC20List method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .addToCreateERC20List(erc20Deployer) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Invoke addToCreateERC20List function of the contract + const trxReceipt = await nftContract.methods + .addToCreateERC20List(erc20Deployer) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Remove ERC20Deployer permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} erc20Deployer Address of the user to be revoked ERC20Deployer Permission + * @return {Promise} trxReceipt + */ + public async removeERC20Deployer( + nftAddress: string, + address: string, + erc20Deployer: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager !== true) { + throw new Error(`Caller is not Manager`) + } + + //Estimate gas for removeFromCreateERC20List method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .removeFromCreateERC20List(erc20Deployer) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call removeFromCreateERC20List function of the contract + const trxReceipt = await nftContract.methods + .removeFromCreateERC20List(erc20Deployer) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Add Metadata Updater permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} metadataUpdater User adress which is going to have Metadata Updater permission + * @return {Promise} trxReceipt + */ + public async addMetadataUpdater( + nftAddress: string, + address: string, + metadataUpdater: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager != true) { + throw new Error(`Caller is not Manager`) + } + + //Estimate gas cost for addToMetadataList method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .addToMetadataList(metadataUpdater) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call addToMetadataList function of the contract + const trxReceipt = await nftContract.methods.addToMetadataList(metadataUpdater).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Remove Metadata Updater permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} metadataUpdater Address of the user to be revoked Metadata updater Permission + * @return {Promise} trxReceipt + */ + public async removeMetadataUpdater( + nftAddress: string, + address: string, + metadataUpdater: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager != true) { + throw new Error(`Caller is not Manager`) + } + + //Estimate gas cost for removeFromMetadataList method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .removeFromMetadataList(metadataUpdater) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call removeFromMetadataList function of the contract + const trxReceipt = await nftContract.methods + .removeFromMetadataList(metadataUpdater) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Add Store Updater permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} storeUpdater User adress which is going to have Store Updater permission + * @return {Promise} trxReceipt + */ + public async addStoreUpdater( + nftAddress: string, + address: string, + storeUpdater: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager != true) { + throw new Error(`Caller is not Manager`) + } + + // Estimate gas cost for addTo725StoreList method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .addTo725StoreList(storeUpdater) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call addTo725StoreList function of the contract + const trxReceipt = await nftContract.methods.addTo725StoreList(storeUpdater).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Remove Store Updater permission - only Manager can succeed + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Manager adress + * @param {String} storeUpdater Address of the user to be revoked Store Updater Permission + * @return {Promise} trxReceipt + */ + public async removeStoreUpdater( + nftAddress: string, + address: string, + storeUpdater: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTPermissions(nftAddress, address)).manager != true) { + throw new Error(`Caller is not Manager`) + } + + // Estimate gas cost for removeFrom725StoreList method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .removeFrom725StoreList(storeUpdater) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call removeFrom725StoreList function of the contract + const trxReceipt = await nftContract.methods + .removeFrom725StoreList(storeUpdater) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * This function allows to remove all ROLES at erc721 level: Managers, ERC20Deployer, MetadataUpdater, StoreUpdater + * Even NFT Owner has to readd himself as Manager + * Permissions at erc20 level stay. + * Only NFT Owner can call it. + * @param {String} nftAddress erc721 contract adress + * @param {String} address NFT Owner adress + * @return {Promise} trxReceipt + */ + + public async cleanPermissions( + nftAddress: string, + address: string + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTOwner(nftAddress)) !== address) { + throw new Error(`Caller is not NFT Owner`) + } + + // Estimate gas cost for cleanPermissions method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .cleanPermissions() + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + // Call cleanPermissions function of the contract + const trxReceipt = await nftContract.methods.cleanPermissions().send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** + * Transfers the NFT + * will clean all permissions both on erc721 and erc20 level. + * @param {String} nftAddress erc721 contract adress + * @param {String} nftOwner Current NFT Owner adress + * @param {String} nftReceiver User which will receive the NFT, will also be set as Manager + * @param {Number} tokenId The id of the token to be transfered + * @return {Promise} trxReceipt + */ + public async transferNFT( + nftAddress: string, + nftOwner: string, + nftReceiver: string, + tokenId?: number + ): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + + if ((await this.getNFTOwner(nftAddress)) !== nftOwner) { + throw new Error(`Caller is not NFT Owner`) + } + + let tokenIdentifier = tokenId || 1 + + // Estimate gas cost for transfer NFT method + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await nftContract.methods + .transferFrom(nftOwner, nftReceiver, tokenIdentifier) + .estimateGas({ from: nftOwner }, (err, estGas) => + err ? gasLimitDefault : estGas + ) + } catch (e) { + estGas = gasLimitDefault + } + + // Call transferFrom function of the contract + const trxReceipt = await nftContract.methods + .transferFrom(nftOwner, nftReceiver, tokenIdentifier) + .send({ + from: nftOwner, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + + return trxReceipt + } + + /** Get Owner + * @param {String} nftAddress erc721 contract adress + * @return {Promise} string + */ + public async getNFTOwner(nftAddress: string): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + const trxReceipt = await nftContract.methods.ownerOf(1).call() + return trxReceipt + } + + /** Get users NFT Permissions + * @param {String} nftAddress erc721 contract adress + * @param {String} address user adress + * @return {Promise} + */ + public async getNFTPermissions(nftAddress: string, address: string): Promise { + const nftContract = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress) + const roles = await nftContract.methods._getPermissions(address).call() + return roles + } } diff --git a/src/datatokens/index.ts b/src/datatokens/index.ts index 2235441b..9076989d 100644 --- a/src/datatokens/index.ts +++ b/src/datatokens/index.ts @@ -1 +1,2 @@ +export * from './Datatoken' export * from './NFTDatatoken' diff --git a/test/unit/NFTDatatoken.test.ts b/test/unit/NFTDatatoken.test.ts new file mode 100644 index 00000000..d311283a --- /dev/null +++ b/test/unit/NFTDatatoken.test.ts @@ -0,0 +1,16 @@ +// import { assert } from 'chai' +// import { LoggerInstance } from '../../../src/utils' + +// const web3 = new Web3('http://127.0.0.1:8545') + +// describe('NFTDatatoken', () => { +// let nftOwner: string +// let user1: string +// let user2: string +// let contracts: TestContractHandler +// let nftDatatoken: NFTDataToken +// let nftFactory: NFTFactory +// let erc20Factory: DT20Factory +// let nftAddress: string + +// }