diff --git a/.gitignore b/.gitignore index c3e82cc..e562349 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist tmp /out-tsc .coverage +cache/ # dependencies node_modules diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..d8cb89f --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,34 @@ +import 'tsconfig-paths/register'; +import { HardhatUserConfig } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-chai-matchers'; + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.20', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + networks: { + hardhat: { + chainId: 1337, + accounts: { + count: 20, + accountsBalance: '1000000000000000000000', // 1000 ETH + }, + }, + localhost: { + url: 'http://127.0.0.1:8545', + chainId: 1337, + }, + }, + mocha: { + timeout: 60000, + }, +}; + +export default config; diff --git a/package-lock.json b/package-lock.json index c102455..b07b207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,13 +30,18 @@ "@commitlint/config-conventional": "^19.8.0", "@commitlint/config-nx-scopes": "^19.8.0", "@commitlint/prompt": "^19.8.0", + "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^5.3.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^9.0.2", "@types/conventional-commits-parser": "^5.0.1", "@types/lodash": "^4.17.16", + "@types/mocha": "^10.0.10", "@types/node": "^18.19.86", "@vitest/coverage-v8": "^1.6.1", + "concurrently": "^9.2.0", "cpy": "^11.1.0", "dotenv": "^16.5.0", "eslint": "^9.25.1", @@ -48,17 +53,20 @@ "eslint-plugin-promise": "^7.2.1", "eth-testing": "^1.14.0", "execa": "^9.5.2", + "hardhat": "^2.25.0", "husky": "^9.1.7", "lint-staged": "^15.5.1", "prettier": "^3.5.3", "rimraf": "^5.0.10", "semantic-release": "^20.1.3", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsup": "^8.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.31.0", "vite-plugin-dts": "^3.9.1", - "vitest": "^1.6.1" + "vitest": "^1.6.1", + "wait-on": "^8.0.3" }, "engines": { "node": ">=18.17.0" @@ -70,6 +78,81 @@ "ethers": "^5.8.0" } }, + "../token-registry": { + "name": "@tradetrust-tt/token-registry", + "version": "4.0.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@typechain/ethers-v5": "10.2.1" + }, + "devDependencies": { + "@babel/cli": "^7.13.16", + "@babel/core": "^7.14.6", + "@babel/eslint-parser": "^7.18.9", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/preset-env": "^7.14.0", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "@commitlint/cli": "^12.1.1", + "@commitlint/config-conventional": "^12.1.1", + "@commitlint/prompt": "^12.1.1", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.6.2", + "@nomicfoundation/hardhat-chai-matchers": "^1.0.2", + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomicfoundation/hardhat-verify": "^2.0.8", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@openzeppelin/contracts": "^4.5.0", + "@openzeppelin/contracts-upgradeable": "^4.5.2", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.2.22", + "@types/chai-as-promised": "^7.1.4", + "@types/faker": "^5.5.8", + "@types/jest": "^26.0.23", + "@types/mocha": "^9.0.0", + "@types/node": "^15.14.9", + "@types/uuid": "^8.3.3", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "babel-jest": "^26.6.3", + "babel-plugin-module-resolver": "^4.1.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "commitizen": "^4.2.4", + "dotenv": "^8.6.0", + "eslint": "^7.29.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-chai-expect": "^2.2.0", + "eslint-plugin-chai-friendly": "^0.6.0", + "eslint-plugin-import": "^2.20.0", + "eslint-plugin-prettier": "^3.1.2", + "ethers": "^5.6.9", + "faker": "^5.5.3", + "git-cz": "^4.7.6", + "hardhat": "^2.10.1", + "hardhat-gas-reporter": "^1.0.8", + "hardhat-watcher": "^2.3.0", + "jest": "^27.2.4", + "prettier": "^2.4.1", + "prettier-plugin-solidity": "^1.0.0-dev.23", + "semantic-release": "^19.0.3", + "solhint": "^3.3.6", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.7.21", + "solium": "^1.2.5", + "ts-node": "^10.9.1", + "typechain": "^8.1.0", + "typescript": "^4.7.4", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "ethers": ">=5.0.8" + } + }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -1650,6 +1733,103 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz", + "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", + "dev": true, + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-9.1.0.tgz", + "integrity": "sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==", + "dev": true, + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "ethereum-cryptography": "^2.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, "node_modules/@ethersproject/abi": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", @@ -2356,6 +2536,21 @@ "node-fetch": "^2.7.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2933,6 +3128,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2968,6 +3175,204 @@ "node": ">= 8" } }, + "node_modules/@nomicfoundation/edr": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.11.3.tgz", + "integrity": "sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==", + "dev": true, + "dependencies": { + "@nomicfoundation/edr-darwin-arm64": "0.11.3", + "@nomicfoundation/edr-darwin-x64": "0.11.3", + "@nomicfoundation/edr-linux-arm64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-arm64-musl": "0.11.3", + "@nomicfoundation/edr-linux-x64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-x64-musl": "0.11.3", + "@nomicfoundation/edr-win32-x64-msvc": "0.11.3" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz", + "integrity": "sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz", + "integrity": "sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz", + "integrity": "sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz", + "integrity": "sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz", + "integrity": "sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz", + "integrity": "sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz", + "integrity": "sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/hardhat-chai-matchers": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-1.0.6.tgz", + "integrity": "sha512-f5ZMNmabZeZegEfuxn/0kW+mm7+yV7VNDxLpMOMGXWFJ2l/Ct3QShujzDRF9cOkK9Ui/hbDeOWGZqyQALDXVCQ==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@types/chai-as-promised": "^7.1.3", + "chai-as-promised": "^7.1.1", + "deep-eql": "^4.0.1", + "ordinal": "^1.0.3" + }, + "peerDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.0", + "chai": "^4.2.0", + "ethers": "^5.0.0", + "hardhat": "^2.9.4" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", + "integrity": "sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==", + "dev": true, + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.2", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.2" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz", + "integrity": "sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz", + "integrity": "sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz", + "integrity": "sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz", + "integrity": "sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", + "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", + "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", + "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomiclabs/hardhat-ethers": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.3.tgz", + "integrity": "sha512-YhzPdzb612X591FOe68q+qXVXGG2ANZRvDo0RRUtimev85rCrAlv/TLMEZw5c+kq9AbzocLTVX/h2jVIFPL9Xg==", + "dev": true, + "peerDependencies": { + "ethers": "^5.0.0", + "hardhat": "^2.0.0" + } + }, "node_modules/@octokit/auth-token": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", @@ -3123,6 +3528,12 @@ "@octokit/openapi-types": "^18.0.0" } }, + "node_modules/@openzeppelin/contracts": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.3.0.tgz", + "integrity": "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA==", + "dev": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3612,6 +4023,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -4496,55 +4916,214 @@ "node": ">=10" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "node_modules/@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", "dev": true, - "engines": { - "node": ">=18" + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6" } }, - "node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "dev": true, "dependencies": { - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" }, "engines": { - "node": ">=18.0.0" + "node": ">=6" } }, - "node_modules/@smithy/config-resolver": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", - "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "dev": true, "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "tslib": "^2.6.2" + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" }, "engines": { - "node": ">=18.0.0" + "node": ">=6" } }, - "node_modules/@smithy/core": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.2.tgz", - "integrity": "sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==", + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sentry/node": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", + "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", + "dev": true, + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "dev": true, + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/tracing/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dev": true, + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.2.tgz", + "integrity": "sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==", "dependencies": { "@smithy/middleware-serde": "^4.0.8", "@smithy/protocol-http": "^5.1.2", @@ -5094,6 +5673,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@tradetrust-tt/ethers-aws-kms-signer/-/ethers-aws-kms-signer-2.1.4.tgz", "integrity": "sha512-bwsPF9TOlkXUICwUIt3FJCBQZ8zJTbT3AlPdj+7dGEgPTY6sAH71BzFYFQ1aJnZht1Sww6y8ix14FK4M54lRsQ==", + "license": "MIT", "dependencies": { "@aws-sdk/client-kms": "^3.830.0", "@ethersproject/abstract-provider": "^5.8.0", @@ -5340,6 +5920,30 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", @@ -5367,12 +5971,24 @@ "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "dev": true }, + "node_modules/@types/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.120", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz", @@ -5944,6 +6560,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "dev": true, + "engines": { + "node": ">=0.3.0" + } + }, "node_modules/aes-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", @@ -5953,7 +6578,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, + "devOptional": true, "dependencies": { "debug": "4" }, @@ -6005,6 +6630,24 @@ } } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6053,6 +6696,19 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/aproba": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", @@ -6338,6 +6994,18 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bip39": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", @@ -6373,6 +7041,100 @@ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -6399,6 +7161,12 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -6431,6 +7199,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/builtins": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", @@ -6452,6 +7226,15 @@ "esbuild": ">=0.18" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6581,6 +7364,18 @@ "node": ">=4" } }, + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -6635,6 +7430,12 @@ "node": ">=10" } }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6644,7 +7445,19 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", @@ -6857,6 +7670,12 @@ "node": ">= 0.8" } }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true + }, "node_modules/command-line-args": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", @@ -7025,6 +7844,89 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -7321,6 +8223,15 @@ "node": ">=16" } }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/copy-file": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", @@ -7818,6 +8729,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -7989,6 +8909,19 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -9298,6 +10231,15 @@ "rollup": "^4.34.8" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9408,6 +10350,12 @@ "node": ">=12.20.0" } }, + "node_modules/fp-ts": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", + "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", + "dev": true + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -10029,104 +10977,414 @@ "node": ">=6" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "engines": { - "node": ">= 0.4" + "node_modules/hardhat": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.25.0.tgz", + "integrity": "sha512-yBiA74Yj3VnTRj7lhnn8GalvBdvsMOqTKRrRATSy/2v0VIR2hR0Jcnmfn4aQBLtGAnr3Q2c8CxL0g3LYegUp+g==", + "dev": true, + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/abi": "^5.1.2", + "@nomicfoundation/edr": "^0.11.1", + "@nomicfoundation/solidity-analyzer": "^0.1.0", + "@sentry/node": "^5.18.1", + "@types/bn.js": "^5.1.0", + "@types/lru-cache": "^5.1.0", + "adm-zip": "^0.4.16", + "aggregate-error": "^3.0.0", + "ansi-escapes": "^4.3.0", + "boxen": "^5.1.2", + "chokidar": "^4.0.0", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "enquirer": "^2.3.0", + "env-paths": "^2.2.0", + "ethereum-cryptography": "^1.0.3", + "find-up": "^5.0.0", + "fp-ts": "1.19.3", + "fs-extra": "^7.0.1", + "immutable": "^4.0.0-rc.12", + "io-ts": "1.10.4", + "json-stream-stringify": "^3.1.4", + "keccak": "^3.0.2", + "lodash": "^4.17.11", + "micro-eth-signer": "^0.14.0", + "mnemonist": "^0.38.0", + "mocha": "^10.0.0", + "p-map": "^4.0.0", + "picocolors": "^1.1.0", + "raw-body": "^2.4.1", + "resolve": "1.17.0", + "semver": "^6.3.0", + "solc": "0.8.26", + "source-map-support": "^0.5.13", + "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.6", + "tsort": "0.0.1", + "undici": "^5.14.0", + "uuid": "^8.3.2", + "ws": "^7.4.6" + }, + "bin": { + "hardhat": "internal/cli/bootstrap.js" }, + "peerDependencies": { + "ts-node": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/hardhat/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/hardhat/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" + "node_modules/hardhat/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/hardhat/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" } }, - "node_modules/has-proto": { + "node_modules/hardhat/node_modules/ethereum-cryptography": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/hardhat/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/hardhat/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, "dependencies": { - "has-symbols": "^1.0.3" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6 <7 || >=8" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "node_modules/hardhat/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/hardhat/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2" + "p-locate": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "node_modules/hardhat/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hardhat/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hardhat/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hardhat/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hardhat/node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hardhat/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/hardhat/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/hardhat/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/hardhat/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/hardhat/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "bin": { @@ -10167,6 +11425,22 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10207,7 +11481,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "devOptional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -10281,6 +11555,12 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10440,6 +11720,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/io-ts": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", + "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", + "dev": true, + "dependencies": { + "fp-ts": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10494,6 +11783,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -11042,6 +12343,19 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -11127,11 +12441,32 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", + "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", + "dev": true, + "engines": { + "node": ">=7.10.1" + } + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -11274,6 +12609,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12002,7 +13352,13 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true + }, + "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", @@ -12133,6 +13489,15 @@ "node": ">= 0.4" } }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/meow": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", @@ -12160,6 +13525,56 @@ "node": ">= 8" } }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz", + "integrity": "sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==", + "dev": true, + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", + "dev": true, + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12287,61 +13702,376 @@ "node": ">=0.10.0" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true + }, + "node_modules/mnemonist": { + "version": "0.38.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", + "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", + "dev": true, + "dependencies": { + "obliterator": "^2.0.0" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "engines": { "node": ">=10" } }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "node_modules/mocha/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/modify-values": { @@ -12657,6 +14387,12 @@ "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", "dev": true }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true + }, "node_modules/node-cache": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", @@ -12727,7 +14463,7 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "optional": true, + "devOptional": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -12770,6 +14506,15 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -15478,6 +17223,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "dev": true + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -15630,6 +17381,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ordinal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", + "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", + "dev": true + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -16322,6 +18079,30 @@ "node": ">=8" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -17306,6 +19087,15 @@ "node": ">=8" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -17360,6 +19150,12 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -17381,6 +19177,18 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -17602,6 +19410,51 @@ "node-gyp-build": "^4.3.0" } }, + "node_modules/solc": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", + "integrity": "sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==", + "dev": true, + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/solc/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17620,6 +19473,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spawn-error-forwarder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", @@ -17725,6 +19588,36 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -18438,6 +20331,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", @@ -18617,11 +20519,31 @@ "integrity": "sha512-Tdu3BWzaer7R5RvBIJcg9r8HrTZgpJmsX+1meXMJzYypbkj8NK2oJN0yvm4Dp/Iv6tzFa/L5jKRmEVTga6K3nA==", "optional": true }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/tsort": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", + "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", + "dev": true + }, "node_modules/tsup": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", @@ -19069,6 +20991,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -19897,6 +21828,36 @@ "typescript": "*" } }, + "node_modules/wait-on": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.3.tgz", + "integrity": "sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==", + "dev": true, + "dependencies": { + "axios": "^1.8.2", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wait-on/node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -20063,6 +22024,18 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -20098,6 +22071,12 @@ "node": ">=8" } }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -20247,6 +22226,54 @@ "node": ">=12" } }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 320119f..b8accd1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ ], "scripts": { "test": "npx vitest --run --test-timeout=15000", + "test:e2e": "concurrently -k \"npm run e2e:node\" \"npm run wait-and-test\"", + "e2e:node": "npx hardhat node", + "wait-and-test": "wait-on tcp:8545 && npm run e2e:test", + "e2e:test": "npx hardhat test src/__tests__/e2e/**/*.test.ts --network hardhat", "type-check": "tsc --noEmit", "lint": "npx eslint . --color --format=table --max-warnings=0", "lint:fix": "npx eslint . --fix", @@ -131,13 +135,18 @@ "@commitlint/config-conventional": "^19.8.0", "@commitlint/config-nx-scopes": "^19.8.0", "@commitlint/prompt": "^19.8.0", + "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^5.3.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^9.0.2", "@types/conventional-commits-parser": "^5.0.1", "@types/lodash": "^4.17.16", + "@types/mocha": "^10.0.10", "@types/node": "^18.19.86", "@vitest/coverage-v8": "^1.6.1", + "concurrently": "^9.2.0", "cpy": "^11.1.0", "dotenv": "^16.5.0", "eslint": "^9.25.1", @@ -149,17 +158,20 @@ "eslint-plugin-promise": "^7.2.1", "eth-testing": "^1.14.0", "execa": "^9.5.2", + "hardhat": "^2.25.0", "husky": "^9.1.7", "lint-staged": "^15.5.1", "prettier": "^3.5.3", "rimraf": "^5.0.10", "semantic-release": "^20.1.3", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsup": "^8.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.31.0", "vite-plugin-dts": "^3.9.1", - "vitest": "^1.6.1" + "vitest": "^1.6.1", + "wait-on": "^8.0.3" }, "overrides": { "ethers": "^5.8.0" diff --git a/src/__tests__/core/verify.test.ts b/src/__tests__/core/verify.test.ts index 6fb349b..cf540cf 100644 --- a/src/__tests__/core/verify.test.ts +++ b/src/__tests__/core/verify.test.ts @@ -8,7 +8,7 @@ import { WRAPPED_DOCUMENT_DID_TOKEN_REGISTRY_V3, WRAPPED_DOCUMENT_DNS_TXT_V2, } from '../fixtures/fixtures'; -import { W3CCredentialStatusCode } from 'src/verify/fragments/document-status/w3cCredentialStatus'; +import { W3CCredentialStatusCode } from '../../verify/fragments/document-status/w3cCredentialStatus'; const providerUrl = 'https://rpc-amoy.polygon.technology'; diff --git a/src/__tests__/e2e/fixtures.ts b/src/__tests__/e2e/fixtures.ts new file mode 100644 index 0000000..fb7d327 --- /dev/null +++ b/src/__tests__/e2e/fixtures.ts @@ -0,0 +1,123 @@ +import { ethers as ethersV5 } from 'ethers'; +import { JsonRpcProvider as JsonRpcProviderV6 } from 'ethersV6'; +import { Wallet as WalletV6 } from 'ethersV6'; +// Hardhat local node URL +export const HARDHAT_RPC_URL = 'http://127.0.0.1:8545'; + +// Create ethers v5 provider for Hardhat +export const providerV5 = new ethersV5.providers.JsonRpcProvider(HARDHAT_RPC_URL); + +// Create ethers v6 provider for Hardhat +export const providerV6 = new JsonRpcProviderV6(HARDHAT_RPC_URL); + +// Utility function to create a signer with v5 provider +export const createSignerV5 = (privateKey: string) => { + return new ethersV5.Wallet(privateKey, providerV5); +}; + +// Utility function to create a signer with v6 provider +export const createSignerV6 = (privateKey: string) => { + return new WalletV6(privateKey, providerV6); +}; + +// Provider detection utilities for testing +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getProviderVersion = (provider: any): 'v5' | 'v6' => { + // Check for v6-specific properties + if (provider._getConnection || provider.getNetwork?.length === 0) { + return 'v6'; + } + // Check for v5-specific properties + if (provider.getNetwork?.length === 1 || provider.connection) { + return 'v5'; + } + return 'v5'; // default fallback +}; + +// Mock hardhat ethers provider for testing +export const createMockHardhatProvider = (version: 'v5' | 'v6' = 'v5') => { + if (version === 'v6') { + return providerV6; + } + return providerV5; +}; + +// Generate test private keys (similar to Hardhat's approach) +const generateTestV5PrivateKeys = (count: number): string[] => { + const keys: string[] = []; + // Start with some well-known test keys (same as Hardhat) + const baseKeys = [ + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', + '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a', + '0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6', + '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a', + '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', + '0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e', + '0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356', + '0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97', + '0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6', + ]; + + for (let i = 0; i < count; i++) { + if (i < baseKeys.length) { + keys.push(baseKeys[i]); + } else { + // Generate additional keys if needed + const randomKey = ethersV5.Wallet.createRandom().privateKey; + keys.push(randomKey); + } + } + + return keys; +}; +const generateTestV6PrivateKeys = (count: number): string[] => { + const keys: string[] = []; + // Start with some well-known test keys (same as Hardhat) + const baseKeys = [ + '0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897', + '0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82', + '0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1', + '0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd', + '0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa', + '0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61', + '0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0', + '0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd', + '0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0', + '0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e', + ]; + + for (let i = 0; i < count; i++) { + if (i < baseKeys.length) { + keys.push(baseKeys[i]); + } else { + // Generate additional keys if needed + const randomKey = WalletV6.createRandom().privateKey; + keys.push(randomKey); + } + } + + return keys; +}; + +// Get multiple signers for ethers v5 (similar to hardhat getSigners) +export const getSignersV5 = async (count: number = 10): Promise => { + const privateKeys = generateTestV5PrivateKeys(count); + const wallets = privateKeys.map((key) => new ethersV5.Wallet(key, providerV5)); + + // Fund the wallets from Hardhat's pre-funded account + // await fundWalletsV5(wallets); + + return wallets; +}; + +// Get multiple signers for ethers v6 (similar to hardhat getSigners) +export const getSignersV6 = async (count: number = 10): Promise => { + const privateKeys = generateTestV6PrivateKeys(count); + const wallets = privateKeys.map((key) => new WalletV6(key, providerV6)); + + // Fund the wallets from Hardhat's pre-funded account + // await fundWalletsV6Simple(wallets); + + return wallets; +}; diff --git a/src/__tests__/e2e/token-registry-functions/rejectTransfer.e2e.test.ts b/src/__tests__/e2e/token-registry-functions/rejectTransfer.e2e.test.ts new file mode 100644 index 0000000..f832219 --- /dev/null +++ b/src/__tests__/e2e/token-registry-functions/rejectTransfer.e2e.test.ts @@ -0,0 +1,1572 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +import { ethers as ethersV6, ZeroAddress } from 'ethersV6'; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-chai-matchers'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import type { + MintTokenOptions, + MintTokenParams, + TransactionOptions, + ProviderInfo, +} from '../../../token-registry-functions/types'; +import { ethers, Signer } from 'ethers'; + +// Import our new signer utilities +import { getSignersV5, getSignersV6, providerV5, providerV6 } from '../fixtures'; +import { createContract, getVersionedContractFactory } from '../utils'; +import { + mint, + transferHolder, + transferBeneficiary, + transferOwners, + nominate, + rejectTransferBeneficiary, + rejectTransferHolder, + rejectTransferOwners, +} from '../../../token-registry-functions'; + +interface ContractAddresses { + tokenAddress: string; + titleEscrow: string; + holder: string; + beneficiary: string; + newHolder: string; + newBeneficiary: string; + owner: string; + newOwner: string; +} + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, +]; + +providers.forEach(({ ethersVersion, titleEscrowVersion }) => { + describe(`Reject Transfer Functions E2E Tests -with ethers ${ethersVersion} and token registry ${titleEscrowVersion}`, async function () { + let TradeTrustTokenContract: any; + let TitleEscrowFactoryContract: any; + let titleEscrowFactoryAddress: any; + let titleEscrow: any; + let deployer: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let owner: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newOwner: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let holder: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let beneficiary: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newHolder: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newBeneficiary: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let addresses: ContractAddresses; + let tradeTrustTokenAddress: string; + + before(async function () { + await network.provider.send('evm_setAutomine', [true]); // Ensure auto-mining + await network.provider.send('hardhat_reset'); // Reset network between tests + // Reset nonce tracker for clean state (especially important for v6) + + // Get signers using our custom utility (returns ethers.Wallet[]) + // For v6, use unique private keys based on provider index to avoid nonce conflicts + const signers = ethersVersion === 'v5' ? await getSignersV5(11) : await getSignersV6(11); // Larger offset for v6 + // const signers = await hardhatEthers.getSigners(); + [deployer, owner, newOwner, holder, beneficiary, newHolder, newBeneficiary] = signers; + + // Deploy TitleEscrowFactory first + console.log('Deploying TitleEscrowFactory...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const titleEscrowFactory = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + TitleEscrowFactoryContract = await titleEscrowFactory.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContract.waitForDeployment(); + } else { + await TitleEscrowFactoryContract.deployTransaction.wait(); + } + + titleEscrowFactoryAddress = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContract as ethers.Contract).address + : (TitleEscrowFactoryContract as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + console.log('Deploying TradeTrustToken...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactory = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + // add a time delay here + + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + ); + // await TradeTrustTokenContract.wait(); + } + + tradeTrustTokenAddress = + ethersVersion === 'v5' + ? (TradeTrustTokenContract as ethers.Contract).address + : await (TradeTrustTokenContract as ethersV6.Contract).getAddress(); + console.log('TradeTrustToken deployed to:', tradeTrustTokenAddress); + + console.log('All mock contracts deployed and initialized for E2E testing'); + console.log('Minting token...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion, + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const tx0 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await tx0.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + titleEscrow = createContract( + await TradeTrustTokenContract.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + addresses = { + tokenAddress: tradeTrustTokenAddress, + titleEscrow: titleEscrow.address || titleEscrow.target, + holder: holder.address, + beneficiary: beneficiary.address, + newHolder: newHolder.address, + newBeneficiary: newBeneficiary.address, + owner: owner.address, + newOwner: newOwner.address, + }; + }); + describe('rejectTransferHolder', () => { + describe('Successful Rejection of Holder', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + holderAddress: addresses.newHolder, + remarks: 'Transfer Holder', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const prevHolder = await titleEscrow.prevHolder(); + // Execute transfer using current holder (newHolder) + if (prevHolder == ZeroAddress) { + const tx0 = await transferHolder(contractOptions, holder as any, params, options); + await tx0.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(currentHolder).to.equal(newHolder.address); + expect(currentBeneficiary).to.equal(beneficiary.address); + expect(prevHolder).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + }); + it('should reject holder transfer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferHolder(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevHolder = await titleEscrow.prevHolder(); + expect(prevHolder).to.equal(ZeroAddress); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should reject holder transfer successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferHolder(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferHolder(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + }); + + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await rejectTransferHolder(contractOptions, newHolder, params, options); + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferHolder(contractOptions, holder as any, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferHolder( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer holder to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferHolder failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + describe('rejectTransferBeneficiary', () => { + describe('Successful Rejection of Beneficiary', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.newBeneficiary, + remarks: 'Transfer Beneficiary', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + // Execute transfer using current holder (newHolder) + if (prevBeneficiary == ZeroAddress) { + const tx0 = await nominate(contractOptions, beneficiary, params, options); + await tx0.wait(); + const tx1 = await transferBeneficiary(contractOptions, holder, params, options); + await tx1.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(currentHolder).to.equal(holder.address); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(prevHolder).to.equal(ZeroAddress); + expect(prevBeneficiary).to.equal(beneficiary.address); + }); + it('should reject beneficiary transfer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevHolder = await titleEscrow.prevHolder(); + expect(prevHolder).to.equal(ZeroAddress); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should reject holder transfer successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + }); + + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferBeneficiary(contractOptions, newBeneficiary, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferBeneficiary( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer beneficiary to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferBeneficiary failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + describe('rejectTransferOwners', () => { + before(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.holder, + remarks: 'Transfer Beneficiary', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + // Execute transfer using current holder (newHolder) + const tx0 = await nominate(contractOptions, beneficiary, params, options); + await tx0.wait(); + const tx1 = await transferBeneficiary(contractOptions, holder, params, options); + await tx1.wait(); + }); + describe('Successful Rejection of Owners', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.newHolder, //keeping both beneficiary and holder same + newHolderAddress: addresses.newHolder, + remarks: 'Transfer Owners', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + + // Execute transfer using current holder (newHolder) + if (prevBeneficiary == ZeroAddress || prevHolder == ZeroAddress) { + const tx1 = await transferOwners(contractOptions, holder, params, options); + await tx1.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(currentHolder).to.equal(newHolder.address); + expect(currentBeneficiary).to.equal(newHolder.address); + + expect(prevHolder).to.equal(holder.address); + expect(prevBeneficiary).to.equal(holder.address); + }); + it('should reject Owners transfer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Reject transfer owners', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should reject holder transfer successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Reject Transfer Owners to Previous address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferOwners( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer beneficiary to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferOwners failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + }); +}); diff --git a/src/__tests__/e2e/token-registry-functions/returnToken.e2e.test.ts b/src/__tests__/e2e/token-registry-functions/returnToken.e2e.test.ts new file mode 100644 index 0000000..9337359 --- /dev/null +++ b/src/__tests__/e2e/token-registry-functions/returnToken.e2e.test.ts @@ -0,0 +1,1556 @@ +import { expect } from 'chai'; +import { ethers as ethersV6, ZeroAddress } from 'ethersV6'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +// Import the functions we want to test +import { + transferBeneficiary, + transferOwners, + nominate, + mint, + rejectReturned, + rejectTransferBeneficiary, + rejectTransferHolder, + rejectTransferOwners, + returnToIssuer, +} from '../../../token-registry-functions'; +import type { + MintTokenOptions, + MintTokenParams, + TransactionOptions, + ProviderInfo, +} from '../../../token-registry-functions/types'; +import { ethers, Signer } from 'ethers'; + +// Import our new signer utilities +import { getSignersV5, getSignersV6, providerV5, providerV6 } from '../fixtures'; +import { createContract, getVersionedContractFactory } from '../utils'; +interface ContractAddresses { + tokenAddress: string; + titleEscrow: string; + holder: string; + beneficiary: string; + newHolder: string; + newBeneficiary: string; + owner: string; + newOwner: string; +} + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; + +providers.forEach(({ ethersVersion, titleEscrowVersion }) => { + describe(`Return Token Functions E2E Tests -with ethers ${ethersVersion} and token registry ${titleEscrowVersion}`, async function () { + let TradeTrustTokenContract: any; + let TitleEscrowFactoryContract: any; + let titleEscrowFactoryAddress: any; + let titleEscrow: any; + let deployer: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let owner: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newOwner: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let holder: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let beneficiary: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newHolder: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let newBeneficiary: ethers.Wallet | ethersV6.Wallet | SignerWithAddress; + let addresses: ContractAddresses; + let tradeTrustTokenAddress: string; + + before(async function () { + // Reset nonce tracker for clean state (especially important for v6) + + // Get signers using our custom utility (returns ethers.Wallet[]) + // For v6, use unique private keys based on provider index to avoid nonce conflicts + const signers = ethersVersion === 'v5' ? await getSignersV5(11) : await getSignersV6(11); // Larger offset for v6 + // const signers = await hardhatEthers.getSigners(); + [deployer, owner, newOwner, holder, beneficiary, newHolder, newBeneficiary] = signers; + + // Deploy TitleEscrowFactory first + console.log('Deploying TitleEscrowFactory...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const titleEscrowFactory = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + TitleEscrowFactoryContract = await titleEscrowFactory.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContract.waitForDeployment(); + } else { + await TitleEscrowFactoryContract.deployTransaction.wait(); + } + + titleEscrowFactoryAddress = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContract as ethers.Contract).address + : (TitleEscrowFactoryContract as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + console.log('Deploying TradeTrustToken...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactory = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + // add a time delay here + + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + ); + // await TradeTrustTokenContract.wait(); + } + + tradeTrustTokenAddress = + ethersVersion === 'v5' + ? (TradeTrustTokenContract as ethers.Contract).address + : await (TradeTrustTokenContract as ethersV6.Contract).getAddress(); + console.log('TradeTrustToken deployed to:', tradeTrustTokenAddress); + + console.log('All mock contracts deployed and initialized for E2E testing'); + console.log('Minting token...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion, + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const tx0 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await tx0.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + titleEscrow = createContract( + await TradeTrustTokenContract.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + titleEscrowVersion, + deployer, + ); + + addresses = { + tokenAddress: tradeTrustTokenAddress, + titleEscrow: titleEscrow.address || titleEscrow.target, + holder: holder.address, + beneficiary: beneficiary.address, + newHolder: newHolder.address, + newBeneficiary: newBeneficiary.address, + owner: owner.address, + newOwner: newOwner.address, + }; + }); + describe('returnToIssuer', () => { + describe.only('Successful Return to Issuer', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + }; + + const params = { + tokenId: '0', + remarks: 'Reject Returned Document', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const ownerOfToken = await TradeTrustTokenContract.ownerOf('0'); + if (ownerOfToken !== addresses.titleEscrow) { + const tx = await rejectReturned(contractOptions, deployer, params, options); + await tx.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + + expect(currentHolder).to.equal(holder.address); + expect(currentBeneficiary).to.equal(holder.address); + }); + it('should return to issuer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + // Execute transfer using type assertion to bypass type issues + const tx = await returnToIssuer(contractOptions, holder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify token owner has changed + const ownerOfToken = await TradeTrustTokenContract.ownerOf('0'); + expect(ownerOfToken).to.equal(addresses.tokenAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + it('should return to issue successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await returnToIssuer(contractOptions, holder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify token owner has changed + const ownerOfToken = await TradeTrustTokenContract.ownerOf('0'); + expect(ownerOfToken).to.equal(addresses.tokenAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await returnToIssuer(contractOptions, holder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify owner has changed + const ownerOfToken = await TradeTrustTokenContract.ownerOf('0'); + expect(ownerOfToken).to.equal(addresses.tokenAddress); + }); + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await returnToIssuer(contractOptions, holder, params, options); + await tx.wait(); + + // Verify owner has changed + const ownerOfToken = await TradeTrustTokenContract.ownerOf('0'); + expect(ownerOfToken).to.equal(addresses.tokenAddress); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferHolder(contractOptions, holder as any, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferHolder( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer holder to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferHolder failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferHolder(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + describe('rejectTransferBeneficiary', () => { + describe('Successful Rejection of Beneficiary', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.newBeneficiary, + remarks: 'Transfer Beneficiary', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + // Execute transfer using current holder (newHolder) + if (prevBeneficiary == ZeroAddress) { + const tx0 = await nominate(contractOptions, beneficiary, params, options); + await tx0.wait(); + const tx1 = await transferBeneficiary(contractOptions, holder, params, options); + await tx1.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(currentHolder).to.equal(holder.address); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(prevHolder).to.equal(ZeroAddress); + expect(prevBeneficiary).to.equal(beneficiary.address); + }); + it('should reject beneficiary transfer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow.holder(); + expect(newHolderAddress).to.equal(holder.address); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevHolder = await titleEscrow.prevHolder(); + expect(prevHolder).to.equal(ZeroAddress); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should reject holder transfer successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + }); + + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await rejectTransferBeneficiary( + contractOptions, + newBeneficiary, + params, + options, + ); + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + expect(newBeneficiaryAddress).to.equal(beneficiary.address); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + expect(prevBeneficiary).to.equal(ZeroAddress); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferBeneficiary(contractOptions, newBeneficiary, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferBeneficiary( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer beneficiary to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferBeneficiary failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferBeneficiary(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + describe('rejectTransferOwners', () => { + before(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.holder, + remarks: 'Transfer Beneficiary', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + // Execute transfer using current holder (newHolder) + const tx0 = await nominate(contractOptions, beneficiary, params, options); + await tx0.wait(); + const tx1 = await transferBeneficiary(contractOptions, holder, params, options); + await tx1.wait(); + }); + describe('Successful Rejection of Owners', () => { + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + newBeneficiaryAddress: addresses.newHolder, //keeping both beneficiary and holder same + newHolderAddress: addresses.newHolder, + remarks: 'Transfer Owners', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + + // Execute transfer using current holder (newHolder) + if (prevBeneficiary == ZeroAddress || prevHolder == ZeroAddress) { + const tx1 = await transferOwners(contractOptions, holder, params, options); + await tx1.wait(); + } + }); + it('should have correct initial state', async function () { + const currentHolder = await titleEscrow.holder(); + const currentBeneficiary = await titleEscrow.beneficiary(); + + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(currentHolder).to.equal(newHolder.address); + expect(currentBeneficiary).to.equal(newHolder.address); + + expect(prevHolder).to.equal(holder.address); + expect(prevBeneficiary).to.equal(holder.address); + }); + it('should reject Owners transfer successfully with remarks provided', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Reject transfer owners', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should reject holder transfer successfully without remarks', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: '', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should detect version v5 automatically when titleEscrowVersion is not passed', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Reject Transfer Owners to Previous address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should handle undefined/empty options object without crashing (safeguards)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = {}; + + const options = { + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + + const tx = await rejectTransferOwners(contractOptions, newHolder, params, options); + await tx.wait(); + + // Verify Owners has changed + const newHolderAddress = await titleEscrow.holder(); + const newBeneficiaryAddress = await titleEscrow.beneficiary(); + const prevHolder = await titleEscrow.prevHolder(); + const prevBeneficiary = await titleEscrow.prevBeneficiary(); + + expect(newHolderAddress).to.equal(holder.address); + expect(newBeneficiaryAddress).to.equal(holder.address); + expect(prevBeneficiary).to.equal(ZeroAddress); + expect(prevHolder).to.equal(ZeroAddress); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + }); + describe('Error Handling', () => { + it('should fail if titleEscrowAddress is not derivable from tokenRegistryAddress and tokenId', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '2', //invalid token ID + }; + + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + expect(error.message).to.include('ERC721: owner query for nonexistent token'); + } + }); + + it('should throw error if signer has no provider', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + const params = { + remarks: 'Transfer both roles', + }; + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + try { + await rejectTransferOwners( + contractOptions, + signerWithoutProvider as any, + params, + options, + ); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + + it('should throw error if titleEscrow contract is not version v5', async () => { + const titleEscrowFactoryV4 = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + 'v4', + deployer, + ); + + const TitleEscrowFactoryContractV4 = await titleEscrowFactoryV4.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContractV4.waitForDeployment(); + } + + const titleEscrowFactoryAddressV4 = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContractV4 as ethers.Contract).address + : (TitleEscrowFactoryContractV4 as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + await new Promise((resolve) => setTimeout(resolve, 1000)); + const tradeTrustTokenFactoryV4 = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + 'v4', + deployer, + ); + + // add a time delay here + let TradeTrustTokenContractV4: any; + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(deployer.address, 'pending'); + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContractV4 = await tradeTrustTokenFactoryV4.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddressV4, + ); + // await TradeTrustTokenContract.wait(); + } + + const tradeTrustTokenAddressV4 = + ethersVersion === 'v5' + ? (TradeTrustTokenContractV4 as ethers.Contract).address + : await (TradeTrustTokenContractV4 as ethersV6.Contract).getAddress(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddressV4, + }; + + const params: MintTokenParams = { + beneficiaryAddress: holder.address, //keeping both initial holder and beneficiary same + holderAddress: holder.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: 'v4', + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const txV4 = await mint(contractOptions, deployer as unknown as Signer, params, options); + await txV4.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const titleEscrowv4 = createContract( + await TradeTrustTokenContractV4.ownerOf('0'), + 'TitleEscrow', + ethersVersion, + 'v4', + deployer, + ); + const contractOptionsV4 = { + titleEscrowAddress: (titleEscrowv4.address || titleEscrowv4.target) as string, + }; + + const paramsV4 = { + newBeneficiaryAddress: addresses.newBeneficiary, + newHolderAddress: addresses.newHolder, + remarks: 'Transfer both roles', + }; + + const optionsV4 = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptionsV4, holder as any, paramsV4, optionsV4); + await tx.wait(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const contractOptionsV4Reject = { + titleEscrowAddress: titleEscrowv4.address || titleEscrowv4.target, + }; + + const paramsV4Reject = { + remarks: 'Reject transfer beneficiary to new address', + }; + + const optionsV4Reject = { + chainId: CHAIN_ID.local, + titleEscrowVersion: 'v4' as const, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners( + contractOptionsV4Reject, + newHolder, + paramsV4Reject, + optionsV4Reject, + ); + } catch (error: any) { + expect(error.message).to.equal('Only Token Registry V5 is supported'); + } + }); + + it('should throw error when callStatic rejectTransferHolder fails', async () => { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow, + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); //reject is invalid + } catch (error: any) { + expect(error.message).to.equal( + 'Pre-check (callStatic) for rejectTransferOwners failed', + ); + } + }); + + it('should fail if tokenRegistryAddress is not provided when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should fail if tokenId is missing when titleEscrowAddress is undefined', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + id: 'test-encryption-id', + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal('Token ID is required'); + } + }); + + it('should throw error if `encrypt` function throws (invalid remarks or id)', async () => { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + }; + + const params = { + remarks: 'Transfer Beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + }; + + // Execute transfer using type assertion to bypass type issues + try { + await rejectTransferOwners(contractOptions, newHolder, params, options); + } catch (error: any) { + expect(error.message).to.equal( + `Cannot read properties of undefined (reading 'length')`, + ); + } + }); + // it('should allow retrying the transaction if it fails due to nonce issues'); + }); + }); + }); +}); diff --git a/src/__tests__/e2e/token-registry-functions/transfer.e2e.test.ts b/src/__tests__/e2e/token-registry-functions/transfer.e2e.test.ts new file mode 100644 index 0000000..e44c5cd --- /dev/null +++ b/src/__tests__/e2e/token-registry-functions/transfer.e2e.test.ts @@ -0,0 +1,840 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +import { ethers as ethersV6 } from 'ethersV6'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; + +// Import the functions we want to test +import { + mint, + transferHolder, + transferBeneficiary, + transferOwners, + nominate, +} from '../../../token-registry-functions'; +import type { + MintTokenOptions, + MintTokenParams, + TransactionOptions, + ProviderInfo, +} from '../../../token-registry-functions/types'; +import { ethers, Signer } from 'ethers'; + +// Import our new signer utilities +import { getSignersV5, getSignersV6, providerV5, providerV6 } from '../fixtures'; +import { createContract, getVersionedContractFactory } from '../utils'; +import { getTitleEscrowAddress } from '../../../core'; + +interface ContractAddresses { + tokenAddress: string; + titleEscrow0: string; + titleEscrow1: string; + holder1: string; + beneficiary1: string; + holder2: string; + beneficiary2: string; +} + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; + +providers.forEach(({ Provider, ethersVersion, titleEscrowVersion }) => { + describe(`Transfer Functions E2E Tests -with ethers ${ethersVersion} and token registry ${titleEscrowVersion}`, async function () { + let TradeTrustTokenContract: any; + let TitleEscrowFactoryContract: any; + let titleEscrowFactoryAddress: any; + let titleEscrow0: any; + let titleEscrow1: any; + let owner: ethers.Wallet | ethersV6.Wallet; + let holder1: ethers.Wallet | ethersV6.Wallet; + let beneficiary1: ethers.Wallet | ethersV6.Wallet; + let holder2: ethers.Wallet | ethersV6.Wallet; + let beneficiary2: ethers.Wallet | ethersV6.Wallet; + let newHolder: ethers.Wallet | ethersV6.Wallet; + let newBeneficiary: ethers.Wallet | ethersV6.Wallet; + let addresses: ContractAddresses; + let tradeTrustTokenAddress: string; + + before(async function () { + await network.provider.send('evm_setAutomine', [true]); // Ensure auto-mining + await network.provider.send('hardhat_reset'); // Reset network between tests + // Reset nonce tracker for clean state (especially important for v6) + // resetNonceTracker(); + + // Get signers using our custom utility (returns ethers.Wallet[]) + // For v6, use unique private keys based on provider index to avoid nonce conflicts + const signers = ethersVersion === 'v5' ? await getSignersV5(8) : await getSignersV6(8); // Larger offset for v6 + // const signers = await hardhatEthers.getSigners(); + [owner, holder1, beneficiary1, holder2, beneficiary2, newHolder, newBeneficiary] = signers; + + // Deploy TitleEscrowFactory first + console.log('Deploying TitleEscrowFactory...'); + + const titleEscrowFactory = getVersionedContractFactory( + 'TitleEscrowFactory', + ethersVersion, + titleEscrowVersion, + owner, + ); + + TitleEscrowFactoryContract = await titleEscrowFactory.deploy(); + if (ethersVersion === 'v6') { + await TitleEscrowFactoryContract.waitForDeployment(); + } else { + await TitleEscrowFactoryContract.deployTransaction.wait(); + } + + titleEscrowFactoryAddress = + ethersVersion === 'v5' + ? (TitleEscrowFactoryContract as ethers.Contract).address + : (TitleEscrowFactoryContract as ethersV6.Contract).target; + + // Deploy TradeTrustToken with proper constructor arguments + console.log('Deploying TradeTrustToken...'); + const tradeTrustTokenFactory = getVersionedContractFactory( + 'TradeTrustToken', + ethersVersion, + titleEscrowVersion, + owner, + ); + + // add a time delay here + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (ethersVersion === 'v6') { + const nonce = await providerV6.getTransactionCount(owner.address, 'pending'); + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + { + nonce: nonce, + }, + ); + } else { + TradeTrustTokenContract = await tradeTrustTokenFactory.deploy( + 'Test TradeTrust Token', + 'TTT', + titleEscrowFactoryAddress, + ); + // await TradeTrustTokenContract.wait(); + } + + tradeTrustTokenAddress = + ethersVersion === 'v5' + ? (TradeTrustTokenContract as ethers.Contract).address + : await (TradeTrustTokenContract as ethersV6.Contract).getAddress(); + console.log('TradeTrustToken deployed to:', tradeTrustTokenAddress); + + console.log('All mock contracts deployed and initialized for E2E testing'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + + describe('Token Minting', function () { + beforeEach(async function () { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + it('should mint token 0 with beneficiary1 and holder1', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary1.address, + holderAddress: holder1.address, + tokenId: '0', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion, + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const tx1 = await mint(contractOptions, owner as unknown as Signer, params, options); + await tx1.wait(); + + // Verify token ownership + const ownerOfToken0 = await TradeTrustTokenContract.ownerOf('0'); + expect(ownerOfToken0).to.not.equal('0x0000000000000000000000000000000000000000'); + }); + + it('should mint token 1 with beneficiary2 and holder2', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary2.address, + holderAddress: holder2.address, + tokenId: '1', + remarks: 'Initial mint for testing', + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + id: 'test-encryption-key', + }; + + const tx2 = await mint(contractOptions, owner as unknown as Signer, params, options); + await tx2.wait(); + + // Verify token ownership + const ownerOfToken1 = await TradeTrustTokenContract.ownerOf('1'); + expect(ownerOfToken1).to.not.equal('0x0000000000000000000000000000000000000000'); + }); + + it('should fail when trying to mint duplicate token ID', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary1.address, + holderAddress: holder1.address, + tokenId: '0', // Same as first token - should fail + remarks: 'Duplicate mint attempt', + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + }; + + // Should revert due to duplicate token ID + try { + await mint(contractOptions, owner as unknown as Signer, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Pre-check (callStatic) for mint failed'); + } + }); + + it('should fail when minting with invalid beneficiary address', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: '0x0000000000000000000000000000000000000000', // Invalid zero address + holderAddress: holder1.address, + tokenId: '999', + remarks: 'Invalid address test', + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + }; + + // Should revert due to invalid beneficiary address + try { + await mint(contractOptions, owner as unknown as Signer, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Pre-check (callStatic) for mint failed'); + } + }); + + it('should fail when non-owner tries to mint', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary1.address, + holderAddress: holder1.address, + tokenId: '998', + remarks: 'Unauthorized mint attempt', + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + }; + + // Use holder1 (non-owner) as signer - should fail + try { + await mint(contractOptions, holder1 as unknown as Signer, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Pre-check (callStatic) for mint failed'); + } + }); + + it('should mint token with encrypted remarks (V5 only)', async function () { + // Skip this test for V4 as it doesn't support encrypted remarks + if (titleEscrowVersion === 'v4') { + console.log('Skipping encrypted remarks test for V4'); + this.skip(); + } + + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary1.address, + holderAddress: holder1.address, + tokenId: '997', + remarks: 'This should be encrypted in V5 contracts', + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + id: 'test-encryption-key', // Enable encryption for V5 + }; + + const tx = await mint(contractOptions, owner as unknown as Signer, params, options); + await tx.wait(); + + // Verify token was created successfully + const ownerOfToken = await TradeTrustTokenContract.ownerOf('997'); + expect(ownerOfToken).to.not.equal('0x0000000000000000000000000000000000000000'); + }); + + it('should mint token without remarks', async function () { + const contractOptions: MintTokenOptions = { + tokenRegistryAddress: tradeTrustTokenAddress, + }; + + const params: MintTokenParams = { + beneficiaryAddress: beneficiary2.address, + holderAddress: holder2.address, + tokenId: '996', + // No remarks property - should work fine + }; + + const options: TransactionOptions = { + titleEscrowVersion: titleEscrowVersion, + chainId: CHAIN_ID.local, + }; + + const tx = await mint(contractOptions, owner as unknown as Signer, params, options); + await tx.wait(); + + // Verify token was created successfully + const ownerOfToken = await TradeTrustTokenContract.ownerOf('996'); + expect(ownerOfToken).to.not.equal('0x0000000000000000000000000000000000000000'); + }); + + it('should set up title escrow addresses and contract instances', async function () { + // Get title escrow addresses using the factory + + const titleEscrow0Address = await getTitleEscrowAddress( + tradeTrustTokenAddress, + '0', + Provider, + ); + + const titleEscrow1Address = await getTitleEscrowAddress( + tradeTrustTokenAddress, + '1', + Provider, + ); + + // Get contract instances + titleEscrow0 = createContract( + titleEscrow0Address, + 'TitleEscrow', + ethersVersion, + titleEscrowVersion, + owner, + ); + + titleEscrow1 = createContract( + titleEscrow1Address, + 'TitleEscrow', + ethersVersion, + titleEscrowVersion, + owner, + ); + + // Set up addresses object for use in transfer tests + addresses = { + tokenAddress: tradeTrustTokenAddress, + titleEscrow0: titleEscrow0Address, + titleEscrow1: titleEscrow1Address, + holder1: holder1.address, + beneficiary1: beneficiary1.address, + holder2: holder2.address, + beneficiary2: beneficiary2.address, + }; + + // Verify that escrow contracts are properly connected + expect(titleEscrow0Address).to.not.equal('0x0000000000000000000000000000000000000000'); + expect(titleEscrow1Address).to.not.equal('0x0000000000000000000000000000000000000000'); + expect(addresses.tokenAddress).to.equal(tradeTrustTokenAddress); + }); + + it('should verify token registry state after minting', async function () { + // Verify specific token ownership + const token0Owner = await TradeTrustTokenContract.ownerOf('0'); + const token1Owner = await TradeTrustTokenContract.ownerOf('1'); + + expect(token0Owner).to.equal(addresses.titleEscrow0); + expect(token1Owner).to.equal(addresses.titleEscrow1); + }); + }); + + describe('Transfer Holder', function () { + it('should have correct initial state', async function () { + const initialHolder = await titleEscrow0.holder(); + const initialBeneficiary = await titleEscrow0.beneficiary(); + + expect(initialHolder).to.equal(holder1.address); + expect(initialBeneficiary).to.equal(beneficiary1.address); + }); + + it('should successfully transfer holder role from holder1 to newHolder', async function () { + const contractOptions = { + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + holderAddress: newHolder.address, + remarks: 'Transfer holder to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Check initial state + const initialHolder = await titleEscrow0.holder(); + + expect(initialHolder).to.equal(holder1.address); + + // Execute transfer using type assertion to bypass type issues + const tx = await transferHolder(contractOptions, holder1, params, options); + + // Wait for transaction to be mined + await tx.wait(); + + // Verify holder has changed + const newHolderAddress = await titleEscrow0.holder(); + expect(newHolderAddress).to.equal(newHolder.address); + + // Verify event was emitted + const receipt = await tx.wait(); + const events = receipt?.logs || []; + expect(events.length).to.be.greaterThan(0); + }); + + it('should fail when non-holder tries to transfer holder role', async function () { + const tokenId = '1'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow1, + }; + + const params = { + holderAddress: newHolder.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + // Try to transfer from non-holder (should fail) + try { + await transferHolder(contractOptions, beneficiary2 as any, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Pre-check (callStatic) for transferHolder failed'); + } + }); + + it('should handle transfer without remarks', async function () { + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '1', + titleEscrowAddress: addresses.titleEscrow1, + }; + + const params = { + holderAddress: newHolder.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + const tx = await transferHolder(contractOptions, holder2 as any, params, options); + await tx.wait(); + + const holderAddress = await titleEscrow1.holder(); + expect(holderAddress).to.equal(newHolder.address); + }); + }); + + describe('Nominate', function () { + it('should successfully nominate a new beneficiary', async function () { + const tokenId = '0'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + newBeneficiaryAddress: newBeneficiary.address, + remarks: 'Nominate new beneficiary', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Current beneficiary is beneficiary1, nominate from them + const tx = await nominate(contractOptions, beneficiary1 as any, params, options); + await tx.wait(); + + // Verify nominee has been set + const nominee = await titleEscrow0.nominee(); + expect(nominee).to.equal(newBeneficiary.address); + }); + + it('should fail when non-beneficiary tries to nominate', async function () { + const tokenId = '1'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow1, + }; + + const params = { + newBeneficiaryAddress: newBeneficiary.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + // Try to nominate from non-beneficiary (should fail) + // Since we don't know the exact revert reason, catch any error + try { + await nominate(contractOptions, holder2 as any, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.satisfy((msg: string) => { + return ( + msg.includes('Pre-check (callStatic) for nominate failed') || + msg.includes('CallerNotBeneficiary') || + msg.includes('revert') || + msg.includes('reverted') + ); + }); + } + }); + + it('should handle nomination without remarks', async function () { + const tokenId = '1'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow1, + }; + + const params = { + newBeneficiaryAddress: holder1.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + const tx = await nominate(contractOptions, beneficiary2 as any, params, options); + await tx.wait(); + + const nominee = await titleEscrow1.nominee(); + expect(nominee).to.equal(holder1.address); + }); + }); + + describe('Transfer Beneficiary', function () { + beforeEach(async function () { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + it('should successfully transfer beneficiary role', async function () { + const tokenId = '0'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + newBeneficiaryAddress: newBeneficiary.address, + remarks: 'Transfer beneficiary to new address', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Check initial state + const initialBeneficiary = await titleEscrow0.beneficiary(); + expect(initialBeneficiary).to.equal(beneficiary1.address); + + // Transfer beneficiary using newHolder (current holder after transferHolder test) + const tx = await transferBeneficiary(contractOptions, newHolder as any, params, options); + + await tx.wait(); + + // Verify beneficiary has changed + const newBeneficiaryAddress = await titleEscrow0.beneficiary(); + expect(newBeneficiaryAddress).to.equal(newBeneficiary.address); + }); + + it('should fail when non-holder tries to transfer beneficiary', async function () { + const tokenId = '0'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + newBeneficiaryAddress: newBeneficiary.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + // Try to transfer from non-holder (should fail) + try { + await transferBeneficiary(contractOptions, beneficiary2 as any, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Pre-check (callStatic) for transferBeneficiary failed'); + } + }); + }); + + describe('transfer Owners', function () { + beforeEach(async function () { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + it('should successfully transfer both holder and beneficiary roles', async function () { + // Make current holder (newHolder) also the beneficiary for token 0 + const prevContractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow0, + }; + const prevParams = { + newBeneficiaryAddress: newHolder.address, + remarks: 'Nominate and endorse new beneficiary', + }; + const prevOptions = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + const nominateTxn = await nominate( + prevContractOptions, + newBeneficiary as any, + prevParams, + prevOptions, + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await nominateTxn.wait(); + const beneficiaryTransferTxn = await transferBeneficiary( + prevContractOptions, + newHolder as any, + prevParams, + prevOptions, + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await beneficiaryTransferTxn.wait(); + const tokenId = '0'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + newBeneficiaryAddress: beneficiary2.address, + newHolderAddress: holder2.address, + remarks: 'Transfer both roles', + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + id: 'test-encryption-id', + }; + + // Execute transfer using current holder (newHolder) + const tx = await transferOwners(contractOptions, newHolder as any, params, options); + await tx.wait(); + + // Verify both roles have changed + const holderAddress = await titleEscrow0.holder(); + const beneficiaryAddress = await titleEscrow0.beneficiary(); + + expect(holderAddress).to.equal(holder2.address); + expect(beneficiaryAddress).to.equal(beneficiary2.address); + }); + + it('should fail when caller is not both holder and beneficiary', async function () { + const tokenId = '1'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow1, + }; + + const params = { + newBeneficiaryAddress: newBeneficiary.address, + newHolderAddress: newHolder.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + // holder2 is holder but beneficiary2 is beneficiary (different addresses) + // Since we don't know the exact revert reason, catch any error + try { + await transferOwners(contractOptions, holder2 as any, params, options); + expect.fail('Expected transaction to revert but it succeeded'); + } catch (error: any) { + // The function should fail either at callStatic level or with a contract revert + // Both are acceptable for this test case + expect(error.message).to.satisfy((msg: string) => { + return ( + msg.includes('Pre-check (callStatic) for transferOwners failed') || + msg.includes('CallerNotBeneficiary') || + msg.includes('revert') || + msg.includes('reverted') + ); + }); + } + }); + }); + + describe('Error handling', function () { + it('should throw error when tokenRegistryAddress and titleEscrowAddress is missing', async function () { + const contractOptions = { + tokenRegistryAddress: '', + tokenId: '0', + // titleEscrowAddress is intentionally missing + }; + + const params = { + holderAddress: newHolder.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + try { + await transferHolder(contractOptions, holder2 as any, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Token registry address is required'); + } + }); + + it('should throw error when provider is missing', async function () { + const signerWithoutProvider = new ethers.Wallet( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId: '0', + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + holderAddress: newHolder.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + }; + + try { + await transferHolder(contractOptions, signerWithoutProvider as any, params, options); + expect.fail('Expected function to throw an error'); + } catch (error: any) { + expect(error.message).to.equal('Provider is required'); + } + }); + }); + + describe('Gas estimation and transaction options', function () { + it('should handle custom gas options', async function () { + const tokenId = '0'; + const contractOptions = { + tokenRegistryAddress: addresses.tokenAddress, + tokenId, + titleEscrowAddress: addresses.titleEscrow0, + }; + + const params = { + holderAddress: holder1.address, + }; + + const options = { + chainId: CHAIN_ID.local, + titleEscrowVersion, + maxFeePerGas: ethers.utils.parseUnits('20', 'gwei').toHexString(), + maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei').toHexString(), + }; + + // Transfer back to holder1 from newHolder + const tx = await transferHolder(contractOptions, holder2 as any, params, options); + await tx.wait(); + + const holderAddress = await titleEscrow0.holder(); + expect(holderAddress).to.equal(holder1.address); + }); + }); + }); +}); diff --git a/src/__tests__/e2e/utils.ts b/src/__tests__/e2e/utils.ts new file mode 100644 index 0000000..060d04c --- /dev/null +++ b/src/__tests__/e2e/utils.ts @@ -0,0 +1,38 @@ +import { ethers as ethersV6, ContractRunner } from 'ethersV6'; +import { v5Contracts } from '../../token-registry-v5'; +import { v4Contracts } from '../../token-registry-v4'; +import { ethers, Signer } from 'ethers'; + +export function getVersionedContractFactory( + contractName: 'TradeTrustToken' | 'TitleEscrowFactory', + ethersVersion: 'v5' | 'v6', + titleEscrowVersion: 'v4' | 'v5', + owner: Signer | ContractRunner, +) { + const contracts = titleEscrowVersion === 'v5' ? v5Contracts : v4Contracts; + const Factory = ethersVersion === 'v5' ? ethers.ContractFactory : ethersV6.ContractFactory; + const signer = ethersVersion === 'v5' ? (owner as Signer) : (owner as ContractRunner); + + return new Factory( + contracts[`${contractName}__factory`].abi, + contracts[`${contractName}__factory`].bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); +} + +export const createContract = ( + address: string, + contractName: 'TitleEscrow' | 'TradeTrustToken', + ethersVersion: 'v5' | 'v6', + contractVersion: 'v4' | 'v5', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer: any, +) => { + const contracts = contractVersion === 'v5' ? v5Contracts : v4Contracts; + const abi = contracts[`${contractName}__factory`].abi; + + return ethersVersion === 'v5' + ? new ethers.Contract(address, abi, signer as Signer) + : new ethersV6.Contract(address, abi, signer as ContractRunner); +}; diff --git a/src/__tests__/token-registry-functions/fixtures.ts b/src/__tests__/token-registry-functions/fixtures.ts new file mode 100644 index 0000000..d37bc3d --- /dev/null +++ b/src/__tests__/token-registry-functions/fixtures.ts @@ -0,0 +1,301 @@ +import { vi } from 'vitest'; +import { ethers as ethersV5 } from 'ethers'; +import { JsonRpcProvider as JsonRpcProviderV6 } from 'ethersV6'; +import * as originalModule from '../../utils/ethers'; + +export const MOCK_V5_ADDRESS = '0xV5TokenRegistryContract'; +export const MOCK_V4_ADDRESS = '0xV4TokenRegistryContract'; +export const MOCK_OWNER_ADDRESS = '0xowner'; + +vi.mock('../../utils/ethers', async (importOriginal) => { + const original = (await importOriginal()) as typeof originalModule; + + return { + ...original, // Keep all original exports + getEthersContractFromProvider: vi.fn(() => vi.fn()), // Only mock this function + }; +}); + +vi.mock('../../core', () => ({ + encrypt: vi.fn(() => 'encrypted_remarks'), + getTitleEscrowAddress: vi.fn(), + isTitleEscrowVersion: vi.fn(() => Promise.resolve(true)), + checkSupportsInterface: vi.fn(), + TitleEscrowInterface: { + V4: '0xTitleEscrowIdV4', + V5: '0xTitleEscrowIdV5', + }, +})); + +vi.mock('../../token-registry-v5', () => { + return { + v5Contracts: { + TitleEscrow__factory: { + connect: vi.fn(() => mockV5TitleEscrowContract), + abi: 'TitleEscrow', + }, + TradeTrustToken__factory: { + connect: vi.fn(() => mockV5TradeTrustTokenContract), + abi: 'TradeTrustToken', + }, + TitleEscrowFactory__factory: { + connect: vi.fn(() => mockV5TitleEscrowFactoryContract), + abi: 'TitleEscrowFactory', + }, + }, + v5SupportInterfaceIds: { + TitleEscrow: '0xTitleEscrowIdV5', + TradeTrustTokenMintable: '0xTradeTrustTokenMintableIdV5', + TradeTrustTokenRestorable: '0xTradeTrustTokenRestorableIdV5', + TradeTrustTokenBurnable: '0xTradeTrustTokenBurnableIdV5', + SBT: '0xSBTIdV5', + }, + }; +}); + +vi.mock('../../token-registry-v4', () => { + return { + v4Contracts: { + TitleEscrow__factory: { + connect: vi.fn(() => mockV4TitleEscrowContract), + abi: 'TitleEscrow', + }, + TradeTrustToken__factory: { + connect: vi.fn(() => mockV4TradeTrustTokenContract), + abi: 'TradeTrustToken', + }, + TitleEscrowFactory__factory: { + connect: vi.fn(() => mockV4TitleEscrowFactoryContract), + abi: 'TitleEscrowFactory', + }, + }, + v4SupportInterfaceIds: { + TitleEscrow: '0xTitleEscrowIdV4', + TradeTrustTokenMintable: '0xTradeTrustTokenMintableIdV4', + TradeTrustTokenRestorable: '0xTradeTrustTokenRestorableIdV4', + TradeTrustTokenBurnable: '0xTradeTrustTokenBurnableIdV4', + SBT: '0xSBTIdV4', + }, + }; +}); + +export const mockV5TitleEscrowFactoryContract = { + callStatic: { + getEscrowAddress: vi.fn(), + }, + getEscrowAddress: vi.fn(() => Promise.resolve('0xV5titleescrow')), +}; + +export const mockV5TradeTrustTokenContract = { + callStatic: { + burn: vi.fn(), + restore: vi.fn(), + mint: vi.fn(), + }, + supportsInterface: vi.fn(), + titleEscrowFactory: vi.fn(() => Promise.resolve('0xV5titleescrowfactory')), + burn: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_burn_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + restore: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_restore_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + mint: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_mint_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + ownerOf: vi.fn(() => Promise.resolve(MOCK_OWNER_ADDRESS)), +}; + +export const mockV5TitleEscrowContract = { + supportsInterface: vi.fn(), + callStatic: { + transferHolder: vi.fn(), + transferBeneficiary: vi.fn(), + transferOwners: vi.fn(), + nominate: vi.fn(), + rejectTransferHolder: vi.fn(), + rejectTransferBeneficiary: vi.fn(), + rejectTransferOwners: vi.fn(), + returnToIssuer: vi.fn(), + }, + transferHolder: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_transfer_holder_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + transferBeneficiary: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_transfer_beneficiary_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + transferOwners: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_transfer_owners_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + nominate: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_nominate_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + holder: vi.fn(() => Promise.resolve('0xcurrent_holder')), + beneficiary: vi.fn(() => Promise.resolve('0xcurrent_beneficiary')), + rejectTransferHolder: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_reject_transfer_holder_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + rejectTransferBeneficiary: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_reject_transfer_beneficiary_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + rejectTransferOwners: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_reject_transfer_owners_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + returnToIssuer: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v5_return_to_issuer_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), +}; + +export const mockV4TitleEscrowContract = { + supportsInterface: vi.fn(), + callStatic: { + transferHolder: vi.fn(), + transferBeneficiary: vi.fn(), + transferOwners: vi.fn(), + nominate: vi.fn(), + surrender: vi.fn(), + }, + transferHolder: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_transfer_holder_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + transferBeneficiary: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_transfer_beneficiary_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + transferOwners: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_transfer_owners_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + nominate: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_nominate_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + holder: vi.fn(() => Promise.resolve('0xcurrent_holder')), + beneficiary: vi.fn(() => Promise.resolve('0xcurrent_beneficiary')), + surrender: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_surrender_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), +}; +export const mockV4TitleEscrowFactoryContract = { + callStatic: { + getEscrowAddress: vi.fn(), + }, + getEscrowAddress: vi.fn(() => Promise.resolve('0xV4titleescrow')), +}; + +export const mockV4TradeTrustTokenContract = { + callStatic: { + burn: vi.fn(), + restore: vi.fn(), + mint: vi.fn(), + }, + titleEscrowFactory: vi.fn(() => Promise.resolve('0xV4titleescrowfactory')), + supportsInterface: vi.fn(), + burn: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_burn_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + restore: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_restore_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + mint: Object.assign( + // Direct call returns hash string + vi.fn(() => Promise.resolve('v4_mint_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + ownerOf: vi.fn(() => Promise.resolve(MOCK_OWNER_ADDRESS)), +}; + +export const PRIVATE_KEY = '0x59c6995e998f97a5a004497e5f1ebce0c16828d44b3f8d0bfa3a89d271d5b6b9'; // random local key + +export const providerV5 = new ethersV5.providers.JsonRpcProvider(); +export const providerV6 = new JsonRpcProviderV6(); diff --git a/src/__tests__/token-registry-functions/mint.test.ts b/src/__tests__/token-registry-functions/mint.test.ts new file mode 100644 index 0000000..ebac886 --- /dev/null +++ b/src/__tests__/token-registry-functions/mint.test.ts @@ -0,0 +1,221 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; +import * as coreModule from '../../core'; + +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { mint } from '../../token-registry-functions'; +import { + MOCK_V4_ADDRESS, + MOCK_V5_ADDRESS, + mockV4TradeTrustTokenContract, + mockV5TradeTrustTokenContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import { ProviderInfo } from '../../token-registry-functions/types'; +import { getEthersContractFromProvider } from '../../utils/ethers/index.js'; + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; +describe('Mint Token', () => { + const mockTokenId = '0xTokenId'; + const mockRemarks = 'Mint remarks'; + const mockChainId = CHAIN_ID.local; + describe.each(providers)( + 'Mint Token with TR version $titleEscrowVersion and ethers version $ethersVersion', + async ({ Provider, ethersVersion, titleEscrowVersion }) => { + const isV5TT = titleEscrowVersion === 'v5'; + // let mockContract = isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract; + const mockTxResponse = titleEscrowVersion === 'v5' ? 'v5_mint_tx_hash' : 'v4_mint_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + const mockTokenRegistryAddress = isV5TT ? MOCK_V5_ADDRESS : MOCK_V4_ADDRESS; + const mockBeneficiaryAddress = '0xBeneficiaryAddress'; + const mockHolderAddress = '0xHolderAddress'; + // const titleEscrowAddress = isV5TT ? '0xv5contract' : '0xv4contract'; + const mockTradeTrustTokenContract = isV5TT + ? mockV5TradeTrustTokenContract + : mockV4TradeTrustTokenContract; + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTradeTrustTokenContract), + ); + }); + beforeEach(() => { + vi.clearAllMocks(); + // vi.spyOn(coreModule, 'encrypt').mockReturnValue(mockEncryptedRemarks.slice(2)); + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return ( + interfaceId === + (isV5TT ? '0xTradeTrustTokenMintableIdV5' : '0xTradeTrustTokenMintableIdV4') + ); + }, + ); + mockTradeTrustTokenContract.callStatic.mint.mockResolvedValue(true); + mockTradeTrustTokenContract.mint.staticCall.mockResolvedValue(true); + }); + + it('should Mint token with remarks', async () => { + const result = await mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + remarks: mockRemarks, + }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual(mockTxResponse); + if (isV5TT) expect(coreModule.encrypt).toHaveBeenCalledWith(mockRemarks, 'encryption-id'); + }); + + it('should mint token without remarks', async () => { + const result = await mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + }, + { chainId: mockChainId, titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('callStatic error'); + mockTradeTrustTokenContract.callStatic.mint.mockRejectedValue(mockError); + mockTradeTrustTokenContract.mint.staticCall.mockRejectedValue(mockError); + await expect( + mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + remarks: mockRemarks, + }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for mint failed'); + if (isV5TT) { + mockV5TradeTrustTokenContract.callStatic.mint = vi.fn(); + } else { + mockV4TradeTrustTokenContract.callStatic.mint = vi.fn(); + } + }); + + it('should throw when token registry address is missing', async () => { + await expect( + mint( + { tokenRegistryAddress: '' }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + signerWithoutProvider, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + remarks: mockRemarks, + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when version is unsupported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + await expect( + mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + remarks: mockRemarks, + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only Token Registry V4/V5 is supported'); + }); + + it('should work with explicit V5/V4 version', async () => { + const result = await mint( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { + beneficiaryAddress: mockBeneficiaryAddress, + holderAddress: mockHolderAddress, + tokenId: mockTokenId, + remarks: mockRemarks, + }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + }, + ); +}); diff --git a/src/__tests__/token-registry-functions/ownerOf.test.ts b/src/__tests__/token-registry-functions/ownerOf.test.ts new file mode 100644 index 0000000..898b2ef --- /dev/null +++ b/src/__tests__/token-registry-functions/ownerOf.test.ts @@ -0,0 +1,146 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; +import * as coreModule from '../../core'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { ownerOf } from '../../token-registry-functions'; +import { v5Contracts } from '../../token-registry-v5'; +import { v4Contracts } from '../../token-registry-v4'; +import { + MOCK_OWNER_ADDRESS, + MOCK_V4_ADDRESS, + MOCK_V5_ADDRESS, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures'; +import { ProviderInfo } from '../../token-registry-functions/types'; + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; + +describe.each(providers)( + 'ownerOf function for ethers version $ethersVersion and TR version $titleEscrowVersion', + ({ Provider, ethersVersion, titleEscrowVersion }) => { + const mockTokenId = '0xTokenId'; + const mockChainId = CHAIN_ID.local; + const isV5TT = titleEscrowVersion === 'v5'; + // let mockContract = isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + const mockTokenRegistryAddress = isV5TT ? MOCK_V5_ADDRESS : MOCK_V4_ADDRESS; + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return interfaceId === (isV5TT ? '0xSBTIdV5' : '0xSBTIdV4'); + }, + ); + }); + + // afterEach(() => { + // vi.restoreAllMocks(); + // }); + + describe('Successful Calls', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('should return owner for V5/v4 contract (auto-detected)', async () => { + const result = await ownerOf( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + {}, + ); + + expect(result).toBe(MOCK_OWNER_ADDRESS); + expect( + (isV5TT ? v5Contracts : v4Contracts).TradeTrustToken__factory.connect, + ).toHaveBeenCalled(); + }); + + it('should return owner for V5/v4 contract (explicit version)', async () => { + const result = await ownerOf( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { titleEscrowVersion }, + ); + + expect(result).toBe(MOCK_OWNER_ADDRESS); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + }); + + describe('Error Handling', () => { + it('should throw when token registry address is missing', async () => { + await expect( + ownerOf( + { tokenRegistryAddress: '' }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + ownerOf( + { tokenRegistryAddress: mockTokenRegistryAddress }, + signerWithoutProvider, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when version is unsupported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + await expect( + ownerOf( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only Token Registry V4/V5 is supported'); + }); + }); + }, +); diff --git a/src/__tests__/token-registry-functions/rejectTransfers.test.ts b/src/__tests__/token-registry-functions/rejectTransfers.test.ts new file mode 100644 index 0000000..879da71 --- /dev/null +++ b/src/__tests__/token-registry-functions/rejectTransfers.test.ts @@ -0,0 +1,460 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { ethers as ethersV6, Network, Wallet as WalletV6 } from 'ethersV6'; +import * as coreModule from '../../core'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { + rejectTransferBeneficiary, + rejectTransferHolder, + rejectTransferOwners, +} from '../../token-registry-functions/rejectTransfers'; +import { mockV5TitleEscrowContract, PRIVATE_KEY, providerV5, providerV6 } from './fixtures'; +import { ProviderInfo } from '../../token-registry-functions/types.js'; +import { getEthersContractFromProvider } from '../../utils/ethers'; + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, +]; + +describe.each(providers)( + 'Reject Transfers', + async ({ Provider, ethersVersion, titleEscrowVersion }) => { + const mockTokenRegistryAddress = '0xTokenRegistry'; + const mockTokenId = '0xTokenId'; + const mockTitleEscrowAddress = '0xTitleEscrow'; + const mockRemarks = 'Rejection remarks'; + const mockChainId = CHAIN_ID.local; + const mockEncryptedRemarks = '0xencryptedRemarks'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + // Handle both v5 and v6 contract constructors + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockV5TitleEscrowContract), + ); + }); + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + // wallet = { + // ...wallet, + // address: '0xcurrent_holder', + // getChainId: vi.fn().mockResolvedValue(CHAIN_ID.mainnet as unknown as number), + // } as any; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(CHAIN_ID.mainnet as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: CHAIN_ID.mainnet, + } as unknown as Network); + // vi.spyOn(wallet, 'getAddress').mockResolvedValue('0xcurrent_holder'); + } + + // Mock core functions + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockResolvedValue(mockTitleEscrowAddress); + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockResolvedValue(true); + vi.spyOn(coreModule, 'encrypt').mockReturnValue(mockEncryptedRemarks.slice(2)); + + // Mock contract calls + }); + describe(`Reject Transfers Holder with ethers version ${ethersVersion}`, () => { + it('should reject transfer holder with signer and all required parameters', async () => { + const result = await rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_holder_tx_hash'); + }); + + it('should reject transfer holder when titleEscrowAddress is provided', async () => { + const result = await rejectTransferHolder( + { + titleEscrowAddress: mockTitleEscrowAddress, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_holder_tx_hash'); + expect(coreModule.getTitleEscrowAddress).not.toHaveBeenCalled(); + }); + + it('should reject transfer holder without remarks', async () => { + const result = await rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('v5_reject_transfer_holder_tx_hash'); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw error when tokenRegistryAddress is missing', async () => { + vi.mocked(coreModule.getTitleEscrowAddress).mockResolvedValue(undefined); + await expect( + rejectTransferHolder( + { + tokenId: mockTokenId, + } as any, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw error when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + signerWithoutProvider, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw error when title escrow is not V5', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockResolvedValue(false); + + await expect( + rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Only Token Registry V5 is supported'); + }); + + it('should throw error when callStatic fails', async () => { + mockV5TitleEscrowContract.callStatic.rejectTransferHolder.mockRejectedValue( + new Error('Simulated failure'), + ); + mockV5TitleEscrowContract.rejectTransferHolder.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + + await expect( + rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for rejectTransferHolder failed'); + mockV5TitleEscrowContract.callStatic.rejectTransferHolder = vi.fn(); + mockV5TitleEscrowContract.rejectTransferHolder.staticCall = vi.fn(); + }); + + it('should use explicit titleEscrowVersion when provided', async () => { + await rejectTransferHolder( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(coreModule.isTitleEscrowVersion).not.toHaveBeenCalled(); + }); + }); + + describe(`Reject Transfers Beneficiary with ethers version ${ethersVersion}`, () => { + it('should reject transfer beneficiary with signer and all required parameters', async () => { + const result = await rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_beneficiary_tx_hash'); + }); + + it('should reject transfer beneficiary when titleEscrowAddress is provided', async () => { + const result = await rejectTransferBeneficiary( + { + titleEscrowAddress: mockTitleEscrowAddress, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_beneficiary_tx_hash'); + expect(coreModule.getTitleEscrowAddress).not.toHaveBeenCalled(); + }); + + it('should reject transfer beneficiary without remarks', async () => { + const result = await rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('v5_reject_transfer_beneficiary_tx_hash'); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw error when tokenRegistryAddress is missing', async () => { + vi.mocked(coreModule.getTitleEscrowAddress).mockResolvedValue(undefined); + await expect( + rejectTransferBeneficiary( + { + tokenId: mockTokenId, + } as any, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw error when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + signerWithoutProvider, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw error when title escrow is not V5', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockResolvedValue(false); + + await expect( + rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Only Token Registry V5 is supported'); + }); + + it('should throw error when callStatic fails', async () => { + mockV5TitleEscrowContract.callStatic.rejectTransferBeneficiary.mockRejectedValue( + new Error('Simulated failure'), + ); + mockV5TitleEscrowContract.rejectTransferBeneficiary.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + await expect( + rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for rejectTransferBeneficiary failed'); + mockV5TitleEscrowContract.callStatic.rejectTransferBeneficiary = vi.fn(); + mockV5TitleEscrowContract.rejectTransferBeneficiary.staticCall = vi.fn(); + }); + + it('should use explicit titleEscrowVersion when provided', async () => { + await rejectTransferBeneficiary( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(coreModule.isTitleEscrowVersion).not.toHaveBeenCalled(); + }); + }); + + describe(`Reject Transfers Owners with ethers version ${ethersVersion}`, () => { + it('should reject transfer beneficiary with signer and all required parameters', async () => { + const result = await rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_owners_tx_hash'); + }); + + it('should reject transfer beneficiary when titleEscrowAddress is provided', async () => { + const result = await rejectTransferOwners( + { + titleEscrowAddress: mockTitleEscrowAddress, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('v5_reject_transfer_owners_tx_hash'); + expect(coreModule.getTitleEscrowAddress).not.toHaveBeenCalled(); + }); + + it('should reject transfer beneficiary without remarks', async () => { + const result = await rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('v5_reject_transfer_owners_tx_hash'); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw error when tokenRegistryAddress is missing', async () => { + vi.mocked(coreModule.getTitleEscrowAddress).mockResolvedValue(undefined); + await expect( + rejectTransferOwners( + { + tokenId: mockTokenId, + } as any, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw error when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + signerWithoutProvider, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw error when title escrow is not V5', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockResolvedValue(false); + + await expect( + rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Only Token Registry V5 is supported'); + }); + + it('should throw error when callStatic fails', async () => { + mockV5TitleEscrowContract.callStatic.rejectTransferOwners.mockRejectedValue( + new Error('Simulated failure'), + ); + mockV5TitleEscrowContract.rejectTransferOwners.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + + await expect( + rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for rejectTransferOwners failed'); + mockV5TitleEscrowContract.callStatic.rejectTransferOwners = vi.fn(); + mockV5TitleEscrowContract.rejectTransferOwners.staticCall = vi.fn(); + }); + + it('should use explicit titleEscrowVersion when provided', async () => { + await rejectTransferOwners( + { + tokenRegistryAddress: mockTokenRegistryAddress, + tokenId: mockTokenId, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(coreModule.isTitleEscrowVersion).not.toHaveBeenCalled(); + }); + }); + }, +); diff --git a/src/__tests__/token-registry-functions/returnToken.test.ts b/src/__tests__/token-registry-functions/returnToken.test.ts new file mode 100644 index 0000000..ec1cc5d --- /dev/null +++ b/src/__tests__/token-registry-functions/returnToken.test.ts @@ -0,0 +1,477 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; +import * as coreModule from '../../core'; + +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { acceptReturned, rejectReturned, returnToIssuer } from '../../token-registry-functions'; +import { + MOCK_V4_ADDRESS, + MOCK_V5_ADDRESS, + mockV4TitleEscrowContract, + mockV4TradeTrustTokenContract, + mockV5TitleEscrowContract, + mockV5TradeTrustTokenContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import { ProviderInfo } from '../../token-registry-functions/types.js'; +import { getEthersContractFromProvider } from '../../utils/ethers/index.js'; + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; +describe('Return Token', () => { + const mockTokenId = '0xTokenId'; + const mockRemarks = 'Return remarks'; + const mockChainId = CHAIN_ID.local; + const mockEncryptedRemarks = '0xencryptedRemarks'; + + describe.each(providers)( + 'Return Token with TR version $titleEscrowVersion and ethers version $ethersVersion', + async ({ Provider, ethersVersion, titleEscrowVersion }) => { + const mockTokenRegistryAddress = '0xTokenRegistry'; + const mockTxResponse = + titleEscrowVersion === 'v5' ? 'v5_return_to_issuer_tx_hash' : 'v4_surrender_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(CHAIN_ID.local as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: CHAIN_ID.local, + } as unknown as Network); + } + const isV5TT = titleEscrowVersion === 'v5'; + const titleEscrowAddress = isV5TT ? '0xv5contract' : '0xv4contract'; + // Handle both v5 and v6 contract constructors + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(isV5TT ? mockV5TitleEscrowContract : mockV4TitleEscrowContract), + ); + }); + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockResolvedValue(titleEscrowAddress); + vi.spyOn(coreModule, 'encrypt').mockReturnValue(mockEncryptedRemarks.slice(2)); + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => { + return versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'); + }, + ); + mockV5TitleEscrowContract.callStatic.returnToIssuer.mockResolvedValue(true); + mockV4TitleEscrowContract.callStatic.surrender.mockResolvedValue(true); + mockV5TitleEscrowContract.returnToIssuer.staticCall.mockResolvedValue(true); + mockV4TitleEscrowContract.surrender.staticCall.mockResolvedValue(true); + }); + + it('should return to issuer with signer and remarks', async () => { + const result = await returnToIssuer( + { + titleEscrowAddress, + }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.encrypt).toHaveBeenCalledWith(mockRemarks, 'encryption-id'); + }); + + it('should return to issuer without remarks', async () => { + const result = await returnToIssuer( + { titleEscrowAddress }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('Simulated failure'); + if (isV5TT) { + mockV5TitleEscrowContract.callStatic.returnToIssuer.mockRejectedValue(mockError); + mockV5TitleEscrowContract.returnToIssuer.staticCall.mockRejectedValue(mockError); + } else { + mockV4TitleEscrowContract.callStatic.surrender.mockRejectedValue(mockError); + mockV4TitleEscrowContract.surrender.staticCall.mockRejectedValue(mockError); + } + + await expect( + returnToIssuer( + { tokenRegistryAddress: mockTokenRegistryAddress, tokenId: mockTokenId }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for returnToIssuer failed'); + if (isV5TT) { + mockV5TitleEscrowContract.callStatic.returnToIssuer = vi.fn(); + } else { + mockV4TitleEscrowContract.callStatic.surrender = vi.fn(); + } + }); + it('should throw error when provider is missing', async () => { + const signerWithoutProvider = isV5TT + ? new WalletV5('0x'.padEnd(66, '1')) + : new WalletV6('0x'.padEnd(66, '1')); + + await expect( + returnToIssuer( + { tokenRegistryAddress: mockTokenRegistryAddress, tokenId: mockTokenId }, + signerWithoutProvider, + {}, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when version is unsupported', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockResolvedValue(false); + + await expect( + returnToIssuer( + { tokenRegistryAddress: mockTokenRegistryAddress, tokenId: mockTokenId }, + wallet, + {}, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only Token Registry V4/V5 is supported'); + }); + + it('should work with explicit version', async () => { + const result = await returnToIssuer( + { tokenRegistryAddress: mockTokenRegistryAddress, tokenId: mockTokenId }, + wallet, + { remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.isTitleEscrowVersion).not.toHaveBeenCalled(); + }); + }, + ); + + describe.each(providers)( + 'Reject Return Token with TR version $titleEscrowVersion and ethers version $ethersVersion', + async ({ Provider, ethersVersion, titleEscrowVersion }) => { + const isV5TT = titleEscrowVersion === 'v5'; + // let mockContract = isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract; + const mockTxResponse = + titleEscrowVersion === 'v5' ? 'v5_restore_tx_hash' : 'v4_restore_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(CHAIN_ID.local as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: CHAIN_ID.local, + } as unknown as Network); + } + const mockTokenRegistryAddress = isV5TT ? MOCK_V5_ADDRESS : MOCK_V4_ADDRESS; + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor( + isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract, + ), + ); + }); + beforeEach(() => { + vi.clearAllMocks(); + // vi.spyOn(coreModule, 'encrypt').mockReturnValue(mockEncryptedRemarks.slice(2)); + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return ( + interfaceId === + (isV5TT ? '0xTradeTrustTokenRestorableIdV5' : '0xTradeTrustTokenRestorableIdV4') + ); + }, + ); + mockV5TradeTrustTokenContract.callStatic.restore.mockResolvedValue(true); + mockV4TradeTrustTokenContract.callStatic.restore.mockResolvedValue(true); + }); + + it('should reject returned token with remarks', async () => { + const result = await rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual(mockTxResponse); + if (isV5TT) expect(coreModule.encrypt).toHaveBeenCalledWith(mockRemarks, 'encryption-id'); + }); + + it('should reject returned token without remarks', async () => { + const result = await rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId, titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('Simulated failure'); + if (isV5TT) { + mockV5TradeTrustTokenContract.callStatic.restore.mockRejectedValue(mockError); + mockV5TradeTrustTokenContract.restore.staticCall.mockRejectedValue(mockError); + } else { + mockV4TradeTrustTokenContract.callStatic.restore.mockRejectedValue(mockError); + mockV4TradeTrustTokenContract.restore.staticCall.mockRejectedValue(mockError); + } + await expect( + rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for rejectReturned failed'); + if (isV5TT) { + mockV5TradeTrustTokenContract.callStatic.restore = vi.fn(); + mockV5TradeTrustTokenContract.restore.staticCall = vi.fn(); + } else { + mockV4TradeTrustTokenContract.callStatic.restore = vi.fn(); + mockV4TradeTrustTokenContract.restore.staticCall = vi.fn(); + } + }); + + it('should throw when token registry address is missing', async () => { + await expect( + rejectReturned( + { tokenRegistryAddress: '' }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + signerWithoutProvider, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when version is unsupported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + await expect( + rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only Token Registry V4/V5 is supported'); + }); + + it('should work with explicit V5/V4 version', async () => { + const result = await rejectReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + }, + ); + + describe.each(providers)( + 'Accept Return Token with TR version $titleEscrowVersion and ethers version $ethersVersion', + async ({ Provider, ethersVersion, titleEscrowVersion }) => { + const isV5TT = titleEscrowVersion === 'v5'; + // let mockContract = isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract; + const mockTxResponse = titleEscrowVersion === 'v5' ? 'v5_burn_tx_hash' : 'v4_burn_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(CHAIN_ID.local as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: CHAIN_ID.local, + } as unknown as Network); + } + const mockTokenRegistryAddress = isV5TT ? MOCK_V5_ADDRESS : MOCK_V4_ADDRESS; + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor( + isV5TT ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract, + ), + ); + }); + beforeEach(() => { + vi.clearAllMocks(); + + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return ( + interfaceId === + (isV5TT ? '0xTradeTrustTokenBurnableIdV5' : '0xTradeTrustTokenBurnableIdV4') + ); + }, + ); + mockV5TradeTrustTokenContract.callStatic.burn.mockResolvedValue(true); + mockV4TradeTrustTokenContract.callStatic.burn.mockResolvedValue(true); + }); + + it('should accept returned token with remarks', async () => { + const result = await acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual(mockTxResponse); + if (isV5TT) expect(coreModule.encrypt).toHaveBeenCalledWith(mockRemarks, 'encryption-id'); + }); + + it('should accept returned token without remarks', async () => { + const result = await acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId, titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('Simulated failure'); + if (isV5TT) { + mockV5TradeTrustTokenContract.callStatic.burn.mockRejectedValue(mockError); + mockV5TradeTrustTokenContract.burn.staticCall.mockRejectedValue(mockError); + } else { + mockV4TradeTrustTokenContract.callStatic.burn.mockRejectedValue(mockError); + mockV4TradeTrustTokenContract.burn.staticCall.mockRejectedValue(mockError); + } + await expect( + acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id' }, + ), + ).rejects.toThrow('Pre-check (callStatic) for acceptReturned failed'); + if (isV5TT) { + mockV5TradeTrustTokenContract.callStatic.burn = vi.fn(); + mockV5TradeTrustTokenContract.burn.staticCall = vi.fn(); + } else { + mockV4TradeTrustTokenContract.callStatic.burn = vi.fn(); + mockV4TradeTrustTokenContract.burn.staticCall = vi.fn(); + } + }); + + it('should throw when token registry address is missing', async () => { + await expect( + acceptReturned( + { tokenRegistryAddress: '' }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + + await expect( + acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + signerWithoutProvider, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when version is unsupported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + await expect( + acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only Token Registry V4/V5 is supported'); + }); + + it('should work with explicit V5/V4 version', async () => { + const result = await acceptReturned( + { tokenRegistryAddress: mockTokenRegistryAddress }, + wallet, + { tokenId: mockTokenId, remarks: mockRemarks }, + { chainId: mockChainId, id: 'encryption-id', titleEscrowVersion }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + }, + ); +}); diff --git a/src/__tests__/token-registry-functions/transfers.test.ts b/src/__tests__/token-registry-functions/transfers.test.ts new file mode 100644 index 0000000..86d9faa --- /dev/null +++ b/src/__tests__/token-registry-functions/transfers.test.ts @@ -0,0 +1,804 @@ +import './fixtures.js'; +import { vi, describe, beforeAll, it, expect } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { ethers as ethersV6, Network, Wallet as WalletV6 } from 'ethersV6'; +import * as coreModule from '../../core'; +import { encrypt } from '../../core'; +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { + nominate, + transferBeneficiary, + transferHolder, + transferOwners, +} from '../../token-registry-functions'; + +import { + mockV4TitleEscrowContract, + mockV5TitleEscrowContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import { getEthersContractFromProvider } from '../../utils/ethers/index.js'; + +// Mock core module +vi.mock('../../core', () => ({ + __esModule: true, + ...vi.importActual('../../core'), + encrypt: vi.fn(() => 'encrypted_remarks'), +})); + +// Mock gas station +vi.mock('../../core/gas-station', () => ({ + getGasStation: vi.fn(), +})); + +// Mock gas station options +vi.mock('../../core/gas-station/mock', () => ({ + getGasOptions: vi.fn(), +})); + +const providers: any[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v5', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + titleEscrowVersion: 'v4', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + titleEscrowVersion: 'v4', + }, +]; +describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEscrowVersion }) => { + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(CHAIN_ID.mainnet as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: CHAIN_ID.mainnet, + } as unknown as Network); + } + const isV5TT = titleEscrowVersion === 'v5'; + const mockTitleEscrowContract = isV5TT ? mockV5TitleEscrowContract : mockV4TitleEscrowContract; + const titleEscrowAddress = isV5TT ? '0xv5contract' : '0xv4contract'; + + // Handle both v5 and v6 contract constructors + beforeAll(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTitleEscrowContract), + ); + }); + describe(`transfer holder with TR Version ${titleEscrowVersion} and ethers version ${ethersVersion}`, () => { + const params = isV5TT + ? { + holderAddress: '0xholder', + remarks: '0xencrypted_remarks', + tokenId: 1, + } + : { + holderAddress: '0xholder', + tokenId: 1, + }; + const txHash = isV5TT ? 'v5_transfer_holder_tx_hash' : 'v4_transfer_holder_tx_hash'; + + it('throws error if titleEscrowAddress is missing ', async () => { + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => Promise.resolve('')); + + await expect( + transferHolder( + { + titleEscrowAddress: '', + }, + wallet, + params, + { titleEscrowVersion }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('handles both v5 and v4 contracts when TR version is not provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferHolder.mockResolvedValue(true); + + const tx = await transferHolder( + { + titleEscrowAddress: titleEscrowAddress, + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT ? ['0xholder', '0xencrypted_remarks', {}] : ['0xholder', {}]; + + expect(mockTitleEscrowContract.transferHolder).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + + it(`detects version automatically via supportsInterface for ${titleEscrowAddress}`, async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferHolder.mockResolvedValue(true); + + const tx = await transferHolder( + { + titleEscrowAddress: '0xauto', + }, + wallet, + params, + {}, // no isV5TT provided + ); + + expect(coreModule.isTitleEscrowVersion).toHaveBeenCalledWith({ + provider: wallet.provider, + titleEscrowAddress: '0xauto', + versionInterface: isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4', + }); + expect(tx).toBe(txHash); + }); + + it('calls gas station when gas options are missing', async () => { + const gasStationMock = vi.fn().mockResolvedValue({ + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + const mockChainId = CHAIN_ID.mainnet; + + // to allow overriding + const { SUPPORTED_CHAINS: SUPPORTED_CHAINS } = await import( + '@tradetrust-tt/tradetrust-utils' + ); + const originalChainData = SUPPORTED_CHAINS[mockChainId].gasStation; + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: gasStationMock, + }; + + mockTitleEscrowContract.callStatic.transferHolder.mockResolvedValue(true); + + await transferHolder( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ); + const resultOptions = isV5TT ? ['0xholder', '0xencrypted_remarks'] : ['0xholder']; + + expect(gasStationMock).toHaveBeenCalled(); + expect(mockTitleEscrowContract.transferHolder).toHaveBeenCalledWith(...resultOptions, { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: originalChainData, + }; + }); + + it('throws error when callStatic fails', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferHolder.mockRejectedValue( + new Error('Simulated failure'), + ); + mockTitleEscrowContract.transferHolder.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + + await expect( + transferHolder( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ), + ).rejects.toThrow('Pre-check (callStatic) for transferHolder failed'); + mockTitleEscrowContract.transferHolder.staticCall.mockResolvedValue(true); + }); + + it('handles both v5 and v4 contracts when tokenId and tokenRegistry is provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => + Promise.resolve(titleEscrowAddress), + ); + mockTitleEscrowContract.callStatic.transferHolder.mockResolvedValue(true); + + const tx = await transferHolder( + { + tokenId: 1, + tokenRegistryAddress: '0xtokenregistry', + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? [ + '0xholder', + '0xencrypted_remarks', + { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }, + ] + : [ + '0xholder', + { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }, + ]; + + expect(mockTitleEscrowContract.transferHolder).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + }); + + describe(`transfer beneficiary with TR Version ${titleEscrowVersion} and ethers version ${ethersVersion}`, () => { + const params = isV5TT + ? { newBeneficiaryAddress: '0xbeneficiary', remarks: '0xencrypted_remarks', tokenId: 1 } + : { newBeneficiaryAddress: '0xbeneficiary', tokenId: 1 }; + + const txHash = isV5TT ? 'v5_transfer_beneficiary_tx_hash' : 'v4_transfer_beneficiary_tx_hash'; + + it('throws error if titleEscrowAddress is missing ', async () => { + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => Promise.resolve('')); + + await expect( + transferBeneficiary( + { + titleEscrowAddress: '', + }, + wallet, + params, + { titleEscrowVersion }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('handles both v5 and v4 contracts when TR version is not provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + + const tx = await transferBeneficiary( + { + titleEscrowAddress: titleEscrowAddress, + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xencrypted_remarks', {}] + : ['0xbeneficiary', {}]; + + expect(mockTitleEscrowContract.transferBeneficiary).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + + it(`detects version automatically via supportsInterface for ${titleEscrowVersion}`, async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + + const tx = await transferBeneficiary( + { + titleEscrowAddress: '0xauto', + }, + wallet, + params, + {}, // no isV5TT provided + ); + + expect(coreModule.isTitleEscrowVersion).toHaveBeenCalledWith({ + provider: wallet.provider, + titleEscrowAddress: '0xauto', + versionInterface: isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4', + }); + expect(tx).toBe(txHash); + }); + + it('calls gas station when gas options are missing', async () => { + const gasStationMock = vi.fn().mockResolvedValue({ + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + const mockChainId = CHAIN_ID.mainnet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + + // to allow overriding + const { SUPPORTED_CHAINS: SUPPORTED_CHAINS } = await import( + '@tradetrust-tt/tradetrust-utils' + ); + const originalChainData = SUPPORTED_CHAINS[mockChainId].gasStation; + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: gasStationMock, + }; + + mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + + await transferBeneficiary( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ); + const resultOptions = isV5TT ? ['0xbeneficiary', '0xencrypted_remarks'] : ['0xbeneficiary']; + + expect(gasStationMock).toHaveBeenCalled(); + expect(mockTitleEscrowContract.transferBeneficiary).toHaveBeenCalledWith(...resultOptions, { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: originalChainData, + }; + }); + + it('throws error when callStatic fails', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferBeneficiary.mockRejectedValue( + new Error('Simulated failure'), + ); + mockTitleEscrowContract.transferBeneficiary.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + + await expect( + transferBeneficiary( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ), + ).rejects.toThrow('Pre-check (callStatic) for transferBeneficiary failed'); + mockTitleEscrowContract.transferBeneficiary.staticCall.mockResolvedValue(true); + }); + + it('handles both v5 and v4 contracts when tokenId and tokenregistry is provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => + Promise.resolve(titleEscrowAddress), + ); + mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + + const tx = await transferBeneficiary( + { + tokenId: 1, + tokenRegistryAddress: '0xtokenregistry', + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xencrypted_remarks', {}] + : ['0xbeneficiary', {}]; + + expect(mockTitleEscrowContract.transferBeneficiary).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + }); + + describe(`transfer owners with TR Version ${titleEscrowVersion} and ethers version ${ethersVersion}`, () => { + const params = isV5TT + ? { + newBeneficiaryAddress: '0xbeneficiary', + newHolderAddress: '0xholder', + remarks: '0xencrypted_remarks', + } + : { newBeneficiaryAddress: '0xbeneficiary', newHolderAddress: '0xholder' }; + const txHash = isV5TT ? 'v5_transfer_owners_tx_hash' : 'v4_transfer_owners_tx_hash'; + + it('throws error if titleEscrowAddress is missing ', async () => { + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => Promise.resolve('')); + + await expect( + transferOwners( + { + titleEscrowAddress: '', + }, + wallet, + params, + { titleEscrowVersion }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('handles both v5 and v4 contracts when TR version is not provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => + Promise.resolve(titleEscrowAddress), + ); + mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + + const tx = await transferOwners( + { + titleEscrowAddress: titleEscrowAddress, + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xholder', '0xencrypted_remarks', {}] + : ['0xbeneficiary', '0xholder', {}]; + + expect(mockTitleEscrowContract.transferOwners).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + + it(`detects version automatically via supportsInterface for ${titleEscrowVersion}`, async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferOwners.mockResolvedValue(true); + + const tx = await transferOwners( + { + titleEscrowAddress: '0xauto', + }, + wallet, + params, + {}, // no isV5TT provided + ); + + expect(coreModule.isTitleEscrowVersion).toHaveBeenCalledWith({ + provider: wallet.provider, + titleEscrowAddress: '0xauto', + versionInterface: isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4', + }); + expect(tx).toBe(txHash); + }); + + it('calls gas station when gas options are missing', async () => { + const gasStationMock = vi.fn().mockResolvedValue({ + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + const mockChainId = CHAIN_ID.mainnet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + + // to allow overriding + const { SUPPORTED_CHAINS: SUPPORTED_CHAINS } = await import( + '@tradetrust-tt/tradetrust-utils' + ); + const originalChainData = SUPPORTED_CHAINS[mockChainId].gasStation; + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: gasStationMock, + }; + + mockTitleEscrowContract.callStatic.transferOwners.mockResolvedValue(true); + + await transferOwners( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ); + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xholder', '0xencrypted_remarks'] + : ['0xbeneficiary', '0xholder']; + + expect(gasStationMock).toHaveBeenCalled(); + expect(mockTitleEscrowContract.transferOwners).toHaveBeenCalledWith(...resultOptions, { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: originalChainData, + }; + }); + + it('throws error when callStatic fails', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.transferOwners.mockRejectedValue( + new Error('Simulated failure'), + ); + mockTitleEscrowContract.transferOwners.staticCall.mockRejectedValue( + new Error('Simulated failure'), + ); + + await expect( + transferOwners( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ), + ).rejects.toThrow('Pre-check (callStatic) for transferOwners failed'); + mockTitleEscrowContract.transferOwners.staticCall.mockResolvedValue(true); + }); + + it('handles both v5 and v4 contracts when tokenId and tokenregistry is provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => + Promise.resolve(titleEscrowAddress), + ); + mockTitleEscrowContract.callStatic.transferOwners.mockResolvedValue(true); + + const tx = await transferOwners( + { + tokenId: 1, + tokenRegistryAddress: '0xtokenregistry', + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xholder', '0xencrypted_remarks', {}] + : ['0xbeneficiary', '0xholder', {}]; + + expect(mockTitleEscrowContract.transferOwners).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + }); + + describe(`nominate with TR Version ${titleEscrowVersion} and ethers version ${ethersVersion}`, () => { + const params = isV5TT + ? { newBeneficiaryAddress: '0xbeneficiary', remarks: '0xencrypted_remarks', tokenId: 1 } + : { newBeneficiaryAddress: '0xbeneficiary', tokenId: 1 }; + + const txHash = isV5TT ? 'v5_nominate_tx_hash' : 'v4_nominate_tx_hash'; + + it('throws error if titleEscrowAddress is missing ', async () => { + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => Promise.resolve('')); + + await expect( + nominate( + { + titleEscrowAddress: '', + }, + wallet, + params, + { titleEscrowVersion }, + ), + ).rejects.toThrow('Token registry address is required'); + }); + + it('handles both v5 and v4 contracts when TR version is not provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.nominate.mockResolvedValue(true); + + const tx = await nominate( + { + titleEscrowAddress: titleEscrowAddress, + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xencrypted_remarks', {}] + : ['0xbeneficiary', {}]; + + expect(mockTitleEscrowContract.nominate).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + + it(`detects version automatically via supportsInterface for ${titleEscrowVersion}`, async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.nominate.mockResolvedValue(true); + + const tx = await nominate( + { + titleEscrowAddress: '0xauto', + }, + wallet, + params, + {}, // no isV5TT provided + ); + + expect(coreModule.isTitleEscrowVersion).toHaveBeenCalledWith({ + provider: wallet.provider, + titleEscrowAddress: '0xauto', + versionInterface: isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4', + }); + expect(tx).toBe(txHash); + }); + + it('calls gas station when gas options are missing', async () => { + const gasStationMock = vi.fn().mockResolvedValue({ + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + const mockChainId = CHAIN_ID.mainnet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + + // to allow overriding + const { SUPPORTED_CHAINS: SUPPORTED_CHAINS } = await import( + '@tradetrust-tt/tradetrust-utils' + ); + const originalChainData = SUPPORTED_CHAINS[mockChainId].gasStation; + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: gasStationMock, + }; + + mockTitleEscrowContract.callStatic.nominate.mockResolvedValue(true); + + await nominate( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ); + const resultOptions = isV5TT ? ['0xbeneficiary', '0xencrypted_remarks'] : ['0xbeneficiary']; + + expect(gasStationMock).toHaveBeenCalled(); + expect(mockTitleEscrowContract.nominate).toHaveBeenCalledWith(...resultOptions, { + maxFeePerGas: 100, + maxPriorityFeePerGas: 50, + }); + + SUPPORTED_CHAINS[mockChainId] = { + ...SUPPORTED_CHAINS[mockChainId], + gasStation: originalChainData, + }; + }); + + it('throws error when callStatic fails', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + mockTitleEscrowContract.callStatic.nominate.mockRejectedValue(new Error('Simulated failure')); + mockTitleEscrowContract.nominate.staticCall.mockRejectedValue(new Error('Simulated failure')); + + await expect( + nominate( + { + titleEscrowAddress: '0xv5contract', + }, + wallet, + params, + { id: 'doc-id', titleEscrowVersion }, + ), + ).rejects.toThrow('Pre-check (callStatic) for nominate failed'); + mockTitleEscrowContract.nominate.staticCall.mockResolvedValue(true); + }); + + it('handles both v5 and v4 contracts when tokenId and tokenregistry is provided', async () => { + vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( + async ({ versionInterface }) => + versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), + ); + vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => + Promise.resolve(titleEscrowAddress), + ); + mockTitleEscrowContract.callStatic.nominate.mockResolvedValue(true); + + const tx = await nominate( + { + tokenId: 1, + tokenRegistryAddress: '0xtokenregistry', + }, + wallet, + params, + { id: 'doc-id', chainId: CHAIN_ID.mainnet }, + ); + if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); + + const resultOptions = isV5TT + ? ['0xbeneficiary', '0xencrypted_remarks', {}] + : ['0xbeneficiary', {}]; + + expect(mockTitleEscrowContract.nominate).toHaveBeenCalledWith(...resultOptions); + expect(tx).toBe(txHash); + }); + }); +}); diff --git a/src/core/endorsement-chain/useEndorsementChain.ts b/src/core/endorsement-chain/useEndorsementChain.ts index d907835..5d8695d 100644 --- a/src/core/endorsement-chain/useEndorsementChain.ts +++ b/src/core/endorsement-chain/useEndorsementChain.ts @@ -145,19 +145,17 @@ export const getDocumentOwner = async ( }; // Check Title Escrow Interface Support -const checkSupportsInterface = async ( - titleEscrowAddress: string, +export const checkSupportsInterface = async ( + contractAddress: string, interfaceId: string, provider: Provider | ethersV6.Provider, ): Promise => { try { const Contract = getEthersContractFromProvider(provider); - const titleEscrowAbi = [ - 'function supportsInterface(bytes4 interfaceId) external view returns (bool)', - ]; + const abi = ['function supportsInterface(bytes4 interfaceId) external view returns (bool)']; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const titleEscrowContract = new Contract(titleEscrowAddress, titleEscrowAbi, provider as any); - return await titleEscrowContract.supportsInterface(interfaceId); + const contract = new Contract(contractAddress, abi, provider as any); + return await contract.supportsInterface(interfaceId); } catch { return false; } diff --git a/src/index.ts b/src/index.ts index 0cec9fa..4728185 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import { v5ComputeInterfaceId, } from './token-registry-v5'; export type { TypedContractMethod } from './token-registry-v5/typedContractMethod'; - +export * from './token-registry-functions'; export * from './core'; export * from './open-attestation'; export * from './verify'; diff --git a/src/token-registry-functions/index.ts b/src/token-registry-functions/index.ts new file mode 100644 index 0000000..607e167 --- /dev/null +++ b/src/token-registry-functions/index.ts @@ -0,0 +1,5 @@ +export * from './transfer'; +export * from './rejectTransfers'; +export * from './returnToken'; +export * from './mint'; +export * from './ownerOf'; diff --git a/src/token-registry-functions/mint.ts b/src/token-registry-functions/mint.ts new file mode 100644 index 0000000..a111e10 --- /dev/null +++ b/src/token-registry-functions/mint.ts @@ -0,0 +1,121 @@ +import { checkSupportsInterface, encrypt } from '../core'; +import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; +import { v4Contracts, v4SupportInterfaceIds } from '../token-registry-v4'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { getTxOptions } from './utils'; +import { MintTokenOptions, MintTokenParams, TransactionOptions } from './types'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; + +/** + * Mints a new token into the TradeTrustToken registry with the specified beneficiary and holder. + * Supports both Token Registry V4 and V5 contracts. + * @param {MintTokenOptions} contractOptions - Contains the `tokenRegistryAddress` for the minting contract. + * @param {Signer | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the mint transaction. + * @param {MintTokenParams} params - Parameters for minting, including `beneficiaryAddress`, `holderAddress`, `tokenId`, and optional `remarks`. + * @param {TransactionOptions} options - Transaction metadata including gas values, version detection, chain ID, and optional encryption ID. + * @returns {Promise} A promise resolving to the transaction result from the mint call. + * @throws {Error} If the token registry address or signer provider is not provided. + * @throws {Error} If neither V4 nor V5 interfaces are supported. + * @throws {Error} If the `callStatic.mint` fails as a pre-check. + */ + +const mint = async ( + contractOptions: MintTokenOptions, + signer: Signer | SignerV6, + params: MintTokenParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { beneficiaryAddress, holderAddress, tokenId, remarks } = params; + + // Detect version if not explicitly provided checkSupportsInterface + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + checkSupportsInterface( + tokenRegistryAddress, + v4SupportInterfaceIds.TradeTrustTokenMintable, + signer.provider, + ), + checkSupportsInterface( + tokenRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenMintable, + signer.provider, + ), + ]); + } + + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let tradeTrustTokenContract: ContractV5 | ContractV6; + if (isV5TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v5Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v4Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + const encryptedRemarks = remarks && isV5TT ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT + ? [beneficiaryAddress, holderAddress, tokenId, encryptedRemarks] + : [beneficiaryAddress, holderAddress, tokenId]; + + if (isV6) { + await (tradeTrustTokenContract as ContractV6).mint.staticCall(...args); + } else { + await (tradeTrustTokenContract as ContractV5).callStatic.mint(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for mint failed'); + } + + const txOptions: TransactionOptions = await getTxOptions( + signer, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + ); + + // Send the actual transaction + + if (isV5TT) { + return await tradeTrustTokenContract.mint( + beneficiaryAddress, + holderAddress, + tokenId, + encryptedRemarks, + txOptions, + ); + } else if (isV4TT) { + return await tradeTrustTokenContract.mint( + beneficiaryAddress, + holderAddress, + tokenId, + txOptions, + ); + } +}; +export { mint }; diff --git a/src/token-registry-functions/ownerOf.ts b/src/token-registry-functions/ownerOf.ts new file mode 100644 index 0000000..9cd2c32 --- /dev/null +++ b/src/token-registry-functions/ownerOf.ts @@ -0,0 +1,68 @@ +import { checkSupportsInterface } from '../core'; +import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; +import { v4Contracts, v4SupportInterfaceIds } from '../token-registry-v4'; +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { OwnerOfTokenOptions, OwnerOfTokenParams, TransactionOptions } from './types'; + +/** + * Retrieves the owner of a given token from the TradeTrustToken contract. + * Supports both Token Registry V4 and V5 implementations. + * @param {OwnerOfTokenOptions} contractOptions - Options containing the token registry address. + * @param {Signer | SignerV6} signer - Signer instance (v5 or v6) used to query the blockchain. + * @param {OwnerOfTokenParams} params - Contains the `tokenId` of the token to query ownership for. + * @param {TransactionOptions} options - Includes the `titleEscrowVersion` and other optional metadata for interface detection. + * @returns {Promise} A promise that resolves to the owner address of the specified token. + * @throws {Error} If token registry address or signer provider is not provided. + * @throws {Error} If the token registry does not support V4 or V5 interfaces. + */ +const ownerOf = async ( + contractOptions: OwnerOfTokenOptions, + signer: Signer | SignerV6, + params: OwnerOfTokenParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress } = contractOptions; + const { titleEscrowVersion } = options; + const { tokenId } = params; + + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + // Detect version if not explicitly provided checkSupportsInterface + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + checkSupportsInterface(tokenRegistryAddress, v4SupportInterfaceIds.SBT, signer.provider), + checkSupportsInterface(tokenRegistryAddress, v5SupportInterfaceIds.SBT, signer.provider), + ]); + } + + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + // Connect V5 contract by default + let tradeTrustTokenContract: v5Contracts.TradeTrustToken | v4Contracts.TradeTrustToken; + if (isV5TT) { + tradeTrustTokenContract = v5Contracts.TradeTrustToken__factory.connect( + tokenRegistryAddress, + signer, + ); + } else if (isV4TT) { + tradeTrustTokenContract = v4Contracts.TradeTrustToken__factory.connect( + tokenRegistryAddress, + signer as Signer, + ); + } + + // Send the actual transaction + + if (isV5TT) { + return await (tradeTrustTokenContract as v5Contracts.TradeTrustToken).ownerOf(tokenId); + } else if (isV4TT) { + return await (tradeTrustTokenContract as v4Contracts.TradeTrustToken).ownerOf(tokenId); + } +}; +export { ownerOf }; diff --git a/src/token-registry-functions/rejectTransfers.ts b/src/token-registry-functions/rejectTransfers.ts new file mode 100644 index 0000000..268dc0d --- /dev/null +++ b/src/token-registry-functions/rejectTransfers.ts @@ -0,0 +1,262 @@ +import { + encrypt, + getTitleEscrowAddress, + isTitleEscrowVersion, + TitleEscrowInterface, +} from './../core'; +import { v5Contracts } from './../token-registry-v5'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { getTxOptions } from './utils'; +import { ContractOptions, RejectTransferParams, TransactionOptions } from './types'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; + +/** + * Rejects the transfer of holder for a title escrow contract. + * @param {ContractOptions} contractOptions - Contract-related options including the token registry address, and optionally, token ID and the title escrow address. + * @param {Signer | SignerV6} signer - Ethers signer (V5 or V6) used to sign and send the transaction. + * @param {RejectTransferParams} params - Contains the `remarks` field which is an optional string that will be encrypted and sent with the transaction. + * @param {TransactionOptions} options - Transfer options including optional `chainId`, `titleEscrowVersion`, `maxFeePerGas`, `maxPriorityFeePerGas`, and an `id` used for encryption. + * @throws error if the title escrow address or signer provider is missing. + * @throws if the version is not V5 compatible. + * @throws if the dry-run (`callStatic`) fails. + * @returns {Promise} The transaction response of the rejectTransferHolder call. + */ +const rejectTransferHolder = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: RejectTransferParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress, tokenId } = contractOptions; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!titleEscrowAddress) { + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!tokenId) throw new Error('Token ID is required'); + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + {}, + ); + } + + if (!titleEscrowAddress) throw new Error('Title escrow address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { remarks } = params; + + // Connect V5 contract by default + const Contract = getEthersContractFromProvider(signer.provider); + const titleEscrowContract: ContractV5 | ContractV6 = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + let isV5TT = titleEscrowVersion === 'v5'; + if (titleEscrowVersion === undefined) { + isV5TT = await isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }); + } + + if (!isV5TT) { + throw new Error('Only Token Registry V5 is supported'); + } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [encryptedRemarks] : []; + + if (isV6) { + await (titleEscrowContract as ContractV6).rejectTransferHolder.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.rejectTransferHolder(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for rejectTransferHolder failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + return await titleEscrowContract.rejectTransferHolder(encryptedRemarks, txOptions); +}; + +/** + * Rejects the transfer of beneficiary for a title escrow contract. + * @param {ContractOptions} contractOptions - Contract-related options including the token registry address, and optionally, token ID and the title escrow address. + * @param {Signer | SignerV6} signer - Ethers signer (V5 or V6) used to sign and send the transaction. + * @param {RejectTransferParams} params - Contains the `remarks` field which is an optional string that will be encrypted and sent with the transaction. + * @param {TransactionOptions} options - Transfer options including optional `chainId`, `titleEscrowVersion`, `maxFeePerGas`, `maxPriorityFeePerGas`, and an `id` used for encryption. + * @throws error if the title escrow address or signer provider is missing. + * @throws error if the version is not V5 compatible. + * @throws error if the dry-run (`callStatic`) fails. + * @returns {Promise} The transaction response of the rejectTransferBeneficiary call. + */ +const rejectTransferBeneficiary = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: RejectTransferParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress, tokenId } = contractOptions; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!titleEscrowAddress) { + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!tokenId) throw new Error('Token ID is required'); + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + {}, + ); + } + + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { remarks } = params; + + // Connect V5 contract by default + const Contract = getEthersContractFromProvider(signer.provider); + const titleEscrowContract: ContractV5 | ContractV6 = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + let isV5TT = titleEscrowVersion === 'v5'; + if (titleEscrowVersion === undefined) { + isV5TT = await isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }); + } + + if (!isV5TT) { + throw new Error('Only Token Registry V5 is supported'); + } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [encryptedRemarks] : []; + + if (isV6) { + await (titleEscrowContract as ContractV6).rejectTransferBeneficiary.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.rejectTransferBeneficiary(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for rejectTransferBeneficiary failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + return await titleEscrowContract.rejectTransferBeneficiary(encryptedRemarks, txOptions); +}; + +/** + * Rejects the transfer of ownership for a title escrow contract. + * @param {ContractOptions} contractOptions - Contract-related options including the token registry address, and optionally, token ID and the title escrow address. + * @param {Signer | SignerV6} signer - Ethers signer (V5 or V6) used to sign and send the transaction. + * @param {RejectTransferParams} params - Contains the `remarks` field which is an optional string that will be encrypted and sent with the transaction. + * @param {TransactionOptions} options - Transfer options including optional `chainId`, `titleEscrowVersion`, `maxFeePerGas`, `maxPriorityFeePerGas`, and an `id` used for encryption. + * @throws error if the title escrow address or signer provider is missing. + * @throws an error if the version is not V5 compatible. + * @throws an error if the dry-run (`callStatic`) fails. + * @returns {Promise} The transaction response of the rejectTransferOwners call. + */ +const rejectTransferOwners = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: RejectTransferParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress, tokenId } = contractOptions; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!titleEscrowAddress) { + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!tokenId) throw new Error('Token ID is required'); + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + {}, + ); + } + + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { remarks } = params; + + // Connect V5 contract by default + const Contract = getEthersContractFromProvider(signer.provider); + const titleEscrowContract: ContractV5 | ContractV6 = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + let isV5TT = titleEscrowVersion === 'v5'; + if (titleEscrowVersion === undefined) { + isV5TT = await isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }); + } + + if (!isV5TT) { + throw new Error('Only Token Registry V5 is supported'); + } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [encryptedRemarks] : []; + + if (isV6) { + await (titleEscrowContract as ContractV6).rejectTransferOwners.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.rejectTransferOwners(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for rejectTransferOwners failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + return await titleEscrowContract.rejectTransferOwners(encryptedRemarks, txOptions); +}; + +export { rejectTransferHolder, rejectTransferBeneficiary, rejectTransferOwners }; diff --git a/src/token-registry-functions/returnToken.ts b/src/token-registry-functions/returnToken.ts new file mode 100644 index 0000000..bd6a311 --- /dev/null +++ b/src/token-registry-functions/returnToken.ts @@ -0,0 +1,317 @@ +import { + checkSupportsInterface, + encrypt, + getTitleEscrowAddress, + isTitleEscrowVersion, + TitleEscrowInterface, +} from '../core'; +import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; +import { v4Contracts, v4SupportInterfaceIds } from '../token-registry-v4'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { getTxOptions } from './utils'; +import { + AcceptReturnedOptions, + AcceptReturnedParams, + ContractOptions, + RejectReturnedOptions, + RejectReturnedParams, + ReturnToIssuerParams, + TransactionOptions, +} from './types'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; + +/** + * Returns the token to the original issuer from the Title Escrow contract. + * @param {ContractOptions} contractOptions - Options including token ID, registry address, and optionally title escrow address. + * @param {Signer | SignerV6} signer - Signer instance (Ethers v5 or v6) that will execute the transaction. + * @param {ReturnToIssuerParams} params - Contains optional remarks to be encrypted and attached to the transaction. + * @param {TransactionOptions} options - Transaction settings including gas fees, escrow version, chain ID, and optional encryption ID. + * @returns {Promise} Promise that resolves to the transaction response from the `returnToIssuer` function. + * @throws {Error} If title escrow address or provider is not provided or if version is unsupported. + * @throws {Error} If the `callStatic.returnToIssuer` fails as a pre-check. + */ +const returnToIssuer = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: ReturnToIssuerParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress, tokenId } = contractOptions; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!titleEscrowAddress) { + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + {}, + ); + } + + if (!titleEscrowAddress) throw new Error('Title Escrow address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { remarks } = params; + + // Connect V5 contract by default + // let titleEscrowContract: v5Contracts.TitleEscrow | v4Contracts.TitleEscrow = + // v5Contracts.TitleEscrow__factory.connect(titleEscrowAddress, signer); + + const encryptedRemarks = remarks && options.id ? `0x${encrypt(remarks, options.id)}` : '0x'; + + // Detect version if not explicitly provided + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + await isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V4, + provider: signer.provider, + }), + await isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }), + ]); + } + + if (!isV5TT && !isV4TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let titleEscrowContract: ContractV5 | ContractV6; + if (isV5TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v4Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [encryptedRemarks] : []; + const staticCallFxn = isV5TT ? 'returnToIssuer' : 'surrender'; + + if (isV6) { + await (titleEscrowContract as ContractV6)[staticCallFxn].staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic[staticCallFxn](...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for returnToIssuer failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + if (isV5TT) { + return await titleEscrowContract.returnToIssuer(encryptedRemarks, txOptions); + } else if (isV4TT) { + return await titleEscrowContract.surrender(txOptions); + } +}; + +/** + * Rejects a previously returned token by restoring it back to the token registry. + * This is only supported on Token Registry V5 contracts with the `restore` functionality. + * @param {AcceptReturnedOptions} contractOptions - Contains the `tokenRegistryAddress` used to locate the TradeTrustToken contract. + * @param {Signer | SignerV6} signer - Signer instance (v5 or v6) used to authorize the transaction. + * @param {AcceptReturnedParams} params - Includes the `tokenId` to restore and optional `remarks` to encrypt. + * @param {TransactionOptions} options - Configuration for the transaction including version, gas fees, and optional `id` used for encryption. + * @returns {Promise} A promise that resolves to the transaction result of the `restore` call. + * @throws {Error} If the token registry address or provider is missing. + * @throws {Error} If the token registry version is unsupported. + * @throws {Error} If the callStatic pre-check fails. + */ +const rejectReturned = async ( + contractOptions: AcceptReturnedOptions, + signer: Signer | SignerV6, + params: RejectReturnedParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { tokenId, remarks } = params; + + // Detect version if not explicitly provided checkSupportsInterface + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + checkSupportsInterface( + tokenRegistryAddress, + v4SupportInterfaceIds.TradeTrustTokenRestorable, + signer.provider, + ), + checkSupportsInterface( + tokenRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenRestorable, + signer.provider, + ), + ]); + } + + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let tradeTrustTokenContract: ContractV5 | ContractV6; + if (isV5TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v5Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v4Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + const encryptedRemarks = remarks && isV5TT ? `0x${encrypt(remarks, options.id!)}` : '0x'; + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [tokenId, encryptedRemarks] : [tokenId]; + + if (isV6) { + await (tradeTrustTokenContract as ContractV6).restore.staticCall(...args); + } else { + await (tradeTrustTokenContract as ContractV5).callStatic.restore(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for rejectReturned failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + if (isV5TT) { + return await tradeTrustTokenContract.restore(tokenId, encryptedRemarks, txOptions); + } else if (isV4TT) { + return await tradeTrustTokenContract.restore(tokenId, txOptions); + } +}; +/** + * Accepts the returned token by burning it from the TradeTrustToken contract. + * Only supported on Token Registry V5 contracts that implement the burnable interface. + * @param {RejectReturnedOptions} contractOptions - Contains the `tokenRegistryAddress` from which the token will be burned. + * @param {Signer | SignerV6} signer - Signer instance (v5 or v6) used to authorize and send the burn transaction. + * @param {AcceptReturnedParams} params - Includes the `tokenId` to burn and optional `remarks` for audit trail. + * @param {TransactionOptions} options - Transaction settings including chain ID, gas fee values, escrow version, and encryption ID for remarks. + * @returns {Promise} A promise resolving to the transaction result of the burn call. + * @throws {Error} If token registry address or signer provider is not provided. + * @throws {Error} If the contract does not support Token Registry V5. + * @throws {Error} If `callStatic.burn` fails as a pre-check. + */ +const acceptReturned = async ( + contractOptions: RejectReturnedOptions, + signer: Signer | SignerV6, + params: AcceptReturnedParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas, titleEscrowVersion } = options; + + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { tokenId, remarks } = params; + + // Detect version if not explicitly provided checkSupportsInterface + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + checkSupportsInterface( + tokenRegistryAddress, + v4SupportInterfaceIds.TradeTrustTokenBurnable, + signer.provider, + ), + checkSupportsInterface( + tokenRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenBurnable, + signer.provider, + ), + ]); + } + + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let tradeTrustTokenContract: ContractV5 | ContractV6; + if (isV5TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v5Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + tradeTrustTokenContract = new Contract( + tokenRegistryAddress, + v4Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + const encryptedRemarks = remarks && isV5TT ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [encryptedRemarks] : []; + + if (isV6) { + await (tradeTrustTokenContract as ContractV6).burn.staticCall(...args); + } else { + await (tradeTrustTokenContract as ContractV5).callStatic.burn(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for acceptReturned failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + if (isV5TT) { + return await tradeTrustTokenContract.burn(tokenId, encryptedRemarks, txOptions); + } else if (isV4TT) { + return await tradeTrustTokenContract.burn(tokenId, txOptions); + } +}; + +export { returnToIssuer, acceptReturned, rejectReturned }; diff --git a/src/token-registry-functions/transfer.ts b/src/token-registry-functions/transfer.ts new file mode 100644 index 0000000..c0dc9a9 --- /dev/null +++ b/src/token-registry-functions/transfer.ts @@ -0,0 +1,529 @@ +import { + encrypt, + getTitleEscrowAddress, + isTitleEscrowVersion, + TitleEscrowInterface, +} from '../core'; +import { v4Contracts } from '../token-registry-v4'; +import { v5Contracts } from '../token-registry-v5'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { + ContractOptions, + NominateParams, + TransactionOptions, + TransferBeneficiaryParams, + TransferHolderParams, + TransferOwnersParams, +} from './types'; +import { getTxOptions } from './utils'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; + +/** + * Transfers holder role of a Title Escrow contract to a new address. + * The caller of this function must be the current holder. + * @param {ContractOptions} contractOptions - Contains `tokenRegistryAddress` and optionally `tokenId` and `titleEscrowAddress`. + * @param {Signer | SignerV6} signer - The signer (ethers v5 or v6) who initiates the transaction. + * @param {TransferHolderParams} params - Object containing `holderAddress` (address to transfer to) and optional `remarks`. + * @param {TransactionOptions} options - Transaction options including: + * - `titleEscrowVersion` (optional): Either "v4" or "v5" + * - `chainId` (optional): Used for gas station lookup + * - `maxFeePerGas` (optional), `maxPriorityFeePerGas` (optional): EIP-1559 gas fee configuration + * - `id` (optional): ID used for encrypting remarks + * @throws If required fields like `titleEscrowAddress` or `signer.provider` are missing. + * @throws If the version is unsupported (neither v4 nor v5). + * @throws If the dry-run via `callStatic` fails. + * @returns {Promise} The transaction response for `transferHolder`. + */ +const transferHolder = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: TransferHolderParams, + options: TransactionOptions, +): Promise => { + const { tokenRegistryAddress, tokenId } = contractOptions; + const { titleEscrowVersion } = options; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (!titleEscrowAddress) { + if (!tokenRegistryAddress) throw new Error('Token registry address is required'); + if (!tokenId) throw new Error('Token ID is required'); + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + { titleEscrowVersion }, + ); + } + + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { holderAddress, remarks } = params; + + // Connect V5 contract by default + // let titleEscrowContract: v5Contracts.TitleEscrow | v4Contracts.TitleEscrow = + // v5Contracts.TitleEscrow__factory.connect(titleEscrowAddress, signer); + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V4, + provider: signer.provider, + }), + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }), + ]); + } + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let titleEscrowContract: ContractV5 | ContractV6; + if (isV5TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v4Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + // check for current holder and signer + // const currentHolder = await titleEscrowContract.holder(); + + // if (currentHolder.toLowerCase() === holderAddress.toLowerCase()) { + // throw new Error('Cannot transfer to current holder'); + // } + // const signerAddress = await getSignerAddressSafe(signer); + // if (currentHolder.toLowerCase() !== signerAddress.toLowerCase()) { + // throw new Error('Only the current holder can transfer'); + // } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [holderAddress, encryptedRemarks] : [holderAddress]; + + if (isV6) { + await (titleEscrowContract as ContractV6).transferHolder.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.transferHolder(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for transferHolder failed'); + } + + // If gas values are missing, query gas station if available + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + // Send the actual transaction + if (isV5TT) { + return await titleEscrowContract.transferHolder(holderAddress, encryptedRemarks, txOptions); + } else if (isV4TT) { + return await titleEscrowContract.transferHolder(holderAddress, txOptions); + } +}; + +/** + * Transfers the beneficiary role of a Title Escrow contract to a new beneficiary address. + * The caller of this function must be the current holder. + * @param {ContractOptions} contractOptions - Contains `tokenRegistryAddress` and optionally `tokenId` and `titleEscrowAddress`. + * @param {Signer | SignerV6} signer - The signer (ethers v5 or v6) who initiates and signs the transaction. + * @param {TransferBeneficiaryParams} params - Object containing: + * - `newBeneficiaryAddress`: Address to which the beneficiary role is being transferred. + * - `remarks` (optional): Optional encrypted message attached with the transaction. + * @param {TransactionOptions} options - Transaction configuration options: + * - `titleEscrowVersion` (optional): Token registry version, either `'v4'` or `'v5'`. + * - `chainId` (optional): Used to query gas station info if gas fee values are missing. + * - `maxFeePerGas`(optional), `maxPriorityFeePerGas`(optional): EIP-1559 gas fee parameters. + * - `id`(optional): Used for encryption of remarks. + * @throws If required values like `titleEscrowAddress` or `signer.provider` are missing. + * @throws If the version is unsupported (neither v4 nor v5). + * @throws If the dry-run `callStatic` fails for pre-checking the transaction. + * @returns {Promise} The transaction response for the `transferBeneficiary` call. + */ +const transferBeneficiary = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: TransferBeneficiaryParams, + options: TransactionOptions, +): Promise => { + const { tokenId, tokenRegistryAddress } = contractOptions; + const { titleEscrowVersion } = options; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (!titleEscrowAddress) { + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + { titleEscrowVersion }, + ); + } + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { newBeneficiaryAddress, remarks } = params; + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V4, + provider: signer.provider, + }), + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }), + ]); + } + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let titleEscrowContract: ContractV5 | ContractV6; + if (isV5TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v4Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + // check for current beneficiary and signer + // const currentHolder = await titleEscrowContract.holder(); + // const currentBeneficiary = await titleEscrowContract.beneficiary(); + // if (currentBeneficiary.toLowerCase() === newBeneficiaryAddress.toLowerCase()) { + // throw new Error('Cannot transfer to current beneficiary'); + // } + // const signerAddress = await signer.getAddress(); + // if (currentHolder.toLowerCase() !== signerAddress.toLowerCase()) { + // throw new Error('Only the current holder can transfer'); + // } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [newBeneficiaryAddress, encryptedRemarks] : [newBeneficiaryAddress]; + + if (isV6) { + await (titleEscrowContract as ContractV6).transferBeneficiary.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.transferBeneficiary(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for transferBeneficiary failed'); + } + + // If gas values are missing, query gas station if available + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + if (isV5TT) { + return await titleEscrowContract.transferBeneficiary( + newBeneficiaryAddress, + encryptedRemarks, + txOptions, + ); + } else if (isV4TT) { + return await titleEscrowContract.transferBeneficiary(newBeneficiaryAddress, txOptions); + } +}; + +/** + * Transfers both the holder and beneficiary roles of a Title Escrow contract to new addresses. + * The caller of this function must be the current holder and beneficiary both. + * @param {ContractOptions} contractOptions - Contains `tokenRegistryAddress` and optionally `tokenId` and `titleEscrowAddress`. + * @param {Signer | SignerV6} signer - The signer (ethers v5 or v6) who initiates and signs the transaction. + * @param {TransferOwnersParams} params - Object containing: + * - `newBeneficiaryAddress`: The new beneficiary address. + * - `newHolderAddress`: The new holder address. + * - `remarks` (optional): Optional remarks that will be encrypted and included with the transaction. + * @param {TransactionOptions} options - Transaction configuration options: + * - `titleEscrowVersion` (optional): Token registry version, either `'v4'` or `'v5'`. + * - `chainId` (optional): Used for gas station lookup if gas fee values are not provided. + * - `maxFeePerGas`(optional), `maxPriorityFeePerGas`(optional): EIP-1559 gas fee parameters. + * - `id`(optional): Used for encrypting remarks. + * @throws If required fields like `titleEscrowAddress` or `signer.provider` are missing. + * @throws If the title escrow version is unsupported. + * @throws If the pre-check `callStatic.transferOwners` fails. + * @returns {Promise} The transaction response from the `transferOwners` call. + */ +const transferOwners = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: TransferOwnersParams, + options: TransactionOptions, +): Promise => { + const { tokenId, tokenRegistryAddress } = contractOptions; + const { titleEscrowVersion } = options; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (!titleEscrowAddress) { + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + { titleEscrowVersion }, + ); + } + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { newBeneficiaryAddress, newHolderAddress, remarks } = params; + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V4, + provider: signer.provider, + }), + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }), + ]); + } + if (!isV4TT && !isV5TT) { + throw new Error('Only Token Registry V4/V5 is supported'); + } + + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let titleEscrowContract: ContractV5 | ContractV6; + if (isV5TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v4Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + // check for current beneficiary, holder and signer + // const currentHolder = await titleEscrowContract.holder(); + // const currentBeneficiary = await titleEscrowContract.beneficiary(); + // if (currentBeneficiary.toLowerCase() === newBeneficiaryAddress.toLowerCase()) { + // throw new Error('Cannot transfer to current beneficiary'); + // } + // if (currentHolder.toLowerCase() === newHolderAddress.toLowerCase()) { + // throw new Error('Cannot transfer to current holder'); + // } + // const signerAddress = await signer.getAddress(); + // if ( + // currentHolder.toLowerCase() !== signerAddress.toLowerCase() || + // currentBeneficiary.toLowerCase() !== signerAddress.toLowerCase() + // ) { + // throw new Error('Holder and Beneficiary must be current signer'); + // } + + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT + ? [newBeneficiaryAddress, newHolderAddress, encryptedRemarks] + : [newBeneficiaryAddress, newHolderAddress]; + + if (isV6) { + await (titleEscrowContract as ContractV6).transferOwners.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.transferOwners(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for transferOwners failed'); + } + + // If gas values are missing, query gas station if available + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + if (isV5TT) { + return await titleEscrowContract.transferOwners( + newBeneficiaryAddress, + newHolderAddress, + encryptedRemarks, + txOptions, + ); + } else if (isV4TT) { + return await titleEscrowContract.transferOwners( + newBeneficiaryAddress, + newHolderAddress, + txOptions, + ); + } +}; + +/** + * Nominates a new beneficiary on the Title Escrow contract. + * The caller of this function must be the current beneficiary. + * @param {ContractOptions} contractOptions - Contains `tokenRegistryAddress` and optionally `tokenId` and `titleEscrowAddress`. + * @param {Signer | SignerV6} signer - The signer (ethers v5 or v6) who will sign and send the transaction. + * @param {NominateParams} params - Nomination parameters: + * - `newBeneficiaryAddress`: The Ethereum address to nominate as the new beneficiary. + * - `remarks` (optional): Remarks to include with the transaction (will be encrypted). + * @param {TransactionOptions} options - Transaction-level configuration: + * - `titleEscrowVersion` (optional): Specifies token registry version, either `'v4'` or `'v5'`. + * - `chainId` (optional): Chain ID used for querying gas stations if fees are not set. + * - `maxFeePerGas`(optional), `maxPriorityFeePerGas`(optional): EIP-1559-compatible gas fee settings. + * - `id`(optional): Used for encrypting the remarks string. + * @throws If required inputs like `titleEscrowAddress` or `signer.provider` are missing. + * @throws If token registry version is unsupported. + * @throws If the dry-run `callStatic.nominate()` fails. + * @returns {Promise} The transaction response from the `nominate` method. + */ +const nominate = async ( + contractOptions: ContractOptions, + signer: Signer | SignerV6, + params: NominateParams, + options: TransactionOptions, +): Promise => { + const { tokenId, tokenRegistryAddress } = contractOptions; + const { titleEscrowVersion } = options; + let { titleEscrowAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + let isV5TT = titleEscrowVersion === 'v5'; + let isV4TT = titleEscrowVersion === 'v4'; + + if (!titleEscrowAddress) { + titleEscrowAddress = await getTitleEscrowAddress( + tokenRegistryAddress, + tokenId as string, + signer.provider, + { titleEscrowVersion }, + ); + } + if (!titleEscrowAddress) throw new Error('Token registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + const { newBeneficiaryAddress, remarks } = params; + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id!)}` : '0x'; + + // Detect version if not explicitly provided + if (titleEscrowVersion === undefined) { + [isV4TT, isV5TT] = await Promise.all([ + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V4, + provider: signer.provider, + }), + isTitleEscrowVersion({ + titleEscrowAddress, + versionInterface: TitleEscrowInterface.V5, + provider: signer.provider, + }), + ]); + } + + const Contract = getEthersContractFromProvider(signer.provider); + // Connect V5 contract by default + let titleEscrowContract: ContractV5 | ContractV6; + if (isV5TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v5Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } else if (isV4TT) { + titleEscrowContract = new Contract( + titleEscrowAddress, + v4Contracts.TitleEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + } + + // check for current beneficiary and signer + // const currentBeneficiary = await titleEscrowContract.beneficiary(); + // const signerAddress = await signer.getAddress(); + // if (currentBeneficiary.toLowerCase() !== signerAddress.toLowerCase()) { + // throw new Error('Beneficiary must be current signer'); + // } + // if (currentBeneficiary.toLowerCase() === newBeneficiaryAddress.toLowerCase()) { + // throw new Error('Cannot nominate to current beneficiary'); + // } + + // Check callStatic (dry run) + // Check callStatic (dry run) + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = isV5TT ? [newBeneficiaryAddress, encryptedRemarks] : [newBeneficiaryAddress]; + + if (isV6) { + await (titleEscrowContract as ContractV6).nominate.staticCall(...args); + } else { + await (titleEscrowContract as ContractV5).callStatic.nominate(...args); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for nominate failed'); + } + + // If gas values are missing, query gas station if available + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + + if (isV5TT) { + return await titleEscrowContract.nominate(newBeneficiaryAddress, encryptedRemarks, txOptions); + } else if (isV4TT) { + return await titleEscrowContract.nominate(newBeneficiaryAddress, txOptions); + } +}; + +export { transferHolder, transferBeneficiary, transferOwners, nominate }; diff --git a/src/token-registry-functions/types.ts b/src/token-registry-functions/types.ts new file mode 100644 index 0000000..3af157f --- /dev/null +++ b/src/token-registry-functions/types.ts @@ -0,0 +1,89 @@ +import { CHAIN_ID } from '@tradetrust-tt/tradetrust-utils'; +import { BigNumber, providers as providersV5 } from 'ethers'; +import { BigNumberish, Provider as ProviderV6 } from 'ethersV6'; + +export type GasValue = BigNumber | BigNumberish | string | number; + +export interface RejectTransferParams { + remarks?: string; +} +export interface ReturnToIssuerParams { + remarks?: string; +} + +export interface AcceptReturnedParams { + tokenId: string | number; + remarks?: string; +} +export interface RejectReturnedParams { + tokenId: string | number; + remarks?: string; +} + +export interface MintTokenParams { + beneficiaryAddress: string; + holderAddress: string; + tokenId: string | number; + remarks?: string; +} +export interface OwnerOfTokenParams { + tokenId: string | number; +} + +export interface TransactionOptions { + chainId?: CHAIN_ID; + titleEscrowVersion?: 'v4' | 'v5'; + maxFeePerGas?: BigNumberish | string | number | BigNumber; + maxPriorityFeePerGas?: BigNumberish | string | number | BigNumber; + id?: string; +} + +export type ContractOptions = + | { + titleEscrowAddress: string; // Present — no restrictions on the rest + tokenId?: string | number; + tokenRegistryAddress?: string; + } + | { + titleEscrowAddress?: undefined; // Absent — must provide both below + tokenId: string | number; + tokenRegistryAddress: string; + }; + +export type AcceptReturnedOptions = { + tokenRegistryAddress: string; +}; +export type RejectReturnedOptions = { + tokenRegistryAddress: string; +}; + +export type MintTokenOptions = { + tokenRegistryAddress: string; +}; +export type OwnerOfTokenOptions = { + tokenRegistryAddress: string; +}; + +export interface TransferHolderParams { + holderAddress: string; + remarks?: string; +} +export interface TransferBeneficiaryParams { + newBeneficiaryAddress: string; + remarks?: string; +} +export interface NominateParams { + newBeneficiaryAddress: string; + remarks?: string; +} +export interface TransferOwnersParams { + newHolderAddress: string; + newBeneficiaryAddress: string; + remarks?: string; +} + +export interface ProviderInfo { + Provider: providersV5.Provider | ProviderV6; + ethersVersion: 'v5' | 'v6'; + titleEscrowVersion: 'v4' | 'v5'; +} diff --git a/src/token-registry-functions/utils.ts b/src/token-registry-functions/utils.ts new file mode 100644 index 0000000..bbfc666 --- /dev/null +++ b/src/token-registry-functions/utils.ts @@ -0,0 +1,44 @@ +import { isV6EthersProvider } from '../utils/ethers'; +import { GasValue } from './types'; +import { CHAIN_ID, SUPPORTED_CHAINS } from '@tradetrust-tt/tradetrust-utils'; +import { Signer } from 'ethers'; +import { Signer as SignerV6 } from 'ethersV6'; + +const getTxOptions = async ( + signer: SignerV6 | Signer, + chainId: CHAIN_ID, + maxFeePerGas: GasValue, + maxPriorityFeePerGas: GasValue, +) => { + // If gas values are missing, query gas station if available + if (!maxFeePerGas || !maxPriorityFeePerGas) { + chainId = chainId ?? ((await getChainIdSafe(signer)) as unknown as CHAIN_ID); + const gasStation = SUPPORTED_CHAINS[chainId]?.gasStation; + + if (gasStation) { + const gasFees = await gasStation(); + maxFeePerGas = gasFees?.maxFeePerGas ?? 0; + maxPriorityFeePerGas = gasFees?.maxPriorityFeePerGas ?? 0; + } + } + return maxFeePerGas && maxPriorityFeePerGas ? { maxFeePerGas, maxPriorityFeePerGas } : {}; +}; + +// 🔍 Handles both Ethers v5 and v6 signer types +const getChainIdSafe = async (signer: SignerV6 | Signer): Promise => { + if (isV6EthersProvider(signer.provider)) { + const network = await (signer as SignerV6).provider?.getNetwork(); + if (!network?.chainId) throw new Error('Cannot determine chainId: provider is missing'); + return network.chainId; + } + return await (signer as Signer).getChainId(); +}; + +const getSignerAddressSafe = async (signer: SignerV6 | Signer): Promise => { + if (isV6EthersProvider(signer.provider)) { + return await (signer as SignerV6).getAddress(); + } + return await (signer as unknown as Signer).getAddress(); +}; + +export { getChainIdSafe, getTxOptions, getSignerAddressSafe }; diff --git a/tsconfig.json b/tsconfig.json index 06eebbb..41deed1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,9 +5,12 @@ "moduleResolution": "nodenext", "experimentalDecorators": true, "emitDecoratorMetadata": true, + "types": ["vitest/globals", "mocha"], "baseUrl": ".", "rootDir": ".", - "paths": {}, + "paths": { + "src/*": ["./src/*"] + }, "resolveJsonModule": true, "allowJs": true, "checkJs": true, diff --git a/vitest.config.ts b/vitest.config.ts index 7867a67..7a76875 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,11 @@ import { config } from 'dotenv'; config({ path: '.env' }); export default defineConfig({ + resolve: { + alias: { + 'src/': new URL('./src/', import.meta.url).pathname, + }, + }, define: { 'import.meta.vitest': 'undefined', }, @@ -15,6 +20,7 @@ export default defineConfig({ ], cacheDir: 'node_modules/.vitest', test: { + globals: true, include: ['**/*.test.{ts,js}'], retry: process.env.CI ? 3 : 0, // setupFiles: ['dotenv/config'], //this line @@ -25,7 +31,7 @@ export default defineConfig({ inline: ['@govtechsg/oa-verify', '@tradetrust-tt/tt-verify'], // Inline oa-verify package directly }, }, - exclude: ['dist', 'node_modules', '*/type{s}.{ts,js}'], + exclude: ['dist', 'node_modules', '*/type{s}.{ts,js}', 'src/__tests__/e2e/**'], coverage: { enabled: true, provider: 'v8',