diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 3a91c864..da2f0486 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,6 +1,6 @@ 2024-07-15 (v.4.0.0) ------------------- - +- Removed `web3` parameter in `Web3SelfTransactionConfig` - Support BlockchainAnchor and Verida Network refactor 2023-12-26 (v.3.0.0) diff --git a/packages/types/src/Web3Interfaces.ts b/packages/types/src/Web3Interfaces.ts index 0a01469f..382a527b 100644 --- a/packages/types/src/Web3Interfaces.ts +++ b/packages/types/src/Web3Interfaces.ts @@ -41,7 +41,6 @@ export interface Web3GasConfiguration { * signer - optional - a Signer that sign the blockchain transactions. If a 'signer' is not provided, then 'contract' with an attached signer need to be used to make transactions * provider - optional - a web3 provider. At least one of `signer`,`provider`, or `rpcUrl` is required * rpcUrl - optinal - a JSON-RPC URL that can be used to connect to an ethereum network. At least one of `signer`, `provider`, or `rpcUrl` is required - * web3 - optional - Can use provider or web.currentProvider as a provider. * */ export interface Web3SelfTransactionConfig extends Web3GasConfiguration { @@ -50,7 +49,6 @@ export interface Web3SelfTransactionConfig extends Web3GasConfiguration { privateKey?: string provider?: Provider rpcUrl?: string - web3?: any chainId?: string | number /** Function list with default gas configuration */ diff --git a/packages/vda-token-client/.env.example b/packages/vda-token-client/.env.example new file mode 100644 index 00000000..b17edb62 --- /dev/null +++ b/packages/vda-token-client/.env.example @@ -0,0 +1,6 @@ +## These are used for test only. +PRIVATE_KEY = "" +RPC_URL="" + +# Test chain : One of "POLAMOY", "POLPOS", "DEVNET" +BLOCKCHAIN_ANCHOR = "POLAMOY" \ No newline at end of file diff --git a/packages/vda-token-client/.gitignore b/packages/vda-token-client/.gitignore new file mode 100644 index 00000000..f6ca2e78 --- /dev/null +++ b/packages/vda-token-client/.gitignore @@ -0,0 +1,11 @@ +#Ignore all files starting with "." +.* +!/.gitignore + +# node_modules in all sub directories +**node_modules + +# build +dist/ +lib/ +build/ \ No newline at end of file diff --git a/packages/vda-token-client/CHANGELOG.md b/packages/vda-token-client/CHANGELOG.md new file mode 100644 index 00000000..c6d8b644 --- /dev/null +++ b/packages/vda-token-client/CHANGELOG.md @@ -0,0 +1,4 @@ +2023-07-12 (v) +------------------- + +- Initial release \ No newline at end of file diff --git a/packages/vda-token-client/README.md b/packages/vda-token-client/README.md new file mode 100644 index 00000000..00f2ee14 --- /dev/null +++ b/packages/vda-token-client/README.md @@ -0,0 +1,78 @@ + +# VDA Token Client + +A client library to managet Verida Tokens + +## Dependencies +This package dependes on the following `verida-js` packages: +- `@verida/types` +- `@verida/helpers` +- `@verida/vda-common` +- `@verida/vda-web3` + +And dependes on the following packages for test: +- `@verida/vda-common-test` + +## Installation + +``` +yarn add @verida/vda-token-client +``` + +## Usage + +There are 2 classes of `VeridaTokenClient` and `VeridaTokenOwner`.
+- `VeridaTokenClient` is for general users to mange VDA tokens. +- `VeridaTokenOwner` adds the owner-specific functions to the `VeridaTokenClient`. + +These classes can be run in 2 modes of __*read-only*__ and __*read & write*__.
+If you provided `privateKey` field in the configuration while creating the instance, it runs in __*read & write*__ mode, otherwise it runs in __*read-only*__ mode. + +In the __*read-only*__ mode, it can calls only the `view` functions of the contract. + +### Read Only + +Setup the library in `read only`: + +```ts +import { VeridaTokenClient } from '@verida/vda-token-client' +import { BlockchainAnchor } from '@verida/types' + +// Create token Client +const blockchainAnchor = BlockchainAnchor.POLAMOY; +const rpcUrl = ``; // This is optional +const tokenClient = await VeridaTokenClient.CreateAsync({ + blockchainAnchor, + rpcUrl +}) +``` + +#### *Example:* Get total supply + +```ts +const value:BigNumber = await tokenClient.totalSupply(); +``` + +### Read and Write + +```ts +import { VeridaTokenClient } from '@verida/vda-token-client' +import { BlockchainAnchor } from '@verida/types' + +const blockchainAnchor = BlockchainAnchor.POLAMOY; +const rpcUrl = ``; // This is optional +const privateKey = ``; +const tokenClient = await VeridaTokenClient.CreateAsync({ + blockchainAnchor, + privateKey, + rpcUrl +}) +``` + +#### *Example:* Transfer token + +```ts +const to = `0x...`; // Recipient address +const amount = BigNumber.from(10); +await tokenClient.transfer(to, amount); +``` diff --git a/packages/vda-token-client/Test.md b/packages/vda-token-client/Test.md new file mode 100644 index 00000000..87d7f5a0 --- /dev/null +++ b/packages/vda-token-client/Test.md @@ -0,0 +1,23 @@ +# VDA Token Client Test + +Here explains stuffs related to test this `@verida/vda-token-client` package. + +## owner.test.ts +- Only the contract owner can test this file + +## read.test.ts +- Anybody can test this file + +## write.test.ts +Please check following before run this test: +- Token balances of following addresses:
+```ts +0x8Ec5df9Ebc9554CECaA1067F974bD34735d3e539: More than `AMOUNT_SEND`(=1000) tokens +0xE3A441c4e699433FCf1262246Cf323d8e4302998: More than `AMOUNT_APPROVE`(=1000) tokens +``` + +- Enough Matic in following addresses: +```ts +0x8Ec5df9Ebc9554CECaA1067F974bD34735d3e539, +0xE3A441c4e699433FCf1262246Cf323d8e4302998 +``` \ No newline at end of file diff --git a/packages/vda-token-client/package.json b/packages/vda-token-client/package.json new file mode 100644 index 00000000..8daad60a --- /dev/null +++ b/packages/vda-token-client/package.json @@ -0,0 +1,43 @@ +{ + "name": "@verida/vda-token-client", + "version": "0.1.0", + "description": "Client to manage Verida token", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "files": [ + "build/src" + ], + "directories": { + "src": "src", + "test": "tests" + }, + "license": "Apache-2.0", + "keywords": [], + "scripts": { + "tests": "ts-mocha './test/**/*.tests.ts'", + "test": "ts-mocha --timeout 200000", + "lint": "gts lint", + "clean": "gts clean", + "compile": "rm -rf build && tsc", + "build": "rm -rf build && tsc", + "fix": "gts fix", + "prepare": "npm run compile" + }, + "dependencies": { + "@ethersproject/providers": "^5.7.2", + "@verida/helpers": "^4.0.0-alpha-1", + "@verida/types": "^4.0.0-alpha-1", + "@verida/web3": "^4.0.0-alpha-1", + "axios": "^0.27.2", + "ethers": "^5.7.0" + }, + "devDependencies": { + "@verida/vda-common-test": "^4.0.0-alpha-1", + "dotenv": "^16.0.3", + "gts": "^3.1.0", + "mocha": "^10.1.0", + "ts-mocha": "^10.0.0", + "ts-node": "^10.7.0", + "typescript": "^4.6.4" + } +} diff --git a/packages/vda-token-client/src/blockchain/VeridaTokenClient.ts b/packages/vda-token-client/src/blockchain/VeridaTokenClient.ts new file mode 100644 index 00000000..c282e4b1 --- /dev/null +++ b/packages/vda-token-client/src/blockchain/VeridaTokenClient.ts @@ -0,0 +1,737 @@ +import { BlockchainAnchor, Web3SelfTransactionConfig } from '@verida/types' +import { BigNumber, BigNumberish, Contract, ethers, Wallet } from "ethers"; +import { getContractInfoForBlockchainAnchor, getDefaultRpcUrl } from "@verida/vda-common"; +import { getVeridaContract, VeridaContract } from '@verida/web3'; +import { JsonRpcProvider } from '@ethersproject/providers'; + +export class VeridaTokenClient { + + protected config: Web3SelfTransactionConfig; + protected blockchainAnchor: BlockchainAnchor; + protected readOnly: boolean; + protected address: String|undefined; + protected vdaWeb3Client? : VeridaContract + protected contract?: Contract; + + protected decimalCache: BigNumber|undefined; + protected nameCache: string|undefined; + protected symbolCache: string|undefined; + protected totalSupplyCache: BigNumber|undefined; + protected rateDenominatorCache: number|undefined; + + protected constructor(config: Web3SelfTransactionConfig, addr?: string) { + this.config = config; + if (!config.blockchainAnchor) { + throw new Error('Should provid "blockchainAnchor" in the configuration'); + } + this.blockchainAnchor = config.blockchainAnchor; + + if (!config.rpcUrl) { + this.config.rpcUrl = getDefaultRpcUrl(this.blockchainAnchor)! + } + + this.readOnly = true; + this.address = addr; + if (this.address !== undefined) { + this.readOnly = false; + } + + const contractInfo = getContractInfoForBlockchainAnchor(this.blockchainAnchor, "token"); + if (!this.readOnly) { + this.vdaWeb3Client = getVeridaContract( + "web3", + { + ...contractInfo, + ...config, + blockchainAnchor: this.blockchainAnchor + } + ); + } else { + const rpcUrl = config.rpcUrl ?? getDefaultRpcUrl(this.blockchainAnchor)!; + const provider = new JsonRpcProvider(rpcUrl); + this.contract = new Contract(contractInfo.address, contractInfo.abi.abi, provider); + } + } + + public static CreateAsync = async (config: Web3SelfTransactionConfig) => { + let addr: string | undefined; + if (config.privateKey) { + addr = new Wallet(config.privateKey!).address; + } else if (config.signer) { + addr = await config.signer.getAddress(); + } + return new VeridaTokenClient(config, addr); + } + + /** + * Get the contract owner + */ + public async owner() { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.owner(); + + if (response.success !== true) { + throw new Error(`Failed to read owner`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.owner(); + + if (!response) { + throw new Error(`Failed to read owner`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to read owner: (${err.message})`); + } + } + + /** + * Get the token name + * @returns Token name + */ + public async name() { + if (this.nameCache) { + return this.nameCache; + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.name(); + + if (response.success !== true) { + throw new Error(`Failed to read name`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.name(); + + if (!response) { + throw new Error(`Failed to read name`); + } + } + + this.nameCache = response; + return this.nameCache; + } catch (err:any ) { + throw new Error(`Failed to read name: (${err.message})`); + } + } + + /** + * Get the token symbol + * @returns Token symbol + */ + public async symbol() { + if (this.symbolCache) { + return this.symbolCache; + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.symbol(); + + if (response.success !== true) { + throw new Error(`Failed to read symbol`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.symbol(); + + if (!response) { + throw new Error(`Failed to read symbol`); + } + } + + this.symbolCache = response; + return this.symbolCache; + } catch (err:any ) { + throw new Error(`Failed to read symbol: (${err.message})`); + } + } + + /** + * Get teh decimal of the Token + * @returns Decimal in Number type + */ + public async decimals() { + if (this.decimalCache) { + return this.decimalCache; + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.decimals(); + + if (response.success !== true) { + throw new Error(`Failed to read deciaml`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.decimals(); + + if (!response) { + throw new Error(`Failed to read decimal`); + } + } + + this.decimalCache = response; + return this.decimalCache; + } catch (err:any ) { + throw new Error(`Failed to read decimal: (${err.message})`); + } + } + + /** + * Get the total supply + * @returns Total supply in big number format + */ + public async totalSupply() { + if (this.totalSupplyCache) { + return this.totalSupplyCache; + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.totalSupply(); + + if (response.success !== true) { + throw new Error(`Failed to read totalSupply`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.totalSupply(); + + if (!response) { + throw new Error(`Failed to read totalSupply`); + } + } + + this.totalSupplyCache = response; + return this.totalSupplyCache; + } catch (err:any ) { + throw new Error(`Failed to read totalSupply: (${err.message})`); + } + } + + /** + * Get the total supply + * @returns Total supply in big number format + */ + public async balanceOf(addr = this.address) { + if (!addr) { + throw new Error(`Provide address to get the balance`); + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.balanceOf(addr); + + if (response.success !== true) { + throw new Error(`Failed to get the balance`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.balanceOf(addr); + + if (!response) { + throw new Error(`Failed to get the balance`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to get the balance of ${addr}: (${err.message})`); + } + } + + /** + * @notice Transfer token to spcified address + * @param to Receiver + * @param value Amount to be transferred + */ + public async transfer(to: string, value: BigNumberish) { + if (this.readOnly) { + throw new Error(`Unable to submit to blockchain. No 'signer' provided in config.`) + } + + const response = await this.vdaWeb3Client!.transfer(to, value); + if (response.success !== true) { + throw new Error(`Failed to transfer: ${response.reason}`); + } + } + + /** + * Get the allowance + * @param owner Address + * @param spender Address + * @returns Amount of allowed token + */ + public async allowance(owner: string, spender: string) { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.allowance(owner, spender); + + if (response.success !== true) { + throw new Error(`Failed to get the allowance`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.allowance(owner, spender); + + if (!response) { + throw new Error(`Failed to get the allowance`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to get the allowance of ${owner} to ${spender}: (${err.message})`); + } + } + + /** + * Approve token + * @param spender Address + * @param value Amount of token to be approved + */ + public async approve(spender: string, value: BigNumberish) { + if (this.readOnly) { + throw new Error(`Unable to submit to blockchain. No 'signer' provided in config.`) + } + + const response = await this.vdaWeb3Client!.approve(spender, value); + if (response.success !== true) { + throw new Error(`Failed to approve: ${response.reason}`); + } + } + + /** + * Transfer token from `from` to `to` + * @param from Address + * @param to Address + * @param value Amount + */ + public async transferFrom(from: string, to: string, value: BigNumberish) { + if (this.readOnly) { + throw new Error(`Unable to submit to blockchain. No 'signer' provided in config.`) + } + + const response = await this.vdaWeb3Client!.transferFrom(from, to, value); + if (response.success !== true) { + throw new Error(`Failed to transfer from ${from} to ${to}: ${response.reason}`); + } + } + + /** + * Get teh `RATE_DENOMINATOR` of the contract for rate specified values + * @returns Rate denominator + */ + public async rateDenominator() { + if (this.rateDenominatorCache) { + return this.rateDenominatorCache; + } + + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.RATE_DENOMINATOR(); + + if (response.success !== true) { + throw new Error(`Failed to read rate denominator`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.RATE_DENOMINATOR(); + + if (!response) { + throw new Error(`Failed to read rate denominator`); + } + } + + this.rateDenominatorCache = Number(response); + return this.rateDenominatorCache; + } catch (err:any ) { + throw new Error(`Failed to read rate denominator: (${err.message})`); + } + } + + /** + * Get number of minters + * @returns Number + */ + public async getMinterCount() { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.getMinterCount(); + + if (response.success !== true) { + throw new Error(`Failed to get number of minters`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.getMinterCount(); + + if (!response) { + throw new Error(`Failed to get number of minters`); + } + } + + return Number(response); + } catch (err:any ) { + throw new Error(`Failed to get number of minters: (${err.message})`); + } + } + + /** + * Get the list of minter addresses + * @returns Array of addresses + */ + public async getMinterList() { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.getMinterList(); + + if (response.success !== true) { + throw new Error(`Failed to get minter list`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.getMinterList(); + + if (!response) { + throw new Error(`Failed to get minter list`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to get minter list: (${err.message})`); + } + } + + /** + * Get the version of the Token contract + * @returns Version in string format + */ + public async getVersion() { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.getVersion(); + + if (response.success !== true) { + throw new Error(`Failed to get version`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.getVersion(); + + if (!response) { + throw new Error(`Failed to get version`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to get version: (${err.message})`); + } + } + + /** + * Check the given address is set as AMM pair in the contract + * @param address AMM pair address to be checked + * @returns true if set + */ + public async isAMMPair(address: string) { + let response + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.automatedMarketMakerPairs(address); + + if (response.success !== true) { + throw new Error(`Failed to check AMM`); + } + + response = response.data + } else { + response = await this.contract!.callStatic.automatedMarketMakerPairs(address); + + if (typeof response !== 'boolean') { + throw new Error(`Failed to check AMM`); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check AMM: (${err.message})`); + } + } + + /** + * Return token balance limit per wallet in rate value + * @returns Decimal value + */ + public async maxAmountPerWalletRate() { + let response + const denominator = await this.rateDenominator(); + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.maxAmountPerWalletRate(); + + if (response.success !== true) { + throw new Error('Failed to get max amount per wallet'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.maxAmountPerWalletRate(); + + if (!response) { + throw new Error('Failed to get max amount per wallet'); + } + } + + return Number(response)/denominator; + } catch (err:any ) { + throw new Error(`Failed to get max amount per wallet: (${err.message})`); + } + } + + /** + * Check whether wallet amount limited + * @returns true if enabled + */ + public async isWalletAmountLimitEnabled() { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.isMaxAmountPerWalletEnabled(); + + if (response.success !== true) { + throw new Error('Failed to check wallet amount limit enabled'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.isMaxAmountPerWalletEnabled(); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check wallet amount limit enabled'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check wallet amount limit enabled: (${err.message})`); + } + } + + /** + * Check whether address is excluded from wallet amount limit + * @param address Wallet address + * @returns true if excluded + */ + public async isExcludedFromWalletAmountLimit(address = this.address) { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.isExcludedFromWalletAmountLimit(address); + + if (response.success !== true) { + throw new Error('Failed to check wallet excluded from amount limit'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.isExcludedFromWalletAmountLimit(address); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check wallet excluded from amount limit'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check wallet excluded from amount limit: (${err.message})`); + } + } + + /** + * Return token amount limit per sell tx + * @returns Decimal + */ + public async maxAmountPerSellRate() { + let response + const denominator = await this.rateDenominator(); + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.maxAmountPerSellRate(); + + if (response.success !== true) { + throw new Error('Failed to get max amount per sell'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.maxAmountPerSellRate(); + + if (!response) { + throw new Error('Failed to get max amount per sell'); + } + } + + return Number(response)/denominator; + } catch (err:any ) { + throw new Error(`Failed to get max amount per sell: (${err.message})`); + } + } + + /** + * Check whether sell amount per transaction limited + * @returns true if enabled + */ + public async isSellAmountLimitEnabled() { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.isMaxAmountPerSellEnabled(); + + if (response.success !== true) { + throw new Error('Failed to check sell amount limit enabled'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.isMaxAmountPerSellEnabled(); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check sell amount limit enabled'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check sell amount limit enabled: (${err.message})`); + } + } + + /** + * Check whether address is excluded from sell amount limit + * @param address Wallet address + * @returns true if excluded + */ + public async isExcludedFromSellAmountLimit(address = this.address) { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.isExcludedFromSellAmountLimit(address); + + if (response.success !== true) { + throw new Error('Failed to check wallet excluded from sell amount limit'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.isExcludedFromSellAmountLimit(address); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check wallet excluded from sell amount limit'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check wallet excluded from sell amount limit: (${err.message})`); + } + } + + /** + * Check whether token transfer enabled by the contract owner + * @returns true if enabled + */ + public async isTransferEnabled() { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.isTransferEnabled(); + + if (response.success !== true) { + throw new Error('Failed to check token trnasfer enabled'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.isTransferEnabled(); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check token trnasfer enabled'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check token trnasfer enabled: (${err.message})`); + } + } + + /** + * Check whether contract is paused + * @returns true if paused + */ + public async isPaused() { + let response + + try { + if (this.vdaWeb3Client) { + response = await this.vdaWeb3Client.paused(); + + if (response.success !== true) { + throw new Error('Failed to check contract paused'); + } + + response = response.data + } else { + response = await this.contract!.callStatic.paused(); + + if (typeof response !== 'boolean' ) { + throw new Error('Failed to check contract paused'); + } + } + + return response; + } catch (err:any ) { + throw new Error(`Failed to check contract paused: (${err.message})`); + } + } +} \ No newline at end of file diff --git a/packages/vda-token-client/src/blockchain/VeridaTokenOwner.ts b/packages/vda-token-client/src/blockchain/VeridaTokenOwner.ts new file mode 100644 index 00000000..0e3fa2ab --- /dev/null +++ b/packages/vda-token-client/src/blockchain/VeridaTokenOwner.ts @@ -0,0 +1,176 @@ +import { Web3SelfTransactionConfig } from '@verida/types'; +import { VeridaTokenClient } from './VeridaTokenClient'; +import { BigNumber, Wallet } from 'ethers'; + +export class VeridaTokenOwner extends VeridaTokenClient { + + protected constructor(config: Web3SelfTransactionConfig, addr?: string) { + super(config, addr); + } + + public static CreateAsync = async (config: Web3SelfTransactionConfig) => { + let addr: string | undefined; + if (config.privateKey) { + addr = new Wallet(config.privateKey!).address; + } else if (config.signer) { + addr = await config.signer.getAddress(); + } else { + // VeridaTokenOwner can't be created in read-only mode + throw new Error(`No 'privateKey' or 'signer' in the configuration`); + } + + return new VeridaTokenOwner(config, addr); + } + + /** + * Mint token + * @param to Address + * @param amount + */ + public async mint(to: string, amount: BigNumber) { + const response = await this.vdaWeb3Client!.mint(to, amount); + if (response.success !== true) { + throw new Error(`Failed to mint ${amount} tokens to ${to}: ${response.reason}`); + } + } + + /** + * Add a minter + * @param minter Address + */ + public async addMinter(minter: string) { + const response = await this.vdaWeb3Client!.addMinter(minter); + if (response.success !== true) { + throw new Error(`Failed to add a minter(${minter}): ${response.reason}`); + } + } + + /** + * Revoke a minter + * @param minter Address + */ + public async revokeMinter(minter: string) { + const response = await this.vdaWeb3Client!.revokeMinter(minter); + if (response.success !== true) { + throw new Error(`Failed to revoke a minter(${minter}): ${response.reason}`); + } + } + + /** + * Enable/disable AMM pair + * @param pair Address of AMM + * @param enabled true/false + */ + public async setAutomatedMarketMakerPair(pair: string, enabled = true) { + const response = await this.vdaWeb3Client!.setAutomatedMarketMakerPair(pair, enabled); + if (response.success !== true) { + throw new Error(`Failed to update the status of AMM pair(${pair}): ${response.reason}`); + } + } + + /** + * Update token limit in percent per wallet + * @param rate Rate value in decimal. Ex: 0.1 means 0.1% + */ + public async updateMaxAmountPerWalletRate(rate: number) { + const denominator = await this.rateDenominator(); + const rateValue = BigNumber.from(denominator * rate); + + const response = await this.vdaWeb3Client!.updateMaxAmountPerWalletRate(rateValue); + if (response.success !== true) { + throw new Error(`Failed to update the max amount rate per wallet: ${response.reason}`); + } + } + + /** + * Update token limit in percent per sell operation + * @param rate Rate value in decimal. Ex: 0.1 means 0.1% + */ + public async updateMaxAmountPerSellRate(rate: number) { + const denominator = await this.rateDenominator(); + const rateValue = BigNumber.from(denominator * rate); + + const response = await this.vdaWeb3Client!.updateMaxAmountPerSellRate(rateValue); + if (response.success !== true) { + throw new Error(`Failed to update the max amount rate per sell: ${response.reason}`); + } + } + + /** + * Exclude/include an address from sell amount limit + * @param address Address + * @param isExcluded true if exclude + */ + public async excludeFromSellAmountLimit(address: string, isExcluded: boolean) { + const response = await this.vdaWeb3Client!.excludeFromSellAmountLimit(address, isExcluded); + if (response.success !== true) { + throw new Error(`Failed to exclude from sell amount limit(${address}): ${response.reason}`); + } + } + + /** + * Exclude/include an address from wallet amount limit + * @param address Address + * @param isExcluded true if exclude + */ + public async excludeFromWalletAmountLimit(address: string, isExcluded: boolean) { + const response = await this.vdaWeb3Client!.excludeFromWalletAmountLimit(address, isExcluded); + if (response.success !== true) { + throw new Error(`Failed to exclude from wallet amount limit(${address}): ${response.reason}`); + } + } + + /** + * Enable/disable the selling amount limit + * @param isEnabled boolean + */ + public async enableMaxAmountPerSell(isEnabled: boolean) { + const response = await this.vdaWeb3Client!.enableMaxAmountPerSell(isEnabled); + if (response.success !== true) { + throw new Error(`Failed to enable amount limit per sell(${isEnabled}): ${response.reason}`); + } + } + + /** + * Enable/disable the wallet amount limit + * @param isEnabled boolean + */ + public async enableMaxAmountPerWallet(isEnabled: boolean) { + const response = await this.vdaWeb3Client!.enableMaxAmountPerWallet(isEnabled); + if (response.success !== true) { + throw new Error(`Failed to enable amount limit per wallet(${isEnabled}): ${response.reason}`); + } + } + + /** + * Enable token transfer + * @dev Only called once + */ + public async enableTransfer() { + const response = await this.vdaWeb3Client!.enableTransfer(); + if (response.success !== true) { + throw new Error(`Failed to enable transfer: ${response.reason}`); + } + } + + /** + * Pause the contract + */ + public async pause() { + const response = await this.vdaWeb3Client!.pause(); + if (response.success !== true) { + throw new Error(`Failed to pause contract: ${response.reason}`); + } + } + + /** + * Unpause the contract + */ + public async unpause() { + const response = await this.vdaWeb3Client!.unpause(); + if (response.success !== true) { + throw new Error(`Failed to unpause contract: ${response.reason}`); + } + } + +} \ No newline at end of file diff --git a/packages/vda-token-client/src/index.ts b/packages/vda-token-client/src/index.ts new file mode 100644 index 00000000..5f077b0a --- /dev/null +++ b/packages/vda-token-client/src/index.ts @@ -0,0 +1,2 @@ +export * from './blockchain/VeridaTokenClient' +export * from './blockchain/VeridaTokenOwner' \ No newline at end of file diff --git a/packages/vda-token-client/test/const.ts b/packages/vda-token-client/test/const.ts new file mode 100644 index 00000000..a6c4f5a8 --- /dev/null +++ b/packages/vda-token-client/test/const.ts @@ -0,0 +1,10 @@ +import { BlockchainAnchor } from "@verida/types" +import { VeridaTokenOwner } from "../src" +import { BigNumber, Wallet } from "ethers"; +import { DID_LIST } from "@verida/vda-common-test"; + +export const TOKEN_SENDER = new Wallet(DID_LIST[0].privateKey); +export const TOKEN_APPROVER = new Wallet(DID_LIST[1].privateKey); +export const TOKEN_RECIVER = new Wallet(DID_LIST[2].privateKey); + +export const TOKEN_MINTER = new Wallet(DID_LIST[3].privateKey); \ No newline at end of file diff --git a/packages/vda-token-client/test/owner.test.ts b/packages/vda-token-client/test/owner.test.ts new file mode 100644 index 00000000..5113ef01 --- /dev/null +++ b/packages/vda-token-client/test/owner.test.ts @@ -0,0 +1,207 @@ +require('dotenv').config(); +import { VeridaTokenOwner } from "../src/index" +import { BigNumber, Wallet } from "ethers"; +import { BlockchainAnchor } from "@verida/types"; + +const assert = require('assert') + +const privateKey = process.env.PRIVATE_KEY +if (!privateKey) { + throw new Error('No PRIVATE_KEY in the env file'); +} +const blockchainAnchor = process.env.BLOCKCHAIN_ANCHOR !== undefined ? BlockchainAnchor[process.env.BLOCKCHAIN_ANCHOR] : BlockchainAnchor.POLAMOY; +const owner = new Wallet(privateKey); + +const createBlockchainAPI = async () => { + return await VeridaTokenOwner.CreateAsync({ + blockchainAnchor, + privateKey, + rpcUrl: process.env.RPC_URL + }) +} + +describe('VeridaTokenOwner test', function() { + this.timeout(200*1000) + + let blockchainApi : VeridaTokenOwner; + + const tempMinter = Wallet.createRandom(); + const randomWallet = Wallet.createRandom(); + + before(async () => { + blockchainApi = await createBlockchainAPI(); + }) + + it("Mint", async () => { + const MINT_AMOUNT = 10; + const orgBalance: BigNumber = await blockchainApi.balanceOf(); + + await blockchainApi.mint(owner.address, BigNumber.from(MINT_AMOUNT)); + + const newBalance: BigNumber = await blockchainApi.balanceOf(); + assert.ok(newBalance.eq(orgBalance.add(MINT_AMOUNT)), "Balance updated"); + }) + + it("Add a minter", async () => { + const orgMinterCount = await blockchainApi.getMinterCount(); + + await blockchainApi.addMinter(tempMinter.address); + + const newMinterCount = await blockchainApi.getMinterCount(); + assert.ok(newMinterCount === (orgMinterCount + 1), "Minter count increased") + }) + + it("Revoke a minter", async () => { + const orgMinterCount = await blockchainApi.getMinterCount(); + + await blockchainApi.revokeMinter(tempMinter.address); + + const newMinterCount = await blockchainApi.getMinterCount(); + assert.ok(newMinterCount === (orgMinterCount - 1), "Minter count decreased") + }) + + it("Set AMM pair", async () => { + const randomPair = Wallet.createRandom(); + + let status = await blockchainApi.isAMMPair(randomPair.address); + assert.ok(!status, "Not set as AMM pair"); + + // Enable as AMM + await blockchainApi.setAutomatedMarketMakerPair(randomPair.address); + status = await blockchainApi.isAMMPair(randomPair.address); + assert.ok(status === true, "Not set as AMM pair"); + + // Disable + await blockchainApi.setAutomatedMarketMakerPair(randomPair.address, false); + status = await blockchainApi.isAMMPair(randomPair.address); + assert.ok(status === false, "Not set as AMM pair"); + }) + + it("Update token limit per wallet", async () => { + const orgRate = await blockchainApi.maxAmountPerWalletRate(); + console.log("Org wallet limit : ", orgRate); + + await blockchainApi.updateMaxAmountPerWalletRate(orgRate + 0.001); + + const newRate = await blockchainApi.maxAmountPerWalletRate(); + assert.ok(newRate !== orgRate, "Max amount per wallet rate is updated"); + + // Restore org value + await blockchainApi.updateMaxAmountPerWalletRate(orgRate); + }) + + it("Update token limit per sell transaction", async () => { + const orgRate = await blockchainApi.maxAmountPerSellRate(); + console.log("Org sell limit : ", orgRate); + + await blockchainApi.updateMaxAmountPerSellRate(orgRate + 0.001); + + const newRate = await blockchainApi.maxAmountPerSellRate(); + assert.ok(newRate !== orgRate, "Max amount per sell rate is updated"); + + // Restore org value + await blockchainApi.updateMaxAmountPerSellRate(orgRate); + }) + + it("Exclude from wallet amount limit", async () => { + // Exclude + await blockchainApi.excludeFromWalletAmountLimit(randomWallet.address, true); + assert.ok(await blockchainApi.isExcludedFromWalletAmountLimit(randomWallet.address) === true, "Excluded") + + // Include + await blockchainApi.excludeFromWalletAmountLimit(randomWallet.address, false); + assert.ok(await blockchainApi.isExcludedFromWalletAmountLimit(randomWallet.address) === false, "Not excluded") + }) + + it("Exclude from sell amount limit", async () => { + // Exclude + await blockchainApi.excludeFromSellAmountLimit(randomWallet.address, true); + assert.ok(await blockchainApi.isExcludedFromSellAmountLimit(randomWallet.address) === true, "Excluded") + + // Include + await blockchainApi.excludeFromSellAmountLimit(randomWallet.address, false); + assert.ok(await blockchainApi.isExcludedFromSellAmountLimit(randomWallet.address) === false, "Not excluded") + }) + + it("Enable/disable amount limit per wallet", async () => { + await blockchainApi.enableMaxAmountPerWallet(true); + assert.ok(await blockchainApi.isWalletAmountLimitEnabled() === true, 'Wallet amount limited'); + + await blockchainApi.enableMaxAmountPerWallet(false); + assert.ok(await blockchainApi.isWalletAmountLimitEnabled() === false, 'Wallet amount not limited'); + }) + + it("Enable/disable amount limit per sell", async () => { + await blockchainApi.enableMaxAmountPerSell(true); + assert.ok(await blockchainApi.isSellAmountLimitEnabled() === true, 'Wallet amount limited'); + + await blockchainApi.enableMaxAmountPerSell(false); + assert.ok(await blockchainApi.isSellAmountLimitEnabled() === false, 'Wallet amount not limited'); + }) + + it("Enable/disable token transfer", async () => { + if (await blockchainApi.isTransferEnabled() === true) { + console.log('Token transfer already enabled'); + } else { + await blockchainApi.enableTransfer(); + assert.ok(await blockchainApi.isTransferEnabled(), 'Transfer enabled'); + } + }) + + it("Pause", async () => { + await blockchainApi.pause(); + assert.ok(await blockchainApi.isPaused(), 'Contract paused'); + + }) + + it("Unpause", async () => { + await blockchainApi.unpause(); + assert.ok(await blockchainApi.isPaused() === false, 'Contract unpaused'); + }) + + + + + + /* + describe('register', function() { + it('Should reject for invalid names', async () => { + const invalidnames = [ + 'hello world.vda', // Space in the name + 'hello!world.vda', // ! in the name + 'david.test.vda', // More than one dot in the name + 'Jerry.test', // Unregistered suffix + 'a.vda', // Name leng should be over 1 + 'abcdefghijklmnopqrstuvwxyz0123456.vda', // Name length should be below 33 + ] + for (let i = 0; i < invalidnames.length; i++) { + try { + const result = await blockchainApi.register(invalidnames[i]) + assert.fail('Fail to register invalid name') + } catch (err) { + assert.ok(err.message.match('Failed to register'), 'Fail to register invalid name') + } + } + }) + + it('Register successfully', async () => { + for (let i = 0; i < REGISTER_COUNT; i++) { + await blockchainApi.register(testNames[i]) + + const nameDID = await blockchainApi.getDID(testNames[i]) + assert.equal(nameDID.toLowerCase(), did.address.toLowerCase(), 'Get registered DID') + } + }) + + it('Should reject for registered name', async () => { + try { + const result = await blockchainApi.register(testNames[0]) + assert.fail('Fail to register invalid name') + } catch (err) { + assert.ok(err.message.match('Failed to register'), 'Fail to register invalid name') + } + }) + }) + */ + +}) \ No newline at end of file diff --git a/packages/vda-token-client/test/read.test.ts b/packages/vda-token-client/test/read.test.ts new file mode 100644 index 00000000..483d276c --- /dev/null +++ b/packages/vda-token-client/test/read.test.ts @@ -0,0 +1,138 @@ +require('dotenv').config(); +import { VeridaTokenClient } from "../src/index" +import { BigNumber, Wallet } from "ethers"; +import { BlockchainAnchor } from "@verida/types"; +import { DID_LIST } from "@verida/vda-common-test"; + +const assert = require('assert') + +const blockchainAnchor = process.env.BLOCKCHAIN_ANCHOR !== undefined ? BlockchainAnchor[process.env.BLOCKCHAIN_ANCHOR] : BlockchainAnchor.POLAMOY; +const rpcUrl = process.env.RPC_URL; + +const createBlockchainAPI = async () => { + return await VeridaTokenClient.CreateAsync({ + blockchainAnchor, + rpcUrl + }) +} + +describe('VeridaTokenClient read-only mode test', function() { + this.timeout(200*1000) + + let blockchainApi : VeridaTokenClient; + + before(async () => { + blockchainApi = await createBlockchainAPI(); + }) + + describe('General Token information', () => { + it("name", async () => { + const value = await blockchainApi.name(); + assert.ok(typeof value === 'string', 'Token name'); + }) + + it("symbol", async () => { + const value = await blockchainApi.symbol(); + assert.ok(typeof value === 'string', 'Token symbol'); + }) + + it("decimals", async () => { + const value = await blockchainApi.decimals(); + assert.ok(BigNumber.from(value).gte(0), 'Token decimals'); + }) + + it("totalSupply", async () => { + const value:BigNumber = (await blockchainApi.totalSupply())!; + assert.ok(value.gte(0), 'Token total supply'); + }) + + it("owner", async () => { + const value = await blockchainApi.owner(); + assert.ok(typeof value === 'string', 'Token owner'); + }) + + it("version", async () => { + const value = await blockchainApi.getVersion(); + assert.ok(typeof value === 'string', 'Token version'); + }) + }) + + describe('Verida-specified Token information', () => { + const randomWallet = Wallet.createRandom(); + + it("Number of minters", async () => { + const value = await blockchainApi.getMinterCount(); + assert.ok(value >= 0, 'Number of minters'); + }) + + it("List of minters", async () => { + const value = await blockchainApi.getMinterList(); + assert.ok(value.length >= 0, 'List of minters'); + }) + + it("Rate denominator", async () => { + const value = await blockchainApi.rateDenominator(); + assert.ok(value >= 0, 'Rate denominator'); + }) + + it("Max amount per wallet", async () => { + const value = await blockchainApi.maxAmountPerWalletRate(); + assert.ok(value >= 0, 'Max amount per wallet'); + }) + + it("Is wallet amount limit enabled", async () => { + const value = await blockchainApi.isWalletAmountLimitEnabled(); + assert.ok(typeof value === 'boolean', 'Wallet amount limit enabled'); + }) + + it("Is excluded from wallet amount limit", async () => { + const value = await blockchainApi.isExcludedFromWalletAmountLimit(randomWallet.address); + assert.ok(value === false, 'Excluded from wallet amount limit'); + }) + + it("Max amount per sell transaction", async () => { + const value = await blockchainApi.maxAmountPerSellRate(); + assert.ok(value >= 0, 'Max amount per sell'); + }) + + it("Is sell amount limit enabled", async () => { + const value = await blockchainApi.isSellAmountLimitEnabled(); + assert.ok(typeof value === 'boolean', 'Sell amount limit enabled'); + }) + + it("Is excluded from sell amount limit", async () => { + const value = await blockchainApi.isExcludedFromSellAmountLimit(randomWallet.address); + assert.ok(value === false, 'Excluded from sell amount limit'); + }) + + it("Is paused", async () => { + const value = await blockchainApi.isPaused(); + assert.ok(typeof value === 'boolean', 'Paused'); + }) + + it("Is transfer enabled", async () => { + const value = await blockchainApi.isTransferEnabled(); + assert.ok(typeof value === 'boolean', 'Transfer enabled'); + }) + + it("Is AMM pair", async () => { + const value = await blockchainApi.isAMMPair(randomWallet.address); + assert.ok(typeof value === 'boolean', 'Is AMM pair'); + }) + }) + + describe("Token operations", () => { + it("Get balance", async () => { + const randomAccount = Wallet.createRandom(); + const bal = await(blockchainApi.balanceOf(randomAccount.address)); + assert.ok(bal.eq(0), "Get balance"); + }) + + it("allowance", async () => { + const sender = new Wallet(DID_LIST[0].privateKey); + const approver = new Wallet(DID_LIST[1].privateKey); + const value = await blockchainApi.allowance(approver.address, sender.address); + assert.ok(value.gte(0), "Get allowance"); + }) + }) +}) \ No newline at end of file diff --git a/packages/vda-token-client/test/utils.ts b/packages/vda-token-client/test/utils.ts new file mode 100644 index 00000000..d0e98473 --- /dev/null +++ b/packages/vda-token-client/test/utils.ts @@ -0,0 +1,29 @@ +import { BlockchainAnchor } from "@verida/types" +import { VeridaTokenOwner } from "../src" +import { BigNumber, Wallet } from "ethers"; + +export interface IMintInformation { + to: string + amount: BigNumber +} + +export const mintToken = async (ownerPrivateKey: string, blockchainAnchor: BlockchainAnchor, info: IMintInformation[], rpcUrl?: string) => { + const tokenOwner = await VeridaTokenOwner.CreateAsync({ + blockchainAnchor, + privateKey: ownerPrivateKey, + rpcUrl + }); + + const contractOwner = await tokenOwner.owner(); + const curKeyAddress = new Wallet(ownerPrivateKey).address; + + if (contractOwner.toLowerCase() !== curKeyAddress.toLowerCase()) { + throw new Error(`Incorrect owner private key`); + } + + for (let i = 0; i < info.length; i++) { + await tokenOwner.mint(info[i].to, info[i].amount); + } +} + +// export const \ No newline at end of file diff --git a/packages/vda-token-client/test/write.test.ts b/packages/vda-token-client/test/write.test.ts new file mode 100644 index 00000000..e349abe7 --- /dev/null +++ b/packages/vda-token-client/test/write.test.ts @@ -0,0 +1,236 @@ +require('dotenv').config(); +import { VeridaTokenClient } from "../src/index" +import { BigNumber, Wallet } from "ethers"; +import { BlockchainAnchor, Network } from "@verida/types"; +import { DID_LIST } from "@verida/vda-common-test"; +import { IMintInformation, mintToken } from "./utils"; +import { getDefaultRpcUrl } from "@verida/vda-common"; +import { JsonRpcProvider } from "@ethersproject/providers"; + +const assert = require('assert') + +const privateKey = process.env.PRIVATE_KEY +if (!privateKey) { + throw new Error('No PRIVATE_KEY in the env file'); +} +const blockchainAnchor = process.env.BLOCKCHAIN_ANCHOR !== undefined ? BlockchainAnchor[process.env.BLOCKCHAIN_ANCHOR] : BlockchainAnchor.POLAMOY; +const rpcUrl = process.env.RPC_URL; + +const createBlockchainAPI = async (walletKey: string) => { + return await VeridaTokenClient.CreateAsync({ + blockchainAnchor, + privateKey: walletKey, + rpcUrl + }) +} + +describe('VeridaTokenClient user tests', function() { + this.timeout(200*1000) + + let blockchainApi : VeridaTokenClient; + + before(async () => { + blockchainApi = await createBlockchainAPI(privateKey); + }) + + describe('General Token information', () => { + it("name", async () => { + const value = await blockchainApi.name(); + assert.ok(typeof value === 'string', 'Token name'); + }) + + it("symbol", async () => { + const value = await blockchainApi.symbol(); + assert.ok(typeof value === 'string', 'Token symbol'); + }) + + it("decimals", async () => { + const value = await blockchainApi.decimals(); + assert.ok(BigNumber.from(value).gte(0), 'Token decimals'); + }) + + it("totalSupply", async () => { + const value:BigNumber = (await blockchainApi.totalSupply())!; + assert.ok(value.gte(0), 'Token total supply'); + }) + + it("owner", async () => { + const value = await blockchainApi.owner(); + assert.ok(typeof value === 'string', 'Token owner'); + }) + + it("version", async () => { + const value = await blockchainApi.getVersion(); + assert.ok(typeof value === 'string', 'Token version'); + }) + }) + + describe('Verida-specified Token information', () => { + const randomWallet = Wallet.createRandom(); + + it("Number of minters", async () => { + const value = await blockchainApi.getMinterCount(); + assert.ok(value >= 0, 'Number of minters'); + }) + + it("List of minters", async () => { + const value = await blockchainApi.getMinterList(); + assert.ok(value.length >= 0, 'List of minters'); + }) + + it("Rate denominator", async () => { + const value = await blockchainApi.rateDenominator(); + assert.ok(value >= 0, 'Rate denominator'); + }) + + it("Max amount per wallet", async () => { + const value = await blockchainApi.maxAmountPerWalletRate(); + assert.ok(value >= 0, 'Max amount per wallet'); + }) + + it("Is wallet amount limit enabled", async () => { + const value = await blockchainApi.isWalletAmountLimitEnabled(); + assert.ok(typeof value === 'boolean', 'Wallet amount limit enabled'); + }) + + it("Is excluded from wallet amount limit", async () => { + const value = await blockchainApi.isExcludedFromWalletAmountLimit(randomWallet.address); + assert.ok(value === false, 'Excluded from wallet amount limit'); + }) + + it("Max amount per sell transaction", async () => { + const value = await blockchainApi.maxAmountPerSellRate(); + assert.ok(value >= 0, 'Max amount per sell'); + }) + + it("Is sell amount limit enabled", async () => { + const value = await blockchainApi.isSellAmountLimitEnabled(); + assert.ok(typeof value === 'boolean', 'Sell amount limit enabled'); + }) + + it("Is excluded from sell amount limit", async () => { + const value = await blockchainApi.isExcludedFromSellAmountLimit(randomWallet.address); + assert.ok(value === false, 'Excluded from sell amount limit'); + }) + + it("Is paused", async () => { + const value = await blockchainApi.isPaused(); + assert.ok(typeof value === 'boolean', 'Paused'); + }) + + it("Is transfer enabled", async () => { + const value = await blockchainApi.isTransferEnabled(); + assert.ok(typeof value === 'boolean', 'Transfer enabled'); + }) + + it("Is AMM pair", async () => { + const value = await blockchainApi.isAMMPair(randomWallet.address); + assert.ok(typeof value === 'boolean', 'Is AMM pair'); + }) + }) + + describe("Token operations", () => { + const sender = new Wallet(DID_LIST[0].privateKey); + const approver = new Wallet(DID_LIST[1].privateKey); + const receiver = new Wallet(DID_LIST[2].privateKey); + + const AMOUNT_SEND = 1000; + const AMOUNT_APPROVE = 1000; + + let senderApi: VeridaTokenClient; + let approverApi: VeridaTokenClient; + + const checkBalances = async () => { + const contractOwner = await blockchainApi.owner(); + const curUser = new Wallet(privateKey); + + const isOwner = contractOwner.toLowerCase() === curUser.address.toLowerCase(); + + // Check token balances + if (isOwner) { + const mintAmounts : IMintInformation[] = []; + if ((await blockchainApi.balanceOf(sender.address)).lt(AMOUNT_SEND * 100)) { + mintAmounts.push({to: sender.address, amount: BigNumber.from(AMOUNT_SEND*100)}); + } + + if ((await blockchainApi.balanceOf(approver.address)).lt(AMOUNT_APPROVE * 100)) { + mintAmounts.push({to: approver.address, amount: BigNumber.from(AMOUNT_APPROVE*100)}); + } + + await mintToken(privateKey, blockchainAnchor, mintAmounts, rpcUrl); + } else { + if ((await blockchainApi.balanceOf(sender.address)).lt(AMOUNT_SEND)) { + throw new Error(`Not enough token at ${sender.address}`); + } + + if ((await blockchainApi.balanceOf(approver.address)).lt(AMOUNT_APPROVE)) { + throw new Error(`Not enough token at ${approver.address}`); + } + } + + // Check Matics + const rpc = rpcUrl ?? getDefaultRpcUrl(blockchainAnchor); + const provider = new JsonRpcProvider(rpc!); + if ((await provider.getBalance(sender.address)).eq(0)) { + throw new Error(`No Matict at ${sender.address}`); + } + + if ((await provider.getBalance(approver.address)).eq(0)) { + throw new Error(`No Matict at ${approver.address}`); + } + } + + before(async () => { + await checkBalances(); + + senderApi = await createBlockchainAPI(sender.privateKey); + approverApi = await createBlockchainAPI(approver.privateKey); + }) + + describe("Get balance", () => { + it("0 for random accounts", async () => { + const randomAccount = Wallet.createRandom(); + const bal = await(blockchainApi.balanceOf(randomAccount.address)); + assert.ok(bal.eq(0), "No balance"); + }) + + it("Get balances successfully", async () => { + const bal = await blockchainApi.balanceOf(sender.address); + assert.ok(bal.gt(0), "Get balance"); + }) + }) + + describe("Transfer", () => { + it("Transfer successfully", async () => { + const orgReceiverBalance = await blockchainApi.balanceOf(receiver.address); + + await senderApi.transfer(receiver.address, AMOUNT_SEND); + + const newReceiverBalance = await blockchainApi.balanceOf(receiver.address); + assert.ok(newReceiverBalance.eq(orgReceiverBalance.add(AMOUNT_SEND)), 'Transferred successfully'); + }) + }) + + describe("Approve & transfer", () => { + it("allowance", async () => { + const value = await blockchainApi.allowance(approver.address, sender.address); + assert.ok(value.gte(0), "Get allowance"); + }) + + it("Approve", async () => { + await approverApi.approve(sender.address, AMOUNT_APPROVE); + const allowed = await blockchainApi.allowance(approver.address, sender.address); + assert.ok(allowed.eq(AMOUNT_APPROVE), "Approved"); + }) + + it("Transfer by `transferFrom` function", async () => { + const orgReceiverBalance = await blockchainApi.balanceOf(receiver.address); + + await senderApi.transferFrom(approver.address, receiver.address, AMOUNT_APPROVE); + + const newReceiverBalance = await blockchainApi.balanceOf(receiver.address); + assert.ok(newReceiverBalance.eq(orgReceiverBalance.add(AMOUNT_APPROVE)), 'Transferred successfully'); + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/vda-token-client/tsconfig.json b/packages/vda-token-client/tsconfig.json new file mode 100644 index 00000000..6b8e551e --- /dev/null +++ b/packages/vda-token-client/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "rootDir": ".", + "outDir": "build", + "sourceMap": true, + "strict": true, + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + ], + "exclude": ["node_modules", "test", "build"], + "lib": [ + "es5", + "es2015" + ] + } + \ No newline at end of file diff --git a/packages/vda-web3-client/CHANGELOG.md b/packages/vda-web3-client/CHANGELOG.md index 9f94cb9b..dd6c21f1 100644 --- a/packages/vda-web3-client/CHANGELOG.md +++ b/packages/vda-web3-client/CHANGELOG.md @@ -5,6 +5,12 @@ 2023-12-26 (v.3.0.0) ------------------- + +- Removed `web3` parameter in `Web3SelfTransactionConfig` + +2023-12-26 (v3.0.0) +------------------- + - Fix bugs in `pure` type function call 2023-10-29 (v2.4.1) diff --git a/packages/vda-web3-client/src/VeridaContractBase.ts b/packages/vda-web3-client/src/VeridaContractBase.ts index a0992612..77a9cff2 100644 --- a/packages/vda-web3-client/src/VeridaContractBase.ts +++ b/packages/vda-web3-client/src/VeridaContractBase.ts @@ -35,6 +35,7 @@ export class VeridaContract { /** Contract instance used in web3 mode */ protected contract?: Contract + /** Signer for transactions */ protected signer?: Signer // Gasless mode variables @@ -73,7 +74,6 @@ export class VeridaContract { this.contract = getContractInstance({ provider, - web3: web3Config.web3, blockchainAnchor: web3Config.blockchainAnchor, rpcUrl: web3Config.rpcUrl, @@ -86,13 +86,15 @@ export class VeridaContract { throw new Error('either provider or rpcUrl is required to initialize') } this.signer = web3Config.signer - if (this.signer === undefined) { - if ( web3Config.privateKey ) - this.signer = new Wallet(web3Config.privateKey, this.contract.provider) - else - throw new Error('either Signer or privateKey is required to initialize') + + if (!this.signer && web3Config.privateKey) { + this.signer = new Wallet(web3Config.privateKey, this.contract.provider); } + // if(this.signer) { + // this.contract = this.contract!.connect(this.signer); + // } + const methods = config.abi.abi methods.forEach((item: any) => { if (item.type === 'function') { @@ -254,6 +256,11 @@ export class VeridaContract { if (methodType === 'view' || methodType === 'pure') { ret = await contract.callStatic[methodName](...params) } else { + + if (!this.signer) { + throw new Error(`No 'signer' or 'privateKey' in the configuration`); + } + let transaction: any // console.log("params : ", params); diff --git a/packages/vda-web3-client/src/config.ts b/packages/vda-web3-client/src/config.ts index 653e35f2..4ed515fc 100644 --- a/packages/vda-web3-client/src/config.ts +++ b/packages/vda-web3-client/src/config.ts @@ -6,22 +6,20 @@ import { BlockchainAnchor, Web3ContractInfo } from '@verida/types' import { BLOCKCHAIN_CHAINIDS } from '@verida/vda-common' /** - * Should contain provider or web3.currentProvider - * { provider: } or {web3: } + * Should contain provider + * { provider: } * * Otherwise, this configuration should contain rpcUrl, and one of `blockchainAnchor` and `chainId` * ex: { rpcUrl: , blockchainAnchor: BlockchainAnchor.POLAMOY} * ex: { rpcUrl: , chainId: '0x89'} * * @param provider Provider supported by `ethers` - * @param web3 `Web3` instance * @param rpcUrl RPC that is used in blockchain transactions * @param blockchainAnchor Verida supported blockchain {@link BlockchainAnchor} * @param chainId Blockchain Id. ex: `0x89` for Polygon mainnet */ export interface ProviderConfiguration { provider?: Provider; - web3?: any; rpcUrl?: string; blockchainAnchor?: BlockchainAnchor; @@ -52,7 +50,7 @@ export function isVeridaWeb3GasConfiguration(obj : Object) { * @returns Contract instance */ export function getContractInstance(conf: ProviderConfiguration & Web3ContractInfo): Contract { - let provider: Provider = conf.provider || conf.web3?.currentProvider + let provider: Provider | undefined = conf.provider; if (!provider) { if (conf.rpcUrl) { if (!conf.chainId && !conf.blockchainAnchor) {