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) {