diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c7d51a90..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/gen-deploy-docs.yml b/.github/workflows/gen-deploy-docs.yml index dc02b3a4..6bcdf4fa 100644 --- a/.github/workflows/gen-deploy-docs.yml +++ b/.github/workflows/gen-deploy-docs.yml @@ -20,10 +20,9 @@ jobs: mv docs/documentation/html massa-web3 - name: Deploy files uses: appleboy/scp-action@master - env: - HOST: ${{ secrets.MASSANET_HOST }} - USERNAME: ${{ secrets.MASSANET_USERNAME }} - KEY: ${{ secrets.MASSANET_SSHKEY }} with: + host: ${{ secrets.MASSANET_HOST }} + username: ${{ secrets.MASSANET_USERNAME }} + key: ${{ secrets.MASSANET_SSHKEY }} source: "./massa-web3" - target: "/var/www/type-doc" \ No newline at end of file + target: "/var/www/type-doc" diff --git a/.github/workflows/powered-by.yml b/.github/workflows/powered-by.yml index 7469008a..dac83a90 100644 --- a/.github/workflows/powered-by.yml +++ b/.github/workflows/powered-by.yml @@ -36,7 +36,7 @@ jobs: - name: Generate Powered-By run: | - ./generate_powered-by.sh + ./scripts/generate_powered-by.sh - name: Commit Changes uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 255d0bbd..85fdc5dd 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -39,7 +39,7 @@ jobs: npm run test - name: allow access to coverage.sh - run: chmod +x ${GITHUB_WORKSPACE}/.github/coverage.sh + run: chmod +x ./scripts/coverage.sh shell: bash - name: Extract coverage @@ -47,16 +47,21 @@ jobs: run: | value=$(npm run test:cov | awk '/All files/ {print $10}' | tr -d '%') echo "coverage=$value" >> $GITHUB_OUTPUT - echo "${GITHUB_WORKSPACE}/.github/coverage.sh" - name: Add test coverage to README - run: ${GITHUB_WORKSPACE}/.github/coverage.sh + run: ./scripts/coverage.sh shell: bash env: COVERAGE: ${{ steps.coverage.outputs.coverage }} + - name: Check if coverage changed + id: check_coverage_changed + run: | + echo "::set-output name=coverage_changed::$(git diff --name-only README.md | grep -E '^README.md$')" + - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 + if: ${{ github.ref == 'refs/heads/main' && steps.check_coverage_changed.outputs.coverage_changed != '' }} with: commit_message: "Generate coverage badge" file_pattern: "README.md" diff --git a/.gitignore b/.gitignore index 19879b61..dcb9d61c 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,7 @@ bundle.* docs #.env files -.env \ No newline at end of file +.env + +# Misc +.DS_Store diff --git a/examples/smartContracts/deployer.ts b/examples/smartContracts/deployer.ts index 022dde3f..2543cbe8 100644 --- a/examples/smartContracts/deployer.ts +++ b/examples/smartContracts/deployer.ts @@ -6,6 +6,7 @@ import { EOperationStatus } from '../../src/interfaces/EOperationStatus'; import { Args } from '../../src/utils/arguments'; import { readFileSync } from 'fs'; import { u64ToBytes, u8toByte } from '../../src/utils/serializers'; +import { fromMAS } from '../../src'; const path = require('path'); const chalk = require('chalk'); @@ -72,6 +73,7 @@ export const deploySmartContracts = async ( web3Client: Client, fee = 0n, maxGas = 1_000_000n, + maxCoins = fromMAS(0.1), deployerAccount?: IAccount, ): Promise => { let deploymentOperationId: string; @@ -150,6 +152,7 @@ export const deploySmartContracts = async ( datastore, fee, maxGas, + maxCoins, } as IContractData, deployerAccount, ); diff --git a/examples/smartContracts/index.ts b/examples/smartContracts/index.ts index cce92734..514ba642 100644 --- a/examples/smartContracts/index.ts +++ b/examples/smartContracts/index.ts @@ -213,6 +213,7 @@ const pollAsyncEvents = async ( web3Client, 0n, 1_000_000n, + fromMAS(0.2), deployerAccount, ); spinner.succeed( diff --git a/package-lock.json b/package-lock.json index 8fb7ec18..cb47690f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@massalabs/massa-web3", - "version": "1.17.4", + "version": "1.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@massalabs/massa-web3", - "version": "1.17.4", + "version": "1.21.0", "license": "(MIT AND Apache-2.0)", "dependencies": { "@noble/ed25519": "^1.7.3", @@ -20,7 +20,6 @@ "buffer": "^6.0.3", "crypto-js": "^4.1.1", "dotenv": "^16.0.3", - "esmify": "^2.1.1", "jest-environment-jsdom": "^29.5.0", "js-base64": "^3.7.5", "string_decoder": "^1.3.0", @@ -71,6 +70,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -109,6 +109,7 @@ "version": "7.21.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -117,6 +118,7 @@ "version": "7.21.8", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -146,6 +148,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dev": true, "dependencies": { "@babel/types": "^7.21.5", "@jridgewell/gen-mapping": "^0.3.2", @@ -160,6 +163,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.21.5", "@babel/helper-validator-option": "^7.21.0", @@ -178,6 +182,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -186,6 +191,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, "dependencies": { "@babel/template": "^7.20.7", "@babel/types": "^7.21.0" @@ -198,6 +204,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -209,6 +216,7 @@ "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, "dependencies": { "@babel/types": "^7.21.4" }, @@ -220,6 +228,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.21.5", "@babel/helper-module-imports": "^7.21.4", @@ -238,6 +247,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -246,6 +256,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "dev": true, "dependencies": { "@babel/types": "^7.21.5" }, @@ -257,6 +268,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -268,6 +280,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -284,6 +297,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -292,6 +306,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "dev": true, "dependencies": { "@babel/template": "^7.20.7", "@babel/traverse": "^7.21.5", @@ -382,6 +397,7 @@ "version": "7.21.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -393,6 +409,7 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -424,17 +441,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -514,6 +520,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -575,22 +582,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", - "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.21.5", - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/helper-simple-access": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", @@ -613,6 +604,7 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", @@ -626,6 +618,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.21.4", "@babel/generator": "^7.21.5", @@ -646,6 +639,7 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.21.5", "@babel/helper-validator-identifier": "^7.19.1", @@ -1179,6 +1173,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1192,6 +1187,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1200,6 +1196,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1217,12 +1214,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -1231,7 +1230,8 @@ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "node_modules/@massalabs/eslint-config": { "version": "0.0.9", @@ -2636,79 +2636,6 @@ "follow-redirects": "^1.14.8" } }, - "node_modules/babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", - "dependencies": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/babel-code-frame/node_modules/js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==" - }, - "node_modules/babel-code-frame/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/babel-jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", @@ -2730,22 +2657,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", - "dependencies": { - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-import-to-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-import-to-require/-/babel-plugin-import-to-require-1.0.0.tgz", - "integrity": "sha512-dc843CwrFivjO8AVgxcHvxl0cb7J7Ed8ZGFP8+PjH3X1CnyzYtAU1WL1349m9Wc/+oqk4ETx2+cIEO2jlp3XyQ==", - "dependencies": { - "babel-template": "^6.26.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -2816,91 +2727,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", - "dependencies": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", - "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/babel-traverse/node_modules/globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-traverse/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", - "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babel-types/node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "bin": { - "babylon": "bin/babylon.js" - } - }, "node_modules/bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", @@ -3037,6 +2863,7 @@ "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3129,7 +2956,8 @@ "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==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/bufferutil": { "version": "4.0.7", @@ -3190,11 +3018,6 @@ "dev": true, "peer": true }, - "node_modules/cached-path-relative": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", - "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==" - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3262,6 +3085,7 @@ "version": "1.0.30001486", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", "integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3583,20 +3407,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -3606,19 +3416,8 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/cosmiconfig": { "version": "5.2.1", @@ -3944,14 +3743,6 @@ "node": ">=12" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -4007,7 +3798,8 @@ "node_modules/electron-to-chromium": { "version": "1.4.387", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.387.tgz", - "integrity": "sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==" + "integrity": "sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==", + "dev": true }, "node_modules/elegant-spinner": { "version": "1.0.1", @@ -4188,6 +3980,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, "engines": { "node": ">=6" } @@ -4941,23 +4734,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esmify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/esmify/-/esmify-2.1.1.tgz", - "integrity": "sha512-GyOVgjG7sNyYB5Mbo15Ll4aGrcXZzZ3LI22rbLOjCI7L/wYelzQpBHRZkZkqbPNZ/QIRilcaHqzgNCLcEsi1lQ==", - "dependencies": { - "@babel/core": "^7.2.2", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-transform-modules-commonjs": "^7.2.0", - "babel-plugin-import-to-require": "^1.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "^1.6.2", - "duplexer2": "^0.1.4", - "through2": "^2.0.5" - } - }, "node_modules/espree": { "version": "9.5.2", "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", @@ -5419,6 +5195,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -5539,6 +5316,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "engines": { "node": ">=4" } @@ -5657,6 +5435,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -5668,6 +5447,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -6032,14 +5812,6 @@ "node": ">=10.13.0" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -6512,11 +6284,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7428,6 +7195,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -7472,6 +7240,7 @@ "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" }, @@ -8118,7 +7887,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -8248,21 +8018,11 @@ "dev": true, "peer": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -8553,7 +8313,8 @@ "node_modules/node-releases": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true }, "node_modules/node-ts": { "version": "5.1.2", @@ -9138,7 +8899,8 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -10356,11 +10118,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -10494,33 +10251,6 @@ "node": ">=4" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/readable-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -10533,11 +10263,6 @@ "node": ">= 10.13.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, "node_modules/regexp-util": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/regexp-util/-/regexp-util-1.2.2.tgz", @@ -10843,6 +10568,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -11521,15 +11247,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -11552,6 +11269,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } @@ -11928,11 +11646,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, "node_modules/typedoc": { "version": "0.23.28", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", @@ -12130,6 +11843,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12210,7 +11924,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/v8-compile-cache": { "version": "2.3.0", @@ -12907,6 +12622,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "engines": { "node": ">=0.4" } @@ -12923,7 +12639,8 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yaml-unist-parser": { "version": "1.1.1", diff --git a/package.json b/package.json index 998d7d52..cf59f32b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@massalabs/massa-web3", - "version": "1.17.4", + "version": "1.21.0", "description": "massa's web3 sdk client", "main": "dist/cmd/index.js", "module": "dist/esm/index.js", @@ -69,7 +69,6 @@ "buffer": "^6.0.3", "crypto-js": "^4.1.1", "dotenv": "^16.0.3", - "esmify": "^2.1.1", "jest-environment-jsdom": "^29.5.0", "js-base64": "^3.7.5", "string_decoder": "^1.3.0", diff --git a/powered-by.md b/powered-by.md index 4b7f9a27..c49ecee0 100644 --- a/powered-by.md +++ b/powered-by.md @@ -89,14 +89,6 @@ The following is a list of all the dependencies of this project: **Many thanks to:** n/a -## [esmify](git://github.com/mattdesl/esmify.git) - -**License:** MIT - perpetual - -**Used version:** 2.1.1 - -**Many thanks to:** Matt DesLauriers dave.des@gmail.com https://github.com/mattdesl - ## [jest-environment-jsdom](git+https://github.com/facebook/jest.git) **License:** MIT - perpetual diff --git a/.github/coverage.sh b/scripts/coverage.sh similarity index 79% rename from .github/coverage.sh rename to scripts/coverage.sh index 14e308ed..57958aba 100755 --- a/.github/coverage.sh +++ b/scripts/coverage.sh @@ -21,7 +21,7 @@ else echo "No coverage found" fi -if [ "$oldCoverage" != "$COVERAGE" ] || [ -z "$oldCoverage" ]; then +if [ -z "$oldCoverage" ] || [ "$(echo "$COVERAGE - $oldCoverage >= 1" | bc -l)" -eq 1 ] || [ "$(echo "$oldCoverage - $COVERAGE >= 1" | bc -l)" -eq 1 ]; then echo "Updating badge" newLine="![check-code-coverage](https://img.shields.io/badge/coverage-$COVERAGE%25-$color)" sed -i "3s#.*#${newLine}#" $filename diff --git a/generate_powered-by.sh b/scripts/generate_powered-by.sh similarity index 100% rename from generate_powered-by.sh rename to scripts/generate_powered-by.sh diff --git a/src/index.ts b/src/index.ts index 8881c69d..c84fc557 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,7 +84,11 @@ export { ON_MASSA_EVENT_DATA, ON_MASSA_EVENT_ERROR, } from './web3/EventPoller'; -export { SmartContractsClient } from './web3/SmartContractsClient'; +export { + SmartContractsClient, + MASSA_PROTOFILE_KEY, + PROTO_FILE_SEPARATOR, +} from './web3/SmartContractsClient'; export * from './utils/arguments'; export { fromMAS, toMAS, MassaUnits } from './utils/converters'; export * from './utils/serializers'; diff --git a/src/interfaces/IContractData.ts b/src/interfaces/IContractData.ts index 39426ddf..c1b22f77 100644 --- a/src/interfaces/IContractData.ts +++ b/src/interfaces/IContractData.ts @@ -9,6 +9,7 @@ * * @see fee of type `bigint` represents the storage fee for the smart contract. * @see maxGas of type `bigint` represents the maximum gas that can be consumed by the smart contract. + * @see maxCoins of type `bigint` represents maximum amount of coins allowed to be spent by the execution * @see contractDataText of type `string | undefined` represents the contract's data as string (optional). * @see contractDataBinary of type `Uint8Array | undefined` represents the contract's data as bytecode (optional). * @see address of type `string | undefined` represents the smart contract address (optional). @@ -17,6 +18,7 @@ export interface IContractData { fee: bigint; maxGas: bigint; + maxCoins: bigint; contractDataText?: string; contractDataBinary?: Uint8Array; address?: string; diff --git a/src/interfaces/IContractReadOperationResponse.ts b/src/interfaces/IContractReadOperationResponse.ts index b3644c3b..642cd611 100644 --- a/src/interfaces/IContractReadOperationResponse.ts +++ b/src/interfaces/IContractReadOperationResponse.ts @@ -1,7 +1,8 @@ import { IContractReadOperationData } from './IContractReadOperationData'; /** - * Represents the response of a read-only operation on a deployed contract. + * Represents the output of a smart contract non persistent execution. + * (this operation did not modify the blockchain state) * * @see returnValue of type `Uint8Array` represents the return value of the read operation. * @see info of type `IContractReadOperationData` represents the inputs of the read operation. diff --git a/src/interfaces/IReadData.ts b/src/interfaces/IReadData.ts index 5cde0bf6..9b5fa2a5 100644 --- a/src/interfaces/IReadData.ts +++ b/src/interfaces/IReadData.ts @@ -1,7 +1,6 @@ /** * Represents the data of a read operation. * - * @see fee - storage fee for taking place in books * @see maxGas - The maximum amount of gas that the execution of the contract is allowed to cost. * @see targetAddress - Target smart contract address * @see targetFunction - Target function name. No function is called if empty. @@ -9,7 +8,6 @@ * @see callerAddress - Caller address */ export interface IReadData { - fee: bigint; maxGas: bigint; targetAddress: string; targetFunction: string; diff --git a/src/interfaces/IWalletClient.ts b/src/interfaces/IWalletClient.ts index 9275f93c..e669437a 100644 --- a/src/interfaces/IWalletClient.ts +++ b/src/interfaces/IWalletClient.ts @@ -109,7 +109,7 @@ export interface IWalletClient { * * @param data - The message to verify. * @param signature - The signature to verify. - * @param accountSignerAddress - The address of the account used to sign the message. + * @param signerPublicKey - The public key of the signer. * * @returns A promise that resolves to a boolean (true if the signature is valid, * false otherwise). @@ -117,7 +117,7 @@ export interface IWalletClient { verifySignature( data: string | Buffer, signature: ISignature, - accountSignerAddress: string, + signerPublicKey: string, ): Promise; /** diff --git a/src/utils/Xbqcrypto.ts b/src/utils/Xbqcrypto.ts index f5149ec3..49135185 100644 --- a/src/utils/Xbqcrypto.ts +++ b/src/utils/Xbqcrypto.ts @@ -65,8 +65,13 @@ export function varintEncode(data: number | bigint): Uint8Array { * * @param data - The varint encoded Uint8Array * - * @returns The decoded number + * @returns The decoded number and the number of bytes read */ -export function varintDecode(data: Uint8Array): number { - return varint.decode(data); +export function varintDecode(data: Uint8Array): { + value: number; + bytes: number; +} { + const value = varint.decode(data); + const bytes = varint.decode.bytes; + return { value, bytes }; } diff --git a/src/utils/bytes.ts b/src/utils/bytes.ts new file mode 100644 index 00000000..69fd1b32 --- /dev/null +++ b/src/utils/bytes.ts @@ -0,0 +1,48 @@ +import { base58Decode } from './Xbqcrypto'; + +/** + * Prefixes for secret and public keys. + * Prefixes are used as a convention to differentiate one key from another. + */ +const SECRET_KEY_PREFIX = 'S'; +const PUBLIC_KEY_PREFIX = 'P'; + +/** + * Get the byte representation of a given secret key. + * + * @param secretKey - The secret key to get the bytes from. + * + * @throws if the secret key is not valid. + * + * @returns a Uint8Array containing the bytes of the secret key. + */ +export function getBytesSecretKey(secretKey: string): Uint8Array { + if (!(secretKey[0] == SECRET_KEY_PREFIX)) { + throw new Error( + `Invalid secret key prefix: "${secretKey[0]}". The secret key should start with "${SECRET_KEY_PREFIX}". Please verify your secret key and try again.`, + ); + } + const secretKeyBase58Decoded: Buffer = base58Decode(secretKey.slice(1)); // Slice off the prefix + return secretKeyBase58Decoded; +} + +/** + * Retrieves the byte representation of a given public key. + * + * @param publicKey - The public key to obtain the bytes from. + * + * @throws If the public key has an incorrect {@link PUBLIC_KEY_PREFIX}. + * + * @returns A Uint8Array containing the bytes of the public key. + */ +export function getBytesPublicKey(publicKey: string): Uint8Array { + if (!(publicKey[0] == PUBLIC_KEY_PREFIX)) { + throw new Error( + `Invalid public key prefix: ${publicKey[0]} should be ${PUBLIC_KEY_PREFIX}`, + ); + } + const publicKeyBase58Decoded: Buffer = base58Decode( + publicKey.slice(1), // Slice off the prefix + ); + return publicKeyBase58Decoded; +} diff --git a/src/utils/keyAndAddresses.ts b/src/utils/keyAndAddresses.ts new file mode 100644 index 00000000..c0d8ba23 --- /dev/null +++ b/src/utils/keyAndAddresses.ts @@ -0,0 +1,114 @@ +import { + base58Encode, + varintEncode, + varintDecode, + hashBlake3, +} from './Xbqcrypto'; + +import * as ed from '@noble/ed25519'; +import { getBytesPublicKey, getBytesSecretKey } from './bytes'; + +/** + * Prefixes for secret keys, public keys, and addresses. + * Prefixes are used as a convention to differentiate one key from another. + */ +const PUBLIC_KEY_PREFIX = 'P'; +const ADDRESS_PREFIX = 'AU'; + +/** + * A secret key. + * The secret key object is created from a base58 encoded string representing the secret key. + * + * @remarks + * - String representation is S + base58Check(version_bytes + secret_key_hash_bytes) + * - bytes attribute is the Uint8Array representation of the secret key. + */ +export class SecretKey { + version: number; + private bytes: Uint8Array; + + constructor(secretKeyBase58Encoded: string) { + const versionAndKeyBytes = getBytesSecretKey(secretKeyBase58Encoded); + + // Slice off the version byte + this.bytes = versionAndKeyBytes.slice(1); + + this.version = varintDecode(versionAndKeyBytes.slice(0, 1)).value; + } + + /* Get the public key from the secret key */ + async getPublicKey(): Promise { + const publicKeyArray: Uint8Array = await ed.getPublicKey(this.bytes); + return new PublicKey(publicKeyArray, this.version); + } + + /* Sign a message hash digest with the secret key */ + async signDigest(messageHashDigest: Uint8Array): Promise { + return await ed.sign(messageHashDigest, this.bytes); + } +} + +/** + * The PublicKey class represents a cryptographic public key. + * + * @remarks + * - The public key is derived from the secret key and got the same version as the secret key. + * - String representation is P + base58Check(version_bytes + public_key_hash_bytes) + * - bytes attribute is the Uint8Array representation of the public key. + */ +export class PublicKey { + version: number; + base58Encode: string; + bytes: Uint8Array; + + constructor(bytes: Uint8Array, version: number) { + this.version = version; + this.bytes = bytes; + const versionBuffer = Buffer.from(varintEncode(this.version)); + + // Generate base58 encoded public key + this.base58Encode = + PUBLIC_KEY_PREFIX + + base58Encode(Buffer.concat([versionBuffer, Buffer.from(this.bytes)])); + } + + // Create a new PublicKey object from a base58 encoded string + static fromString(base58Encoded: string): PublicKey { + const versionAndKeyBytes = getBytesPublicKey(base58Encoded); + + // Slice off the version byte + const version = varintDecode(versionAndKeyBytes.slice(0, 1)).value; + const keyBytes = versionAndKeyBytes.slice(1); + + return new PublicKey(keyBytes, version); + } +} + +/** + * An address. + * + * @remarks the address object is created from a public key and got the same version as the public key. + * + * @remarks + * - String representation is A + U/S + base58Check(version_bytes + hashBlake3(version_bytes + public_key_bytes)) + * - The address bytes representation is `version + hashBlake3(version + publicKey)`. + * - bytes is not an attribute of the address object because it is not needed. + */ +export class Address { + version: number; + base58Encode: string; + + constructor(publicKey: PublicKey) { + this.version = publicKey.version; + + const versionBuffer = Buffer.from(varintEncode(this.version)); + const versionAndPublicKey = Buffer.concat([versionBuffer, publicKey.bytes]); + + // Generate base58 encoded address + this.base58Encode = + ADDRESS_PREFIX + + base58Encode( + Buffer.concat([versionBuffer, hashBlake3(versionAndPublicKey)]), + ); + } +} diff --git a/src/utils/serializers/arrays.ts b/src/utils/serializers/arrays.ts index cd3d3105..84434c7d 100644 --- a/src/utils/serializers/arrays.ts +++ b/src/utils/serializers/arrays.ts @@ -26,7 +26,7 @@ import { * * @returns The size of the typed array unit. */ -const getDatatypeSize = (type: ArrayType): number => { +export const getDatatypeSize = (type: ArrayType): number => { switch (type) { case ArrayType.BOOL: case ArrayType.U8: diff --git a/src/utils/uint8ArrayToString.ts b/src/utils/uint8ArrayToString.ts new file mode 100644 index 00000000..b0f590f5 --- /dev/null +++ b/src/utils/uint8ArrayToString.ts @@ -0,0 +1,15 @@ +/** + * Converts any Uint8Array to string + * + * @param bytesArray - Uint8Array to convert + * + * @returns The string representation of the Uint8Array + */ +export function bytesArrayToString(bytesArray: Uint8Array): string { + let str = ''; + // use a for-of loop + for (const byte of bytesArray) { + str += String.fromCharCode(byte); + } + return str; +} diff --git a/src/web3/BaseClient.ts b/src/web3/BaseClient.ts index 1f886d74..f4bb8e64 100755 --- a/src/web3/BaseClient.ts +++ b/src/web3/BaseClient.ts @@ -16,11 +16,12 @@ const encodeAddressToBytes = ( address: string, isSmartContract = false, ): Buffer => { - let targetAddressEncoded = base58Decode(address.slice(2)).slice(1); + let targetAddressEncoded = base58Decode(address.slice(2)); targetAddressEncoded = Buffer.concat([ isSmartContract ? Buffer.from([1]) : Buffer.from([0]), targetAddressEncoded, ]); + return targetAddressEncoded; }; @@ -302,6 +303,11 @@ export class BaseClient { varintEncode((data as IContractData).maxGas), ); + // max coins amount + const maxCoinEncoded = Buffer.from( + varintEncode((data as IContractData).maxCoins), + ); + // contract data const contractDataEncoded = Buffer.from(scBinaryCode); const dataLengthEncoded = Buffer.from( @@ -339,6 +345,7 @@ export class BaseClient { expirePeriodEncoded, typeIdEncoded, maxGasEncoded, + maxCoinEncoded, dataLengthEncoded, contractDataEncoded, datastoreSerializedBufferLen, @@ -350,6 +357,7 @@ export class BaseClient { expirePeriodEncoded, typeIdEncoded, maxGasEncoded, + maxCoinEncoded, dataLengthEncoded, contractDataEncoded, datastoreSerializedBufferLen, diff --git a/src/web3/PublicApiClient.ts b/src/web3/PublicApiClient.ts index f00510a3..03fba4b3 100755 --- a/src/web3/PublicApiClient.ts +++ b/src/web3/PublicApiClient.ts @@ -154,6 +154,12 @@ export class PublicApiClient extends BaseClient implements IPublicApiClient { /** * Show data about a block (content, finality ...). * + * @remarks + * The blocks are stored in the node cache. After a certain time (depending of the network activity), + * the blocks are removed from the cache and the node will not be able to return the block data. + * The corresponding api parameter is 'max_discarded_blocks'. + * More information can be found here: https://docs.massa.net/en/latest/testnet/all-config.html + * * @param blockIds - The block ids as an array of strings. * * @returns A promise which resolves in the block data. diff --git a/src/web3/SmartContractsClient.ts b/src/web3/SmartContractsClient.ts index f7ece1be..78880d27 100644 --- a/src/web3/SmartContractsClient.ts +++ b/src/web3/SmartContractsClient.ts @@ -34,6 +34,7 @@ import { strToBytes } from '../utils/serializers/strings'; import { BaseClient } from './BaseClient'; import { PublicApiClient } from './PublicApiClient'; import { WalletClient } from './WalletClient'; +import { getBytesPublicKey } from '../utils/bytes'; const MAX_READ_BLOCK_GAS = BigInt(4_294_967_295); const TX_POLL_INTERVAL_MS = 10000; @@ -42,7 +43,11 @@ const TX_STATUS_CHECK_RETRY_COUNT = 100; /** * The key name (as a string) to look for when we are retrieving the proto file from a contract */ -const MASSA_PROTOFILE_KEY = 'protoMassa'; +export const MASSA_PROTOFILE_KEY = 'protoMassa'; +/** + * The separator used to split the proto file content into separate proto files + */ +export const PROTO_FILE_SEPARATOR = '|||||'; /** * Smart Contracts Client object enables smart contract deployment, calls and streaming of events. */ @@ -96,6 +101,13 @@ export class SmartContractsClient const expiryPeriod: number = nodeStatusInfo.next_slot.period + this.clientConfig.periodOffset; + // Check if SC data exists + if (!contractData.contractDataBinary) { + throw new Error( + `Expected non-null contract bytecode, but received null.`, + ); + } + // get the block size if ( contractData.contractDataBinary.length > @@ -120,19 +132,12 @@ export class SmartContractsClient ); // sign payload + const bytesPublicKey: Uint8Array = getBytesPublicKey(sender.publicKey); const signature: ISignature = await WalletClient.walletSignMessage( - Buffer.concat([ - WalletClient.getBytesPublicKey(sender.publicKey), - bytesCompact, - ]), + Buffer.concat([bytesPublicKey, bytesCompact]), sender, ); - // Check if SC data exists - if (!contractData.contractDataBinary) { - throw new Error(`Contract data required. Got null`); - } - const data = { serialized_content: Array.prototype.slice.call(bytesCompact), creator_public_key: sender.publicKey, @@ -187,11 +192,9 @@ export class SmartContractsClient ); // sign payload + const bytesPublicKey: Uint8Array = getBytesPublicKey(sender.publicKey); const signature: ISignature = await WalletClient.walletSignMessage( - Buffer.concat([ - WalletClient.getBytesPublicKey(sender.publicKey), - bytesCompact, - ]), + Buffer.concat([bytesPublicKey, bytesCompact]), sender, ); // request data @@ -220,11 +223,12 @@ export class SmartContractsClient } /** - * Executes a read operation on a smart contract. + * Execute a dry run Smart contract call and returns some data regarding its execution + * such as the changes of in the states that would have happen if the transaction was really executed on chain. * * @param readData - The data required for the a read operation of a smart contract. * - * @returns A promise that resolves to object the result of the read operation. + * @returns A promise that resolves to an object which represents the result of the operation and contains data about its execution. */ public async readSmartContract( readData: IReadData, @@ -330,12 +334,16 @@ export class SmartContractsClient } /** - * Execute a read-only smart contract. + * Send a read-only smart contract execution request. * - * @param contractData - The data required for the read-only smart contract. + * @remarks + * This method is used to dry-run a smart contract execution and get the changes of the states that would + * have happen if the transaction was really executed on chain. + * This operation does not modify the blockchain state. + * + * @param contractData - The data required for the operation. * - * @returns A promise which resolves to an object containing the result - * of the operation. + * @returns A promise which resolves to an object containing data about the operation. * * @throws * - If the contract binary data is missing. @@ -347,11 +355,13 @@ export class SmartContractsClient contractData: IContractData, ): Promise { if (!contractData.contractDataBinary) { - throw new Error(`Contract binary data required. Got null`); + throw new Error( + `Expected non-null contract bytecode, but received null.`, + ); } if (!contractData.address) { - throw new Error(`Contract address required. Got null`); + throw new Error(`Expected contract address, but received null.`); } const data = { @@ -436,7 +446,7 @@ export class SmartContractsClient status = await this.getOperationStatus(opId); } catch (ex) { if (++errCounter > 100) { - const msg = `Failed to retrieve the tx status after 10 failed attempts for operation id: ${opId}.`; + const msg = `Failed to retrieve the tx status after 100 failed attempts for operation id: ${opId}.`; console.error(msg, ex); throw ex; } diff --git a/src/web3/WalletClient.ts b/src/web3/WalletClient.ts index df169f98..4252c79c 100755 --- a/src/web3/WalletClient.ts +++ b/src/web3/WalletClient.ts @@ -21,11 +21,11 @@ import { IBalance } from '../interfaces/IBalance'; import * as ed from '@noble/ed25519'; import { IWalletClient } from '../interfaces/IWalletClient'; import { fromMAS } from '../utils/converters'; +import { getBytesPublicKey } from '../utils/bytes'; -const VERSION_NUMBER = 0; -const ADDRESS_PREFIX = 'AU'; -const PUBLIC_KEY_PREFIX = 'P'; +import { Address, SecretKey, PublicKey } from '../utils/keyAndAddresses'; const SECRET_KEY_PREFIX = 'S'; +const VERSION_NUMBER = 0; const MAX_WALLET_ACCOUNTS = 256; /** @@ -171,25 +171,21 @@ export class WalletClient extends BaseClient implements IWalletClient { } const accountsToCreate: IAccount[] = []; - for (const secretKey of secretKeys) { - const secretKeyBase58Decoded = WalletClient.getBytesSecretKey(secretKey); - const publicKey: Uint8Array = await ed.getPublicKey( - secretKeyBase58Decoded, - ); + const uniqueSecretKeys = secretKeys.filter( + (value, index, self) => self.indexOf(value) === index, + ); - const version = Buffer.from(varintEncode(VERSION_NUMBER)); - const publicKeyBase58Encoded: string = - PUBLIC_KEY_PREFIX + base58Encode(Buffer.concat([version, publicKey])); - const addressBase58Encoded = - ADDRESS_PREFIX + - base58Encode(Buffer.concat([version, hashBlake3(publicKey)])); + for (const secretKeyBase58Encoded of uniqueSecretKeys) { + const secretKey = new SecretKey(secretKeyBase58Encoded); + const publicKey: PublicKey = await secretKey.getPublicKey(); + const address: Address = new Address(publicKey); - if (!this.getWalletAccountByAddress(addressBase58Encoded)) { + if (!this.getWalletAccountByAddress(address.base58Encode)) { accountsToCreate.push({ - secretKey: secretKey, // submitted in base58 - publicKey: publicKeyBase58Encoded, - address: addressBase58Encoded, - createdInThread: getThreadNumber(addressBase58Encoded), + secretKey: secretKeyBase58Encoded, + publicKey: publicKey.base58Encode, + address: address.base58Encode, + createdInThread: getThreadNumber(address.base58Encode), } as IAccount); } } @@ -229,39 +225,32 @@ export class WalletClient extends BaseClient implements IWalletClient { throw new Error('Missing account private key'); } + // Create the secret key object const secretKeyBase58Encoded: string = account.secretKey; - const secretKeyBase58Decoded = WalletClient.getBytesSecretKey( - secretKeyBase58Encoded, - ); - // get public key - const publicKey: Uint8Array = await ed.getPublicKey( - secretKeyBase58Decoded, - ); - const version = Buffer.from(varintEncode(VERSION_NUMBER)); - const publicKeyBase58Encoded: string = - PUBLIC_KEY_PREFIX + base58Encode(Buffer.concat([version, publicKey])); - if (account.publicKey && account.publicKey !== publicKeyBase58Encoded) { + const secretKey: SecretKey = new SecretKey(secretKeyBase58Encoded); + + // create the public key object + const publicKey: PublicKey = await secretKey.getPublicKey(); + if (account.publicKey && account.publicKey !== publicKey.base58Encode) { throw new Error( 'Public key does not correspond the the private key submitted', ); } // get wallet account address - const addressBase58Encoded = - ADDRESS_PREFIX + - base58Encode(Buffer.concat([version, hashBlake3(publicKey)])); - if (account.address && account.address !== addressBase58Encoded) { + const address: Address = new Address(publicKey); + if (account.address && account.address !== address.base58Encode) { throw new Error( 'Account address not correspond the the address submitted', ); } - if (!this.getWalletAccountByAddress(addressBase58Encoded)) { + if (!this.getWalletAccountByAddress(address.base58Encode)) { accountsAdded.push({ - address: addressBase58Encoded, + address: address.base58Encode, secretKey: secretKeyBase58Encoded, - publicKey: publicKeyBase58Encoded, - createdInThread: getThreadNumber(addressBase58Encoded), + publicKey: publicKey.base58Encode, + createdInThread: getThreadNumber(address.base58Encode), } as IAccount); } } @@ -318,31 +307,31 @@ export class WalletClient extends BaseClient implements IWalletClient { /** * Generates a new wallet account. + * @param version_number - The version number of the secret key to be generated, to create a new account. * * @returns A Promise that resolves to an {@link IAccount} object, which represents the newly created account. */ public static async walletGenerateNewAccount(): Promise { // generate private key - const secretKey: Uint8Array = ed.utils.randomPrivateKey(); + const secretKeyArray: Uint8Array = ed.utils.randomPrivateKey(); + const version = Buffer.from(varintEncode(VERSION_NUMBER)); const secretKeyBase58Encoded: string = - SECRET_KEY_PREFIX + base58Encode(Buffer.concat([version, secretKey])); + SECRET_KEY_PREFIX + + base58Encode(Buffer.concat([version, secretKeyArray])); + const secretKey: SecretKey = new SecretKey(secretKeyBase58Encoded); // get public key - const publicKey: Uint8Array = await ed.getPublicKey(secretKey); - const publicKeyBase58Encoded: string = - PUBLIC_KEY_PREFIX + base58Encode(Buffer.concat([version, publicKey])); + const publicKey: PublicKey = await secretKey.getPublicKey(); // get wallet account address - const addressBase58Encoded = - ADDRESS_PREFIX + - base58Encode(Buffer.concat([version, hashBlake3(publicKey)])); + const address: Address = new Address(publicKey); return { - address: addressBase58Encoded, + address: address.base58Encode, secretKey: secretKeyBase58Encoded, - publicKey: publicKeyBase58Encoded, - createdInThread: getThreadNumber(addressBase58Encoded), + publicKey: publicKey.base58Encode, + createdInThread: getThreadNumber(address.base58Encode), } as IAccount; } @@ -356,23 +345,19 @@ export class WalletClient extends BaseClient implements IWalletClient { public static async getAccountFromSecretKey( secretKeyBase58: string, ): Promise { - const version = Buffer.from(varintEncode(VERSION_NUMBER)); // get private key - const secretKeyBase58Decoded = this.getBytesSecretKey(secretKeyBase58); + const secretKey: SecretKey = new SecretKey(secretKeyBase58); // get public key - const publicKey: Uint8Array = await ed.getPublicKey(secretKeyBase58Decoded); - const publicKeyBase58Encoded: string = - PUBLIC_KEY_PREFIX + base58Encode(Buffer.concat([version, publicKey])); + const publicKey: PublicKey = await secretKey.getPublicKey(); // get wallet account address - const addressBase58Encoded = - ADDRESS_PREFIX + - base58Encode(Buffer.concat([version, hashBlake3(publicKey)])); + const address: Address = new Address(publicKey); + return { - address: addressBase58Encoded, + address: address.base58Encode, secretKey: secretKeyBase58, - publicKey: publicKeyBase58Encoded, - createdInThread: getThreadNumber(addressBase58Encoded), + publicKey: publicKey.base58Encode, + createdInThread: getThreadNumber(address.base58Encode), } as IAccount; } @@ -456,7 +441,7 @@ export class WalletClient extends BaseClient implements IWalletClient { } // get private key - const secretKeyBase58Decoded = this.getBytesSecretKey(signer.secretKey); + const secretKey: SecretKey = new SecretKey(signer.secretKey); // bytes compaction const bytesCompact: Buffer = Buffer.from(data); @@ -464,7 +449,7 @@ export class WalletClient extends BaseClient implements IWalletClient { const messageHashDigest: Uint8Array = hashBlake3(bytesCompact); // sign the digest - const sig = await ed.sign(messageHashDigest, secretKeyBase58Decoded); + const sig = await secretKey.signDigest(messageHashDigest); // check sig length if (sig.length != 64) { @@ -475,13 +460,14 @@ export class WalletClient extends BaseClient implements IWalletClient { // verify signature if (signer.publicKey) { - // get public key - const publicKeyBase58Decoded = this.getBytesPublicKey(signer.publicKey); + const publicKey: PublicKey = await secretKey.getPublicKey(); + const isVerified = await ed.verify( sig, messageHashDigest, - publicKeyBase58Decoded, + publicKey.bytes, ); + if (!isVerified) { throw new Error( `Signature could not be verified with public key. Please inspect`, @@ -489,8 +475,9 @@ export class WalletClient extends BaseClient implements IWalletClient { } } - // convert sig - const base58Encoded = base58Encode(sig); + // convert signature to base58 + const version = Buffer.from(varintEncode(secretKey.version)); + const base58Encoded = base58Encode(Buffer.concat([version, sig])); return { base58Encoded, @@ -512,68 +499,37 @@ export class WalletClient extends BaseClient implements IWalletClient { signerPubKey: string, ): Promise { // setup the public key. - const publicKeyBase58Decoded: Uint8Array = - WalletClient.getBytesPublicKey(signerPubKey); + const publicKey: PublicKey = PublicKey.fromString(signerPubKey); // setup the message digest. const bytesCompact: Buffer = Buffer.from(data); const messageDigest: Uint8Array = hashBlake3(bytesCompact); - // setup the signature. - const signatureBytes: Buffer = base58Decode(signature.base58Encoded); - - // verify signature. - return (await ed.verify( - signatureBytes, - messageDigest, - publicKeyBase58Decoded, - )) as boolean; - } - - /** - * Retrieves the byte representation of a given public key. - * - * @param publicKey - The public key to obtain the bytes from. - * - * @throws If the public key has an incorrect {@link PUBLIC_KEY_PREFIX}. - * - * @returns A Uint8Array containing the bytes of the public key. - */ - public static getBytesPublicKey(publicKey: string): Uint8Array { - if (!(publicKey[0] == PUBLIC_KEY_PREFIX)) { - throw new Error( - `Invalid public key prefix: ${publicKey[0]} should be ${PUBLIC_KEY_PREFIX}`, + try { + // setup the signature. + const versionAndSignatureBytes: Buffer = base58Decode( + signature.base58Encoded, ); - } - const publicKeyVersionBase58Decoded: Buffer = base58Decode( - publicKey.slice(1), - ); - // Version is little for now. - const publicKeyBase58Decoded = publicKeyVersionBase58Decoded.slice(1); - return publicKeyBase58Decoded; - } - /** - * Get the byte representation of a given secret key. - * - * @param secretKey - The secret key to get the bytes from. - * - * @throws if the secret key is not valid. - * - * @returns a Uint8Array containing the bytes of the secret key. - */ - public static getBytesSecretKey(secretKey: string): Uint8Array { - if (!(secretKey[0] == SECRET_KEY_PREFIX)) { - throw new Error( - `Invalid secret key prefix: "${secretKey[0]}". The secret key should start with "${SECRET_KEY_PREFIX}". Please verify your secret key and try again.`, + // removing the version byte + const signatureBytes: Uint8Array = versionAndSignatureBytes.slice(1); + // check sig length + if (signatureBytes.length != 64) { + throw new Error( + `Invalid signature length. Expected 64, got ${signatureBytes.length}`, + ); + } + // verify signature. + const isVerified = await ed.verify( + signatureBytes, + messageDigest, + publicKey.bytes, ); + return isVerified; + } catch (err) { + console.error('Failed to verify signature:', err); + return false; } - const secretKeyVersionBase58Decoded: Buffer = base58Decode( - secretKey.slice(1), - ); - // Version is little for now. - const secretKeyBase58Decoded = secretKeyVersionBase58Decoded.slice(1); - return secretKeyBase58Decoded; } /** @@ -585,14 +541,20 @@ export class WalletClient extends BaseClient implements IWalletClient { * it returns `null`. */ public async getAccountBalance(address: string): Promise { - const addresses: Array = - await this.publicApiClient.getAddresses([address]); - if (addresses.length === 0) return null; - const addressInfo: IAddressInfo = addresses.at(0); - return { - candidate: fromMAS(addressInfo.candidate_balance), - final: fromMAS(addressInfo.final_balance), - } as IBalance; + try { + const addresses: Array = + await this.publicApiClient.getAddresses([address]); + if (addresses.length === 0) return null; + + const addressInfo: IAddressInfo = addresses.at(0); + return { + candidate: fromMAS(addressInfo.candidate_balance), + final: fromMAS(addressInfo.final_balance), + } as IBalance; + } catch (err) { + console.error('Failed to get account balance:', err); + return null; + } } /** @@ -630,11 +592,9 @@ export class WalletClient extends BaseClient implements IWalletClient { ); // sign payload + const bytesPublicKey: Uint8Array = getBytesPublicKey(sender.publicKey); const signature: ISignature = await WalletClient.walletSignMessage( - Buffer.concat([ - WalletClient.getBytesPublicKey(sender.publicKey), - bytesCompact, - ]), + Buffer.concat([bytesPublicKey, bytesCompact]), sender, ); @@ -688,10 +648,7 @@ export class WalletClient extends BaseClient implements IWalletClient { // sign payload const signature: ISignature = await WalletClient.walletSignMessage( - Buffer.concat([ - WalletClient.getBytesPublicKey(sender.publicKey), - bytesCompact, - ]), + Buffer.concat([getBytesPublicKey(sender.publicKey), bytesCompact]), sender, ); @@ -744,10 +701,7 @@ export class WalletClient extends BaseClient implements IWalletClient { // sign payload const signature: ISignature = await WalletClient.walletSignMessage( - Buffer.concat([ - WalletClient.getBytesPublicKey(sender.publicKey), - bytesCompact, - ]), + Buffer.concat([getBytesPublicKey(sender.publicKey), bytesCompact]), sender, ); diff --git a/test/utils/encode_decode.spec.ts b/test/utils/encode_decode.spec.ts new file mode 100644 index 00000000..85a8a76b --- /dev/null +++ b/test/utils/encode_decode.spec.ts @@ -0,0 +1,84 @@ +import { expect, it, describe } from '@jest/globals'; +import { + signedBigIntUtils, + unsignedBigIntUtils, +} from '../../src/utils/encode_decode_int/index'; + +describe('BigInt Serializers Tests', () => { + beforeAll(() => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => null); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('Unsigned BigInt Serializer Tests', () => { + it('should encode and decode positive unsigned BigInt', () => { + const values = [1n, 1111111n, 1111111111111111n]; + + for (let value of values) { + const buffer = unsignedBigIntUtils.encode(value); + const decodedValue = unsignedBigIntUtils.decode(buffer); + expect(decodedValue).toEqual(value); + } + }); + + it('should calculate encoding length for unsigned BigInt', () => { + expect(unsignedBigIntUtils.encodingLength(1111111n)).toEqual(3); + }); + + it('should throw error when encoding negative unsigned BigInt', () => { + expect(() => unsignedBigIntUtils.encode(-1n)).toThrow( + 'value must be unsigned', + ); + }); + + it('should throw error when buffer is too small', () => { + const smallBuffer = new ArrayBuffer(1); + expect(() => unsignedBigIntUtils.encode(1111111n, smallBuffer)).toThrow( + 'the buffer is too small', + ); + }); + + it('should throw error when decoding with offset out of range', () => { + const encodedValue = unsignedBigIntUtils.encode(1111111n); + expect(() => unsignedBigIntUtils.decode(encodedValue, 100)).toThrow( + 'offset out of range', + ); + }); + }); + + describe('Signed BigInt Serializer Tests', () => { + it('should encode and decode positive signed BigInt', () => { + const values = [1n, 1111111n, BigInt(1) ** BigInt(11)]; + + for (let value of values) { + const buffer = signedBigIntUtils.encode(value); + const uint8Array = new Uint8Array(buffer); + const decodedValue = signedBigIntUtils.decode(uint8Array); + expect(decodedValue).toEqual(value); + } + }); + + it('should encode and decode negative signed BigInt', () => { + const values = [-1n, -1111111n, -(BigInt(1) ** BigInt(11))]; + + for (let value of values) { + const buffer = signedBigIntUtils.encode(value); + const uint8Array = new Uint8Array(buffer); + const decodedValue = signedBigIntUtils.decode(uint8Array); + expect(decodedValue).toEqual(value); + } + }); + + it('should calculate encoding length for positive signed BigInt', () => { + expect(signedBigIntUtils.encodingLength(1111111n)).toEqual(4); + }); + + it('should calculate encoding length for negative signed BigInt', () => { + expect(signedBigIntUtils.encodingLength(-1111111n)).toEqual(4); + }); + }); +}); diff --git a/test/utils/keyAndAddresses.spec.ts b/test/utils/keyAndAddresses.spec.ts new file mode 100644 index 00000000..24ad79f7 --- /dev/null +++ b/test/utils/keyAndAddresses.spec.ts @@ -0,0 +1,73 @@ +import { hashBlake3 } from '../../src/utils/Xbqcrypto'; +import { + mockAddressResult, + mockPublicKeyObject, + mockSecretKeyOject, + mockSignatureResult, +} from '../web3/mockData'; +import { SecretKey, PublicKey, Address } from '../../src/utils/keyAndAddresses'; + +describe('SecretKey', () => { + it('should construct SecretKey correctly', async () => { + const secretKey = new SecretKey(mockSecretKeyOject.base58Encoded); + + expect(secretKey.version).toEqual(mockSecretKeyOject.version); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((secretKey as any).bytes).toEqual(mockSecretKeyOject.bytes); + }); + + it('should get the public key correctly', async () => { + const secretKey: SecretKey = new SecretKey( + mockSecretKeyOject.base58Encoded, + ); + const publicKey: PublicKey = await secretKey.getPublicKey(); + + expect(publicKey.version).toEqual(mockPublicKeyObject.version); + expect(publicKey.bytes).toEqual(mockPublicKeyObject.bytes); + expect(publicKey.base58Encode).toEqual(mockPublicKeyObject.base58Encoded); + }); + + it('should sign the digest correctly', async () => { + const data = 'hello world'; + // bytes compaction + const bytesCompact: Buffer = Buffer.from(data); + // Hash byte compact + const messageHashDigest: Uint8Array = hashBlake3(bytesCompact); + + const secretKey: SecretKey = new SecretKey( + mockSecretKeyOject.base58Encoded, + ); + // sign the digest + const sig = await secretKey.signDigest(messageHashDigest); + + expect(sig).toEqual(mockSignatureResult.bytes); + }); +}); + +describe('PublicKey', () => { + it('should construct PublicKey correctly', async () => { + const bytes = mockPublicKeyObject.bytes; + const version = mockPublicKeyObject.version; + const expectedBase58Encode = mockPublicKeyObject.base58Encoded; + + const publicKey = new PublicKey(bytes, version); + + expect(publicKey.bytes).toEqual(bytes); + expect(publicKey.version).toEqual(version); + expect(publicKey.base58Encode).toEqual(expectedBase58Encode); + }); +}); + +describe('Address', () => { + it('should construct Address correctly', async () => { + const secretKeyObject: SecretKey = new SecretKey( + mockSecretKeyOject.base58Encoded, + ); + const publicKeyObject: PublicKey = await secretKeyObject.getPublicKey(); + + const addressObject: Address = new Address(publicKeyObject); + + expect(addressObject.version).toEqual(mockAddressResult.version); + expect(addressObject.base58Encode).toEqual(mockAddressResult.base58Encoded); + }); +}); diff --git a/test/utils/retryExecuteFunction.spec.ts b/test/utils/retryExecuteFunction.spec.ts new file mode 100644 index 00000000..86279945 --- /dev/null +++ b/test/utils/retryExecuteFunction.spec.ts @@ -0,0 +1,72 @@ +import { JSON_RPC_REQUEST_METHOD } from '../../src/interfaces/JsonRpcMethods'; +import { trySafeExecute } from '../../src/utils/retryExecuteFunction'; +import { wait } from '../../src/utils/time'; + +jest.mock('../../src/utils/time'); + +describe('trySafeExecute function', () => { + beforeAll(() => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => null); + }); + + beforeEach(() => { + jest.spyOn(global, 'setTimeout'); + (wait as jest.Mock).mockImplementation((delay: number) => { + return new Promise((resolve) => setTimeout(resolve, delay)); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should execute a function successfully on the first attempt', async () => { + const mockFunc = jest.fn().mockResolvedValue('success'); + const result = await trySafeExecute(mockFunc, [ + JSON_RPC_REQUEST_METHOD.GET_STATUS, + {}, + ]); + expect(result).toEqual('success'); + expect(mockFunc).toHaveBeenCalledTimes(1); + }); + + it('should retry the function upon failure and succeed', async () => { + const mockFunc = jest + .fn() + .mockRejectedValueOnce(new Error('failed')) + .mockResolvedValue('success'); + const result = await trySafeExecute(mockFunc, [ + JSON_RPC_REQUEST_METHOD.GET_STATUS, + {}, + ]); + expect(result).toEqual('success'); + expect(mockFunc).toHaveBeenCalledTimes(2); + }); + + it('should retry the function the correct number of times and then throw an error', async () => { + const mockFunc = jest.fn().mockRejectedValue(new Error('failed')); + await expect( + trySafeExecute(mockFunc, [JSON_RPC_REQUEST_METHOD.GET_STATUS, {}], 3), + ).rejects.toThrow('failed'); + expect(mockFunc).toHaveBeenCalledTimes(3); + }); + + it('should throw an error when no function is provided', async () => { + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + trySafeExecute(null as any, [JSON_RPC_REQUEST_METHOD.GET_STATUS, {}]), + ).rejects.toThrow(`Function execution init conditions are erroneous: null`); + }); + + it('should handle missing args parameter', async () => { + const mockFunc = jest.fn().mockResolvedValue('success'); + const result = await trySafeExecute(mockFunc); + expect(result).toEqual('success'); + expect(mockFunc).toHaveBeenCalledWith(null, {}); + }); +}); diff --git a/test/utils/serializers.spec.ts b/test/utils/serializers.spec.ts index 903c9484..4e7a38e6 100644 --- a/test/utils/serializers.spec.ts +++ b/test/utils/serializers.spec.ts @@ -1,6 +1,40 @@ +import { expect, it, describe } from '@jest/globals'; import * as ser from '../../src/utils/serializers'; import { asTests } from './fixtures/as-serializer'; import { Args, ArrayType } from '../../src/utils/arguments'; +import { + deserializeObj, + getDatatypeSize, + serializableObjectsArrayToBytes, + arrayToBytes, + bytesToArray, + bytesToSerializableObjectArray, +} from '../../src/utils/serializers'; +import { + IDeserializedResult, + ISerializable, +} from '../../src/interfaces/ISerializable'; + +// Implement a simple serializable class for testing. +class TestSerializable implements ISerializable { + value: number; + + constructor(value = 0) { + this.value = value; + } + + serialize(): Uint8Array { + return new Uint8Array([this.value]); + } + + deserialize( + data: Uint8Array, + offset: number, + ): IDeserializedResult { + this.value = data[offset]; + return { instance: this, offset: offset + 1 }; + } +} describe('Serialization tests', () => { it('ser/deser with emojis', () => { @@ -26,30 +60,128 @@ describe('Serialization tests', () => { const val = 123; expect(ser.byteToU8(ser.u8toByte(val))).toEqual(val); }); + it('throws an error when trying to serialize a negative Uint8 value', () => { + const negativeValue = -1; + const args = new Args(); + + expect(() => args.addU8(negativeValue)).toThrow( + `Unable to serialize invalid Uint8 value ${negativeValue}`, + ); + }); + it('throws an error when trying to serialize a Uint8 value greater than 255', () => { + const largeValue = 256; + const args = new Args(); + + expect(() => args.addU8(largeValue)).toThrow( + `Unable to serialize invalid Uint8 value ${largeValue}`, + ); + }); it('ser/deser u32', () => { const val = 666; expect(ser.bytesToU32(ser.u32ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize a negative u32 value', () => { + const negativeValue = -1; + const args = new Args(); + + expect(() => args.addU32(negativeValue)).toThrow( + `Unable to serialize invalid Uint32 value ${negativeValue}`, + ); + }); + it('throws an error when trying to serialize a Uint32 value greater than 4294967295', () => { + const largeValue = 4294967296; + const args = new Args(); + + expect(() => args.addU32(largeValue)).toThrow( + `Unable to serialize invalid Uint32 value ${largeValue}`, + ); + }); it('ser/deser u64', () => { const val = BigInt(666); expect(ser.bytesToU64(ser.u64ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize a negative u64 value', () => { + const negativeValue = BigInt(-1); + const args = new Args(); + + expect(() => args.addU64(negativeValue)).toThrow( + `Unable to serialize invalid Uint64 value ${negativeValue}`, + ); + }); + it('throws an error when trying to serialize a u64 value greater than 18446744073709551615', () => { + const largeValue = BigInt('18446744073709551616'); + const args = new Args(); + + expect(() => args.addU64(largeValue)).toThrow( + `Unable to serialize invalid Uint64 value ${largeValue}`, + ); + }); it('ser/deser u128', () => { const val = 123456789123456789n; expect(ser.bytesToU128(ser.u128ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize a negative u128 value', () => { + const negativeValue = BigInt(-1); + const args = new Args(); + + expect(() => args.addU128(negativeValue)).toThrow( + `Unable to serialize invalid Uint128 value ${negativeValue}`, + ); + }); + it('throws an error when trying to serialize a u128 value greater than 340282366920938463463374607431768211455', () => { + const largeValue = BigInt('340282366920938463463374607431768211456'); + const args = new Args(); + + expect(() => args.addU128(largeValue)).toThrow( + `Unable to serialize invalid Uint128 value ${largeValue}`, + ); + }); it('ser/deser u256', () => { const val = 123456789012345678901234567890n; expect(ser.bytesToU256(ser.u256ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize a negative u256 value', () => { + const negativeValue = BigInt(-1); + const args = new Args(); + + expect(() => args.addU256(negativeValue)).toThrow( + `Unable to serialize invalid Uint256 value ${negativeValue}`, + ); + }); + it('throws an error when trying to serialize a u256 value greater than 115792089237316195423570985008687907853269984665640564039457584007913129639935', () => { + const largeValue = BigInt( + '115792089237316195423570985008687907853269984665640564039457584007913129639936', + ); + const args = new Args(); + + expect(() => args.addU256(largeValue)).toThrow( + `Unable to serialize invalid Uint256 value ${largeValue}`, + ); + }); it('ser/deser i32', () => { const val = -666; expect(ser.bytesToI32(ser.i32ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize an invalid int32 value', () => { + const invalidValue = Math.pow(2, 31); + const args = new Args(); + + expect(() => args.addI32(invalidValue)).toThrow( + `Unable to serialize invalid int32 value ${invalidValue}`, + ); + }); it('ser/deser i64', () => { const val = BigInt(-666); expect(ser.bytesToI64(ser.i64ToBytes(val))).toEqual(val); }); + it('throws an error when trying to serialize an invalid int64 value', () => { + const invalidValue = BigInt('9223372036854775808'); + const args = new Args(); + + expect(() => args.addI64(invalidValue)).toThrow( + `Unable to serialize invalid int64 value ${invalidValue.toString()}`, + ); + }); it('ser/deser f32', () => { const val = -666.666; expect(ser.bytesToF32(ser.f32ToBytes(val))).toBeCloseTo(val, 0.001); @@ -62,6 +194,14 @@ describe('Serialization tests', () => { const val = Number.MAX_VALUE; expect(ser.bytesToF64(ser.f64ToBytes(val))).toEqual(val); }); + it('ser/deser empty string', () => { + const str = ''; + expect(ser.bytesToStr(ser.strToBytes(str))).toEqual(str); + }); + it('ser/deser empty Uint8Array', () => { + const arr = new Uint8Array(0); + expect(ser.bytesToStr(arr)).toEqual(''); + }); }); describe('Test against assemblyscript serializer', () => { @@ -97,3 +237,208 @@ describe('Test against assemblyscript serializer', () => { expect(new Args(serialized).nextArray(ArrayType.STRING)).toEqual(input); }); }); + +describe('array.ts functions', () => { + describe('getDatatypeSize tests', () => { + it('returns the correct size for BOOL', () => { + expect(getDatatypeSize(ArrayType.BOOL)).toEqual(1); + }); + + it('returns the correct size for U8', () => { + expect(getDatatypeSize(ArrayType.U8)).toEqual(1); + }); + + it('returns the correct size for F32', () => { + expect(getDatatypeSize(ArrayType.F32)).toEqual(4); + }); + + it('returns the correct size for I32', () => { + expect(getDatatypeSize(ArrayType.I32)).toEqual(4); + }); + + it('returns the correct size for U32', () => { + expect(getDatatypeSize(ArrayType.U32)).toEqual(4); + }); + + it('returns the correct size for F64', () => { + expect(getDatatypeSize(ArrayType.F64)).toEqual(8); + }); + + it('returns the correct size for I64', () => { + expect(getDatatypeSize(ArrayType.I64)).toEqual(8); + }); + + it('returns the correct size for U64', () => { + expect(getDatatypeSize(ArrayType.U64)).toEqual(8); + }); + + it('returns the correct size for U128', () => { + expect(getDatatypeSize(ArrayType.U128)).toEqual(16); + }); + + it('returns the correct size for U256', () => { + expect(getDatatypeSize(ArrayType.U256)).toEqual(32); + }); + + it('throws an error for unsupported types', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => getDatatypeSize((ArrayType as any).BadType)).toThrow( + 'Unsupported type', + ); + }); + }); + + describe('serializableObjectsArrayToBytes tests', () => { + it('serializes an array of serializable objects to bytes', () => { + const obj1 = new TestSerializable(1); + const obj2 = new TestSerializable(2); + const obj3 = new TestSerializable(3); + + const serialized = serializableObjectsArrayToBytes([obj1, obj2, obj3]); + + expect(serialized).toEqual(new Uint8Array([1, 2, 3])); + }); + }); + + describe('deserializeObj tests', () => { + it('deserializes a bytes array into an instance of the given class', () => { + const data = new Uint8Array([1, 2, 3]); + const result = deserializeObj(data, 0, TestSerializable); + + expect(result.instance).toBeInstanceOf(TestSerializable); + expect(result.instance.value).toEqual(1); + expect(result.offset).toEqual(1); + }); + }); + + describe('bytesToSerializableObjectArray', () => { + it('deserializes a bytes array into an array of instances of the given class', () => { + const data = new Uint8Array([1, 2, 3, 4, 5]); + const result = bytesToSerializableObjectArray(data, TestSerializable); + + expect(result.length).toEqual(5); + // Check that each element in the array is an instance of the TestSerializable class + for (let i = 0; i < result.length; i++) { + expect(result[i]).toBeInstanceOf(TestSerializable); + // Check that the value of each TestSerializable instance is correct + expect(result[i].value).toEqual(i + 1); + } + }); + }); + + describe('arrayToBytes and bytesToArray tests', () => { + it('converts a bool array to bytes and back correctly', () => { + const dataArray = [true, false, true, true, false]; + const byteArray = arrayToBytes(dataArray, ArrayType.BOOL); + const arrayBack = bytesToArray(byteArray, ArrayType.BOOL); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a string array to bytes and back correctly', () => { + const dataArray = ['hello', 'world']; + const byteArray = arrayToBytes(dataArray, ArrayType.STRING); + const arrayBack = bytesToArray(byteArray, ArrayType.STRING); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a U8 array to bytes and back correctly', () => { + const dataArray = [1, 2, 3, 4, 5]; + const byteArray = arrayToBytes(dataArray, ArrayType.U8); + const arrayBack = bytesToArray(byteArray, ArrayType.U8); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a U32 array to bytes and back correctly', () => { + const dataArray = [10, 20, 30, 40, 50]; + const byteArray = arrayToBytes(dataArray, ArrayType.U32); + const arrayBack = bytesToArray(byteArray, ArrayType.U32); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a U64 array to bytes and back correctly', () => { + const dataArray = [ + BigInt(10), + BigInt(20), + BigInt(30), + BigInt(40), + BigInt(50), + ]; + const byteArray = arrayToBytes(dataArray, ArrayType.U64); + const arrayBack = bytesToArray(byteArray, ArrayType.U64); + expect(arrayBack).toEqual(dataArray); + }); + it('converts a U128 array to bytes and back correctly', () => { + const dataArray = [ + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + ]; + + const byteArray = arrayToBytes(dataArray, ArrayType.U128); + const arrayBack = bytesToArray(byteArray, ArrayType.U128); + expect(arrayBack).toEqual(dataArray); + }); + it('converts a U256 array to bytes and back correctly', () => { + const dataArray = [ + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + 123456789123456789n, + ]; + + const byteArray = arrayToBytes(dataArray, ArrayType.U256); + const arrayBack = bytesToArray(byteArray, ArrayType.U256); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a I32 array to bytes and back correctly', () => { + const dataArray = [-10, -20, -30, -40, -50]; + const byteArray = arrayToBytes(dataArray, ArrayType.I32); + const arrayBack = bytesToArray(byteArray, ArrayType.I32); + expect(arrayBack).toEqual(dataArray); + }); + + it('converts a I64 array to bytes and back correctly', () => { + const dataArray = [ + BigInt(-10), + BigInt(-20), + BigInt(-30), + BigInt(-40), + BigInt(-50), + ]; + const byteArray = arrayToBytes(dataArray, ArrayType.I64); + const arrayBack = bytesToArray(byteArray, ArrayType.I64); + expect(arrayBack).toEqual(dataArray); + }); + it('converts a F32 array to bytes and back correctly', () => { + const dataArray = [1.1, 2.2, 3.3, 4.4, 5.5]; + const byteArray = arrayToBytes(dataArray, ArrayType.F32); + const arrayBack = bytesToArray(byteArray, ArrayType.F32); + + arrayBack.forEach((value, index) => { + expect(value).toBeCloseTo(dataArray[index], 5); // 5 is the precision (number of digits after the decimal point) + }); + }); + it('converts a F64 array to bytes and back correctly', () => { + const dataArray = [1.1, 2.2, 3.3, 4.4, 5.5]; + const byteArray = arrayToBytes(dataArray, ArrayType.F64); + const arrayBack = bytesToArray(byteArray, ArrayType.F64); + + arrayBack.forEach((value, index) => { + expect(value).toBeCloseTo(dataArray[index], 5); // 5 is the precision (number of digits after the decimal point) + }); + }); + it('throws an error when an unsupported type is used', () => { + const dataArray = [1, 2, 3, 4, 5]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const unsupportedType = 'someUnsupportedType' as any; + + expect(() => arrayToBytes(dataArray, unsupportedType)).toThrow( + `Unsupported type: ${unsupportedType}`, + ); + }); + }); +}); diff --git a/test/utils/time.spec.ts b/test/utils/time.spec.ts new file mode 100644 index 00000000..a366cad5 --- /dev/null +++ b/test/utils/time.spec.ts @@ -0,0 +1,93 @@ +import { + Timeout, + Interval, + wait, + withTimeoutRejection, +} from '../../src/utils/time'; + +describe('Timer utilities', () => { + jest.useFakeTimers(); + + beforeAll(() => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => null); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('Timeout class', () => { + it('should call the callback after the specified timeout', () => { + const callback = jest.fn(); + void new Timeout(1000, callback); + + expect(callback).not.toBeCalled(); + jest.advanceTimersByTime(1000); + expect(callback).toBeCalled(); + }); + + it('should not call the callback if clear is called before the timeout', () => { + const callback = jest.fn(); + const timeout = new Timeout(1000, callback); + jest.advanceTimersByTime(500); + timeout.clear(); + + expect(callback).not.toBeCalled(); + }); + }); + + describe('Interval class', () => { + it('should call the callback every interval', () => { + const callback = jest.fn(); + void new Interval(1000, callback); + + jest.advanceTimersByTime(3000); + expect(callback).toBeCalledTimes(3); + }); + + it('should stop calling the callback if clear is called', () => { + const callback = jest.fn(); + const interval = new Interval(1000, callback); + interval.clear(); + + jest.advanceTimersByTime(3000); + expect(callback).not.toBeCalled(); + }); + }); + + describe('wait function', () => { + it('should resolve after the specified time', async () => { + const promise = wait(1000); + jest.advanceTimersByTime(1000); + + await expect(promise).resolves.toBeUndefined(); + }); + }); + + describe('withTimeoutRejection function', () => { + it('should resolve with the value of the promise if it resolves before the timeout', async () => { + const promise = Promise.resolve('success'); + const result = withTimeoutRejection(promise, 1000); + jest.advanceTimersByTime(500); + + await expect(result).resolves.toBe('success'); + }); + + it('should reject if the promise does not resolve before the timeout', async () => { + const promise = new Promise((resolve) => + setTimeout(resolve, 2000, 'success'), + ); + const result = withTimeoutRejection(promise, 1000); + jest.advanceTimersByTime(1000); + + await expect(result).rejects.toThrow( + `Timeout of 1000 has passed and promise did not resolve`, + ); + }); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); +}); diff --git a/test/web3/Client.spec.ts b/test/web3/Client.spec.ts new file mode 100644 index 00000000..577ca24a --- /dev/null +++ b/test/web3/Client.spec.ts @@ -0,0 +1,74 @@ +import { Client } from '../../src/web3/Client'; +import { IClientConfig } from '../../src/interfaces/IClientConfig'; +import { IProvider } from '../../src/interfaces/IProvider'; +import { ProviderType } from '../../src/interfaces/IProvider'; +import { DefaultProviderUrls } from '../../src/web3/ClientFactory'; + +describe('Client Class', () => { + let clientConfig: IClientConfig; + let client: Client; + + beforeEach(() => { + clientConfig = { + providers: [ + { url: 'https://mock-public-api', type: ProviderType.PUBLIC }, + { url: 'https://mock-private-api', type: ProviderType.PRIVATE }, + ], + periodOffset: 0, + }; + client = new Client(clientConfig); + }); + + test('should return the wallet client', () => { + const walletClient = client.wallet(); + expect(walletClient).toBeTruthy(); + }); + + test('should return the smart contracts client', () => { + const smartContractsClient = client.smartContracts(); + expect(smartContractsClient).toBeTruthy(); + }); + + test('should set custom providers', () => { + const newProviders: Array = [ + { url: 'https://mock-public-api', type: ProviderType.PUBLIC }, + { url: 'https://mock-private-api', type: ProviderType.PRIVATE }, + ]; + client.setCustomProviders(newProviders); + + const currentProviders = client.getProviders(); + expect(currentProviders).toEqual(newProviders); + }); + + test('should return public providers', () => { + const publicProviders = client.getPublicProviders(); + expect(publicProviders).toHaveLength(1); + expect(publicProviders[0].type).toEqual(ProviderType.PUBLIC); + }); + + test('should return private providers', () => { + const privateProviders = client.getPrivateProviders(); + expect(privateProviders).toHaveLength(1); + expect(privateProviders[0].type).toEqual(ProviderType.PRIVATE); + }); + + test('should set new default provider', () => { + const newDefaultProvider = 'https://new-default-provider.com'; + client.setNewDefaultProvider(newDefaultProvider as DefaultProviderUrls); + + const currentProviders = client.getProviders(); + expect(currentProviders).toHaveLength(2); + expect(currentProviders[0].url).toBe(newDefaultProvider); + expect(currentProviders[1].url).toBe(newDefaultProvider); + }); + + test('should return the private api client', () => { + const privateApiClient = client.privateApi(); + expect(privateApiClient).toBeTruthy(); + }); + + test('should return the public api client', () => { + const publicApiClient = client.publicApi(); + expect(publicApiClient).toBeTruthy(); + }); +}); diff --git a/test/web3/eventPoller.spec.ts b/test/web3/eventPoller.spec.ts new file mode 100644 index 00000000..096c38df --- /dev/null +++ b/test/web3/eventPoller.spec.ts @@ -0,0 +1,202 @@ +import { + EventPoller, + ON_MASSA_EVENT_DATA, + ON_MASSA_EVENT_ERROR, +} from '../../src/web3/EventPoller'; +import { IEventFilter } from '../../src/interfaces/IEventFilter'; +import { IEventRegexFilter } from '../../src/interfaces/IEventRegexFilter'; +import { IEvent } from '../../src/interfaces/IEvent'; +import { ISlot } from '../../src/interfaces/ISlot'; +import { Client } from '../../src/web3/Client'; +import { WalletClient } from '../../src/web3/WalletClient'; +import { + ClientFactory, + DefaultProviderUrls, +} from '../../src/web3/ClientFactory'; +import { IAccount } from '../../src/interfaces/IAccount'; +import { Timeout } from '../../src/utils/time'; + +// Mock the Timeout class +jest.mock('../../src/utils/time', () => { + function Timeout(timeoutMil, callback) { + this.isCleared = false; + this.isCalled = false; + this.timeoutHook = setTimeout(() => { + if (!this.isCleared) { + this.isCalled = true; + callback(); + } + }, timeoutMil); + this.clear = function () { + if (!this.isCleared) { + clearTimeout(this.timeoutHook); + this.isCleared = true; + } + }; + } + + return { + Timeout: jest.fn(Timeout), + }; +}); + +const mockedEvents: IEvent[] = [ + { + id: 'event1', + data: '{"key1": "value1"}', + context: { + slot: { + period: 1, + thread: 1, + }, + block: null, + read_only: false, + call_stack: ['address1'], + index_in_slot: 0, + origin_operation_id: null, + is_final: true, + is_error: false, + }, + }, + { + id: 'event2', + data: '{"key2": "value2"}', + context: { + slot: { + period: 2, + thread: 2, + }, + block: null, + read_only: false, + call_stack: ['address2'], + index_in_slot: 0, + origin_operation_id: null, + is_final: true, + is_error: false, + }, + }, + { + id: 'event3', + data: '{"key3": "value3"}', + context: { + slot: { + period: 3, + thread: 3, + }, + block: null, + read_only: false, + call_stack: ['address3'], + index_in_slot: 0, + origin_operation_id: null, + is_final: true, + is_error: false, + }, + }, +]; + +describe('EventPoller', () => { + let eventPoller: EventPoller; + let baseAccount: IAccount; + let web3Client: Client; + + const pollIntervalMillis = 1000; + const eventFilter: IEventFilter | IEventRegexFilter = { + start: null, + end: null, + emitter_address: null, + original_caller_address: null, + original_operation_id: null, + is_final: null, + }; + + beforeAll(async () => { + baseAccount = await WalletClient.walletGenerateNewAccount(); + const provider = DefaultProviderUrls.TESTNET; + web3Client = await ClientFactory.createDefaultClient( + provider, + true, + baseAccount, + ); + }); + + beforeEach(async () => { + eventPoller = new EventPoller(eventFilter, pollIntervalMillis, web3Client); + }); + + afterEach(() => { + eventPoller.stopPolling(); + jest.clearAllMocks(); + }); + + test('should poll for events, filter them, sort them, and emit ON_MASSA_EVENT_DATA event', async () => { + // Mock the getFilteredScOutputEvents method to return the mocked events + jest + .spyOn(web3Client.smartContracts(), 'getFilteredScOutputEvents') + .mockResolvedValue(mockedEvents); + jest.spyOn(eventPoller, 'emit'); + + // Set lastSlot to simulate an existing value + eventPoller['lastSlot'] = { period: 1, thread: 1 } as ISlot; + + await eventPoller['callback'](); + + // Verify the correct methods were called with the correct arguments + expect( + web3Client.smartContracts().getFilteredScOutputEvents, + ).toHaveBeenCalledWith(eventFilter); + expect(eventPoller.emit).toHaveBeenCalledWith( + ON_MASSA_EVENT_DATA, + mockedEvents.slice(1), // The events with a slot after { period: 1, thread: 1 } + ); + expect(eventPoller['lastSlot']).toEqual( + mockedEvents[mockedEvents.length - 1].context.slot, + ); + }); + + test('should emit ON_MASSA_EVENT_ERROR event if an error occurs', async () => { + const errorMessage = 'An error occurred'; + // Mock the getFilteredScOutputEvents method to throw an error + jest + .spyOn(web3Client.smartContracts(), 'getFilteredScOutputEvents') + .mockRejectedValue(new Error(errorMessage)); + jest.spyOn(eventPoller, 'emit'); + + await eventPoller['callback'](); + // Verify the error event was emitted + expect(eventPoller.emit).toHaveBeenCalledWith( + ON_MASSA_EVENT_ERROR, + new Error(errorMessage), + ); + }); + + test('should reset the interval and call callback again after the specified poll interval', async () => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => null); + + // Set lastSlot to simulate an existing value + eventPoller['lastSlot'] = { period: 1, thread: 1 } as ISlot; + + // Replacing setTimeout with jest's fake timers + jest.useFakeTimers(); + await eventPoller['callback'](); + + // Assert that setTimeout was called once and with the correct arguments + expect(Timeout).toHaveBeenCalledTimes(1); + expect(Timeout).toHaveBeenCalledWith( + pollIntervalMillis, + expect.any(Function), + ); + + // Fast-forward the timer and assert callback is called again + jest.runOnlyPendingTimers(); + + // Assert that getFilteredScOutputEvents was called twice + expect( + web3Client.smartContracts().getFilteredScOutputEvents, + ).toHaveBeenCalledTimes(2); + + jest.useRealTimers(); + + consoleSpy.mockRestore(); + }); +}); diff --git a/test/web3/mockData.ts b/test/web3/mockData.ts new file mode 100644 index 00000000..8625a826 --- /dev/null +++ b/test/web3/mockData.ts @@ -0,0 +1,596 @@ +/* + This file contains mock data for testing purposes. +*/ + +import { IClientConfig } from '../../src/interfaces/IClientConfig'; +import { ProviderType, IProvider } from '../../src/interfaces/IProvider'; +import { PERIOD_OFFSET } from '../../src/web3/BaseClient'; +import { IAccount } from '../../src/interfaces/IAccount'; +import { IContractData } from '../../src/interfaces/IContractData'; +import { ICallData } from '../../src/interfaces/ICallData'; +import { IGetGraphInterval } from '../../src/interfaces/IGetGraphInterval'; +import { IReadData } from '../../src/interfaces/IReadData'; +import { IContractReadOperationData } from '../../src/interfaces/IContractReadOperationData'; +import { IContractReadOperationResponse } from '../../src/interfaces/IContractReadOperationResponse'; +import { IEvent } from '../../src/interfaces/IEvent'; +import { ISlot } from '../../src/interfaces/ISlot'; +import { IEventFilter } from '../../src/interfaces/IEventFilter'; +import { IEventRegexFilter } from '../../src/interfaces/IEventRegexFilter'; +import { IExecuteReadOnlyResponse } from '../../src/interfaces/IExecuteReadOnlyResponse'; +import { ISignature } from '../../src/interfaces/ISignature'; +import { IOperationData } from '../../src/interfaces/IOperationData'; +import { INodeStatus } from '../../src/interfaces/INodeStatus'; +import { IEndorsement } from '../../src/interfaces/IEndorsement'; + +// util function to create an event, only for that test file to avoid code duplication +function createEvent( + id: string, + data: string, + slot: ISlot, + callStack: string[], +): IEvent { + return { + id, + data: JSON.stringify(data), + context: { + slot, + block: null, + read_only: false, + call_stack: callStack, + index_in_slot: 0, + origin_operation_id: null, + is_final: true, + is_error: false, + }, + }; +} + +export const mockNodeStatusInfo: INodeStatus = { + node_id: 'N129tbNd4oVMRsnFvQcgSq4PUAZYYDA1pvqtef2ER6W7JqgY1Bfg', + node_ip: null, + version: 'SAND.23.0', + current_time: 1687275917301, + current_cycle: 6, + connected_nodes: {}, + last_slot: { period: 830, thread: 1 }, + next_slot: { period: 830, thread: 2 }, + consensus_stats: { + start_timestamp: 1687275857301, + end_timestamp: 1687275917301, + final_block_count: 120, + final_operation_count: 1296, + staker_count: 1, + stale_block_count: 0, + clique_count: 1, + }, + pool_stats: { endorsement_count: 0, operation_count: 1296 }, + network_stats: { + in_connection_count: 0, + out_connection_count: 0, + known_peer_count: 0, + banned_peer_count: 0, + active_node_count: 0, + }, + config: { + genesis_timestamp: 1687262636363, + end_timestamp: null, + thread_count: 32, + t0: 16000, + delta_f0: 1088, + operation_validity_periods: 10, + periods_per_cycle: 128, + block_reward: '0.30', + roll_price: '100', + max_block_size: 1000000, + pos_lock_cycles: 64, + pos_lookback_cycles: 64, + }, +}; + +export const mockGraphInterval: IGetGraphInterval = { + start: 1624153200000, + end: 1624156800000, +}; + +export const mockBlock = { + header: { + content: { + slot: { period: 830, thread: 1 }, + parents: ['0x000'], + operation_merkle_root: '0x000', + endorsements: [], + }, + signature: '0x000', + creator_public_key: '0x000', + creator_address: '0x000', + id: '0x000', + }, +}; + +export const mockAddresses: string[] = [ + 'AU1qx8SWRBX3EaLLWmcviYiQqS7zb4jV4QykHt2TskjTPJbQAHF7', + 'AU1mTRrw6vVY2ehJTpL2PzHewP5iS1kGV2jhh3P9gNtLRxj4Z2fp', + 'AU12WVAJoH2giHAjSxk9R1XK3YhpCw2QxmkCbtXxcr4T3XCUG55nr', +]; + +export const mockAddressesInfo = [ + { + address: 'AU1qx8SWRBX3EaLLWmcviYiQqS7zb4jV4QykHt2TskjTPJbQAHF7', + candidate_balance: '0', + final_balance: '0', + thread: 1, + }, + { + address: 'AU1mTRrw6vVY2ehJTpL2PzHewP5iS1kGV2jhh3P9gNtLRxj4Z2fp', + candidate_balance: '1', + final_balance: '1', + thread: 2, + }, + { + address: 'AU12WVAJoH2giHAjSxk9R1XK3YhpCw2QxmkCbtXxcr4T3XCUG55nr', + candidate_balance: '50', + final_balance: '50', + thread: 3, + }, +]; + +export const mockBlockIds = ['0x000', '0x001']; + +export const mockBlockData = [ + { + id: mockBlockIds[0], + content: null, + is_final: false, + is_in_blockclique: false, + is_stale: false, + }, + { + id: mockBlockIds[1], + content: null, + is_final: true, + is_in_blockclique: false, + is_stale: false, + }, +]; + +export const mockEndorsementIds = ['0x000', '0x001']; + +export const mockEndorsementData: Array = [ + { + id: mockEndorsementIds[0], + in_pool: false, + in_blocks: ['0x000'], + is_final: false, + endorsement: { + content: { + sender_public_key: '0x000', + slot: { period: 830, thread: 1 }, + index: 0, + endorsed_block: '0x000', + }, + signature: '0x000', + }, + }, + { + id: mockEndorsementIds[1], + in_pool: false, + in_blocks: ['0x001'], + is_final: true, + endorsement: { + content: { + sender_public_key: '0x000', + slot: { period: 830, thread: 1 }, + index: 0, + endorsed_block: '0x000', + }, + signature: '0x000', + }, + }, +]; + +export const mockOpIds: Array = [ + 'O1z2xVtwFsKP3po3vkPmpEtZiJvwEd4v1hpK7iT8P3rk9zCEs9f', + 'O1s1xVtwFsKP3po3vkPmpELsiJvwEdk0yhpK7iT8P3rk9zCEs9g', + 'O1b4xVtwFsKP3po3vkPmpEjZiJvwEdk6yhpK7iT8P3rk9zCEs9h', + 'O1t2xVtwFsKP3po3vkPmpELZiJvwEd2vy3pK7iT8P3rk9zCEs9i', +]; + +export const mockOperationData = [ + { + id: mockOpIds[0], + in_blocks: ['0x000'], + in_pool: false, + is_operation_final: false, + thread: 1, + operation: {}, + }, + { + id: mockOpIds[1], + in_blocks: ['0x001'], + in_pool: false, + is_operation_final: true, + thread: 2, + operation: {}, + }, + { + id: mockOpIds[2], + in_blocks: ['0x002'], + in_pool: true, + is_operation_final: false, + thread: 3, + operation: {}, + }, + { + id: mockOpIds[3], + in_blocks: ['0x003'], + in_pool: false, + is_operation_final: false, + thread: 4, + operation: {}, + }, +]; + +export const mockOperationDataDetailed: Array = [ + { + id: mockOperationData[0].id, + in_blocks: ['0x000'], + in_pool: false, + is_operation_final: false, + thread: 1, + operation: { + content: { + expire_period: 0, + fee: '0', + op: { + Transaction: { + amount: '1000', + recipient_address: + 'AU1QRRX6o2igWogY8qbBtqLYsNzYNHwvnpMC48Y6CLCv4cXe9gmK', + }, + }, + sender_public_key: 'public_key', + }, + signature: 'signature', + content_creator_pub_key: 'pub_key', + content_creator_address: + 'AU1fMUjzAR6Big9Woz3P3vTjAywLbb9KwSyC8btfK3KMDj8ffAHu', + id: 'id', + }, + op_exec_status: true, + }, + { + id: mockOperationData[1].id, + in_blocks: ['2'], + in_pool: false, + is_operation_final: true, + thread: 2, + operation: { + content: { + expire_period: 0, + fee: '0', + op: { + Transaction: { + amount: '1000', + recipient_address: + 'AU1QRRX6o2igWogY8qbBtqLYsNzYNHwvnpMC48Y6CLCv4cXe9gmK', + }, + }, + sender_public_key: 'public_key', + }, + signature: 'signature', + content_creator_pub_key: 'pub_key', + content_creator_address: + 'AU1fMUjzAR6Big9Woz3P3vTjAywLbb9KwSyC8btfK3KMDj8ffAHu', + id: 'id', + }, + op_exec_status: false, + }, + { + id: mockOperationData[2].id, + in_blocks: [], + in_pool: true, + is_operation_final: false, + thread: 3, + operation: { + content: { + expire_period: 0, + fee: '0', + op: { + Transaction: { + amount: '1000', + recipient_address: + 'AU1fMUjzAR6Big9Woz3P3vTjAywLbb9KwSyC8btfK3KMDj8ffAHu', + }, + }, + sender_public_key: 'public_key', + }, + signature: 'signature', + content_creator_pub_key: 'pub_key', + content_creator_address: + 'AU12Set6aygzt1k7ZkDwrkStYovVBzeGs8VgaZogy11s7fQzaytv3', + id: 'id', + }, + op_exec_status: true, + }, + { + id: mockOperationData[3].id, + in_blocks: [], + in_pool: false, + is_operation_final: false, + thread: 4, + operation: { + content: { + expire_period: 0, + fee: '0', + op: { + Transaction: { + amount: '1000', + recipient_address: + 'AU1fMUjzAR6Big9Woz3P3vTjAywLbb9KwSyC8btfK3KMDj8ffAHu', + }, + }, + sender_public_key: 'public_key', + }, + signature: 'signature', + content_creator_pub_key: 'pub_key', + content_creator_address: + 'AU12Set6aygzt1k7ZkDwrkStYovVBzeGs8VgaZogy11s7fQzaytv3', + id: 'id', + }, + op_exec_status: true, + }, +]; + +export const mockStackersData = [ + { + AU1qx8SWRBX3EaLLWmcviYiQqS7zb4jV4QykHt2TskjTPJbQAHF7: 1, + AU1mTRrw6vVY2ehJTpL2PzHewP5iS1kGV2jhh3P9gNtLRxj4Z2fp: 2, + AU12WVAJoH2giHAjSxk9R1XK3YhpCw2QxmkCbtXxcr4T3XCUG55nr: 3, + }, +]; + +export const mockDatastoreEntryInput = [ + { + address: 'AU1qx8SWRBX3EaLLWmcviYiQqS7zb4jV4QykHt2TskjTPJbQAHF7', + key: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), + }, + { + address: 'AU1mTRrw6vVY2ehJTpL2PzHewP5iS1kGV2jhh3P9gNtLRxj4Z2fp', + key: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]), + }, +]; + +export const mockDatastoreEntries = [ + { + final_value: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), + candidate_value: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), + }, + { + final_value: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]), + candidate_value: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]), + }, +]; + +export const mockClientConfig: IClientConfig = { + providers: [ + { + url: 'https://mock-public-api.com', + type: ProviderType.PUBLIC, + } as IProvider, + { + url: 'https://mock-private-api.com', + type: ProviderType.PRIVATE, + } as IProvider, + ], + periodOffset: PERIOD_OFFSET, +}; + +export const mockDeployerAccount: IAccount = { + address: 'AU1QRRX6o2igWogY8qbBtqLYsNzYNHwvnpMC48Y6CLCv4cXe9gmK', + publicKey: 'P129tbNd4oVMRsnFvQcgSq4PUAZYYDA1pvqtef2ER6W7JqgY1Bfg', + secretKey: 'S12XuWmm5jULpJGXBnkeBsuiNmsGi2F4rMiTvriCzENxBR4Ev7vd', + createdInThread: 0, +}; + +export const mockContractData: IContractData = { + fee: 100000000000000000n, + maxGas: 100000000000000000n, + maxCoins: 100000000000000000n, + address: 'AU1fMUjzAR6Big9Woz3P3vTjAywLbb9KwSyC8btfK3KMDj8ffAHu', + contractDataText: 'Hello World!', + contractDataBinary: new Uint8Array([0x00, 0x01, 0x02, 0x03]), + datastore: new Map([ + [ + new Uint8Array([0x00, 0x01, 0x02, 0x03]), + new Uint8Array([0x00, 0x01, 0x02, 0x03]), + ], + [ + new Uint8Array([0x04, 0x05, 0x06, 0x07]), + new Uint8Array([0x04, 0x05, 0x06, 0x07]), + ], + [ + new Uint8Array([0x08, 0x09, 0x0a, 0x0b]), + new Uint8Array([0x08, 0x09, 0x0a, 0x0b]), + ], + [ + new Uint8Array([0x0c, 0x0d, 0x0e, 0x0f]), + new Uint8Array([0x0c, 0x0d, 0x0e, 0x0f]), + ], + ]), +}; + +export const mockCallData: ICallData = { + fee: 100000000000000000n, + maxGas: 100000000000000000n, + coins: 100000000000000000n, + targetAddress: 'AS12sRd6E6zKdBx3PGeZpCUUM8sE5oSA5mTa3VV4AoDCoqpoxwkmu', + functionName: 'test', + parameter: [1, 2, 3, 4], +}; + +export const mockedEvents: IEvent[] = [ + createEvent('event1', 'value1', { period: 1, thread: 1 }, ['address1']), // n°1 + createEvent('event2', 'value2', { period: 2, thread: 1 }, ['address2']), // n°3 + createEvent('event3', 'value3', { period: 1, thread: 2 }, ['address3']), // n°2 + createEvent('event5', 'value5', { period: 2, thread: 2 }, ['address4']), // n°4 + createEvent('event4', 'value4', { period: 1, thread: 2 }, ['address4']), // n°2 + createEvent('event6', 'value6', { period: 3, thread: 2 }, ['address4']), // n°5 +]; + +export const mockEventFilter: IEventFilter | IEventRegexFilter = { + start: { period: 2, thread: 1 }, + end: { period: 3, thread: 2 }, + emitter_address: 'address4', + original_caller_address: null, + original_operation_id: null, + is_final: null, +}; +export const mockReadData: IReadData = { + maxGas: 100000n, + targetAddress: 'AS12sRd6E6zKdBx3PGeZpCUUM8sE5oSA5mTa3VV4AoDCoqpoxwkmu', + targetFunction: 'test', + parameter: [1, 2, 3, 4], + callerAddress: 'AU1QRRX6o2igWogY8qbBtqLYsNzYNHwvnpMC48Y6CLCv4cXe9gmK', +}; + +export const mockContractReadOperationData: Array = + [ + { + executed_at: { + period: 1, + thread: 0, + }, + result: { + Ok: new Uint8Array([0x00, 0x01, 0x02, 0x03]), + }, + output_events: [ + { + id: 'O1z2xVtwFsKP3po3vkPmpELZiJvwEdkvyhpK7iT8P3rk9zCEs9f', + data: 'Hello World!', + context: { + slot: { + period: 1, + thread: 0, + }, + block: null, + read_only: true, + call_stack: [], + index_in_slot: 0, + origin_operation_id: null, + is_final: true, + is_error: false, + }, + }, + ], + gas_cost: 1000000, + }, + ]; + +export const mockContractReadOperationResponse: IContractReadOperationResponse = + { + returnValue: new Uint8Array([0x00, 0x01, 0x02, 0x03]), + info: mockContractReadOperationData[0], + }; + +export const mockContractReadOnlyOperationResponse: IExecuteReadOnlyResponse = { + returnValue: new Uint8Array([0x00, 0x01, 0x02, 0x03]), + info: mockContractReadOperationData[0], +}; + +export const validSignature: ISignature = { + base58Encoded: + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE', +}; + +export const mockContractReadOperationDataWithError = [ + { + result: { + Error: 'Some error message. Inspect smart contract for more details', + }, + }, +]; + +export const mockSecretKeyOject = { + base58Encoded: 'S1eK3SEXGDAWN6pZhdr4Q7WJv6UHss55EB14hPy4XqBpiktfPu6', + bytes: Buffer.from( + '54b8270c454be4dd5adb9f75eea6492c0d9a61d23d839c9924100622110f5a2f', + 'hex', + ), + version: 0, +}; + +export const mockPublicKeyObject = { + base58Encoded: 'P121uDTpo58d3SxQTENXKqSJTpB21ueSAy8RqQ2virGVeWs339ub', + bytes: Uint8Array.from([ + 133, 189, 128, 53, 164, 216, 224, 156, 184, 1, 149, 251, 189, 7, 11, 151, + 51, 176, 124, 204, 229, 184, 245, 144, 208, 126, 241, 70, 210, 29, 122, 105, + ]), + version: 0, +}; + +export const mockSignatureResult = { + bytes: Uint8Array.from([ + 226, 197, 65, 223, 9, 206, 158, 120, 130, 222, 17, 217, 123, 27, 195, 26, + 237, 44, 219, 157, 85, 238, 191, 144, 250, 133, 142, 70, 170, 207, 92, 157, + 211, 42, 128, 159, 61, 214, 186, 133, 154, 100, 82, 251, 44, 56, 232, 24, + 203, 7, 121, 84, 78, 50, 205, 62, 146, 193, 4, 221, 73, 223, 226, 0, + ]), +}; + +export const mockAddressResult = { + version: 0, + base58Encoded: 'AU12Set6aygzt1k7ZkDwrkStYovVBzeGs8VgaZogy11s7fQzaytv3', +}; + +export const mockResultSendJsonRPCRequestWalletInfo = [ + { + address: 'AU1QRRX6o2igWogY8qbBtqLYsNzYNHwvnpMC48Y6CLCv4cXe9gmK', + thread: 6, + final_balance: '500', + final_roll_count: 0, + final_datastore_keys: [], + candidate_balance: '500', + candidate_roll_count: 0, + candidate_datastore_keys: [], + deferred_credits: [], + next_block_draws: [], + next_endorsement_draws: [], + created_blocks: [], + created_operations: [], + created_endorsements: [], + cycle_infos: [ + [Object], + [Object], + [Object], + [Object], + [Object], + [Object], + [Object], + ], + }, + { + address: 'AU12eqpwwPeVxt2riAZZv2QMyFJ7qnZwBmATFRU3JKjkXvtupLHDS', + thread: 27, + final_balance: '0', + final_roll_count: 0, + final_datastore_keys: [], + candidate_balance: '0', + candidate_roll_count: 0, + candidate_datastore_keys: [], + deferred_credits: [], + next_block_draws: [], + next_endorsement_draws: [], + created_blocks: [], + created_operations: [], + created_endorsements: [], + cycle_infos: [ + [Object], + [Object], + [Object], + [Object], + [Object], + [Object], + [Object], + ], + }, +]; diff --git a/test/web3/privateApiClient.spec.ts b/test/web3/privateApiClient.spec.ts new file mode 100644 index 00000000..7cf9b6e8 --- /dev/null +++ b/test/web3/privateApiClient.spec.ts @@ -0,0 +1,206 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { JSON_RPC_REQUEST_METHOD } from '../../src/interfaces/JsonRpcMethods'; +import { IClientConfig } from '../../src/interfaces/IClientConfig'; +import { ProviderType, IProvider } from '../../src/interfaces/IProvider'; +import { PrivateApiClient } from '../../src'; + +export const PERIOD_OFFSET = 5; + +describe('PrivateApiClient', () => { + let client: PrivateApiClient; + let mockSendJsonRPCRequest: jest.SpyInstance; + let ipAddress = '192.168.0.1'; + + const getRpcArgs = (wrapArgsInArray, mockData) => { + const argsInArray = Array.isArray(mockData) ? mockData : [mockData]; + const rpcArgs = wrapArgsInArray ? [argsInArray] : argsInArray; + return rpcArgs; + }; + + // Function to generate tests for a set of similar operations to avoid code duplication + function generateAPITests( + operation: string, + mockData: any, + mockResponse: any, + jsonRpcRequestMethod: JSON_RPC_REQUEST_METHOD, + wrapArgsInArray = true, + ) { + test(`should call sendJsonRPCRequest with correct arguments`, async () => { + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + await (client as any)[operation](mockData); + + const rpcArgs = getRpcArgs(wrapArgsInArray, mockData); + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + jsonRpcRequestMethod, + rpcArgs, + ); + }); + + test(`should return the correct result`, async () => { + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + const result = await (client as any)[operation](mockData); + + expect(result).toEqual(mockResponse); + }); + + test(`should handle errors correctly`, async () => { + const mockError = new Error('Error message'); + mockSendJsonRPCRequest.mockRejectedValue(mockError); + + await expect((client as any)[operation](mockData)).rejects.toThrow( + mockError, + ); + }); + + test(`should call trySafeExecute if retryStrategyOn is true`, async () => { + // Enable retry strategy + const originalRetryStrategy = (client as any).clientConfig + .retryStrategyOn; + (client as any).clientConfig.retryStrategyOn = true; + + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + const result = await (client as any)[operation](mockData); + + const rpcArgs = getRpcArgs(wrapArgsInArray, mockData); + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + jsonRpcRequestMethod, + rpcArgs, + ); + + expect(result).toEqual(mockResponse); + + // Restore retry strategy + (client as any).clientConfig.retryStrategyOn = originalRetryStrategy; + }); + } + + beforeEach(() => { + const clientConfig: IClientConfig = { + providers: [ + { + url: 'https://mock-public-api.com', + type: ProviderType.PUBLIC, + } as IProvider, + { + url: 'https://mock-private-api.com', + type: ProviderType.PRIVATE, + } as IProvider, + ], + periodOffset: PERIOD_OFFSET, + }; + client = new PrivateApiClient(clientConfig); + mockSendJsonRPCRequest = jest.spyOn(client as any, 'sendJsonRPCRequest'); + }); + + describe('nodeAddToPeersWhitelist', () => { + generateAPITests( + 'nodeAddToPeersWhitelist', + ipAddress, + undefined, + JSON_RPC_REQUEST_METHOD.NODE_ADD_TO_PEERS_WHITELIST, + ); + }); + + describe('nodeRemoveFromWhitelist', () => { + generateAPITests( + 'nodeRemoveFromWhitelist', + ipAddress, + undefined, + JSON_RPC_REQUEST_METHOD.NODE_REMOVE_FROM_WHITELIST, + ); + }); + + describe('nodeUnbanByIpAddress', () => { + generateAPITests( + 'nodeUnbanByIpAddress', + ipAddress, + undefined, + JSON_RPC_REQUEST_METHOD.NODE_UNBAN_BY_IP, + ); + }); + + describe('nodeUnbanById', () => { + generateAPITests( + 'nodeUnbanById', + 'nodeIdExample', + undefined, + JSON_RPC_REQUEST_METHOD.NODE_UNBAN_BY_ID, + true, + ); + }); + + describe('nodeBanByIpAddress', () => { + generateAPITests( + 'nodeBanByIpAddress', + '127.0.0.1', + undefined, + JSON_RPC_REQUEST_METHOD.NODE_BAN_BY_IP, + true, + ); + }); + + describe('nodeBanById', () => { + generateAPITests( + 'nodeBanById', + 'nodeIdExample', + undefined, + JSON_RPC_REQUEST_METHOD.NODE_BAN_BY_ID, + true, + ); + }); + + describe('nodeStop', () => { + generateAPITests( + 'nodeStop', + [], + undefined, + JSON_RPC_REQUEST_METHOD.STOP_NODE, + false, + ); + }); + + describe('nodeSignMessage', () => { + generateAPITests( + 'nodeSignMessage', + new Uint8Array([]), + { signature: 'mockSignature' }, + JSON_RPC_REQUEST_METHOD.NODE_SIGN_MESSAGE, + false, + ); + }); + + describe('nodeGetStakingAddresses', () => { + generateAPITests( + 'nodeGetStakingAddresses', + [], + ['address1', 'address2'], + JSON_RPC_REQUEST_METHOD.GET_STAKING_ADDRESSES, + false, + ); + }); + + describe('nodeRemoveStakingAddresses', () => { + generateAPITests( + 'nodeRemoveStakingAddresses', + ['address1', 'address2'], + undefined, + JSON_RPC_REQUEST_METHOD.REMOVE_STAKING_ADDRESSES, + true, + ); + }); + + describe('nodeAddStakingSecretKeys', () => { + generateAPITests( + 'nodeAddStakingSecretKeys', + ['key1', 'key2'], + undefined, + JSON_RPC_REQUEST_METHOD.ADD_STAKING_PRIVATE_KEYS, + true, + ); + }); +}); diff --git a/test/web3/publicApiClient.spec.ts b/test/web3/publicApiClient.spec.ts new file mode 100644 index 00000000..949d81d3 --- /dev/null +++ b/test/web3/publicApiClient.spec.ts @@ -0,0 +1,268 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PublicApiClient } from '../../src/web3/PublicApiClient'; +import { JSON_RPC_REQUEST_METHOD } from '../../src/interfaces/JsonRpcMethods'; +import { IClientConfig } from '../../src/interfaces/IClientConfig'; +import { ProviderType, IProvider } from '../../src/interfaces/IProvider'; +import { + mockAddresses, + mockAddressesInfo, + mockGraphInterval, + mockNodeStatusInfo, + mockBlockIds, + mockBlockData, + mockEndorsementIds, + mockEndorsementData, + mockOperationData, + mockStackersData, + mockDatastoreEntryInput, + mockDatastoreEntries, + mockOpIds, +} from './mockData'; + +export const PERIOD_OFFSET = 5; + +describe('PublicApiClient', () => { + let client: PublicApiClient; + let mockSendJsonRPCRequest: jest.SpyInstance; + + // Function to generate tests for a set of similar operations to avoid code duplication + function generateAPITests( + operation: string, + mockData: any, + mockResponse: any, + jsonRpcRequestMethod: JSON_RPC_REQUEST_METHOD, + wrapArgsInArray: boolean, + ) { + test(`should call sendJsonRPCRequest with correct arguments`, async () => { + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + await (client as any)[operation](mockData); + + let rpcArgs; + + if (wrapArgsInArray) { + rpcArgs = [Array.isArray(mockData) ? mockData : []]; + } else { + rpcArgs = Array.isArray(mockData) ? mockData : []; + } + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + jsonRpcRequestMethod, + rpcArgs, + ); + }); + + test(`should return the correct result`, async () => { + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + const result = await (client as any)[operation](mockData); + + expect(result).toEqual(mockResponse); + }); + + test(`should handle errors correctly`, async () => { + const mockError = new Error('Error message'); + mockSendJsonRPCRequest.mockRejectedValue(mockError); + + await expect((client as any)[operation](mockData)).rejects.toThrow( + mockError, + ); + }); + + test(`should call trySafeExecute if retryStrategyOn is true`, async () => { + // Enable retry strategy + const originalRetryStrategy = (client as any).clientConfig + .retryStrategyOn; + (client as any).clientConfig.retryStrategyOn = true; + + mockSendJsonRPCRequest.mockResolvedValue(Promise.resolve(mockResponse)); + + const result = await (client as any)[operation](mockData); + + let rpcArgs; + if (wrapArgsInArray) { + rpcArgs = [Array.isArray(mockData) ? mockData : []]; + } else { + rpcArgs = Array.isArray(mockData) ? mockData : []; + } + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + jsonRpcRequestMethod, + rpcArgs, + ); + + expect(result).toEqual(mockResponse); + + // Restore retry strategy + (client as any).clientConfig.retryStrategyOn = originalRetryStrategy; + }); + } + + beforeEach(() => { + const clientConfig: IClientConfig = { + providers: [ + { + url: 'https://mock-public-api.com', + type: ProviderType.PUBLIC, + } as IProvider, + { + url: 'https://mock-private-api.com', + type: ProviderType.PRIVATE, + } as IProvider, + ], + periodOffset: PERIOD_OFFSET, + }; + client = new PublicApiClient(clientConfig); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockSendJsonRPCRequest = jest.spyOn(client as any, 'sendJsonRPCRequest'); + }); + + describe('getGraphInterval', () => { + generateAPITests( + 'getGraphInterval', + [], + mockGraphInterval, + JSON_RPC_REQUEST_METHOD.GET_GRAPH_INTERVAL, + true, + ); + }); + + describe('getBlockcliqueBlockBySlot', () => { + generateAPITests( + 'getBlockcliqueBlockBySlot', + [], + mockBlockData, + JSON_RPC_REQUEST_METHOD.GET_BLOCKCLIQUE_BLOCK_BY_SLOT, + true, + ); + }); + + describe('getNodeStatus', () => { + generateAPITests( + 'getNodeStatus', + [], + mockNodeStatusInfo, + JSON_RPC_REQUEST_METHOD.GET_STATUS, + false, + ); + }); + + describe('getAddresses', () => { + generateAPITests( + 'getAddresses', + mockAddresses, + mockAddressesInfo, + JSON_RPC_REQUEST_METHOD.GET_ADDRESSES, + true, + ); + }); + + describe('getBlocks', () => { + generateAPITests( + 'getBlocks', + mockBlockIds, + mockBlockData, + JSON_RPC_REQUEST_METHOD.GET_BLOCKS, + true, + ); + }); + + describe('getEndorsements', () => { + generateAPITests( + 'getEndorsements', + mockEndorsementIds, + mockEndorsementData, + JSON_RPC_REQUEST_METHOD.GET_ENDORSEMENTS, + true, + ); + }); + + describe('getOperations', () => { + generateAPITests( + 'getOperations', + mockOpIds, + mockOperationData, + JSON_RPC_REQUEST_METHOD.GET_OPERATIONS, + true, + ); + }); + + describe('getCliques', () => { + generateAPITests( + 'getCliques', + [], + mockEndorsementData, + JSON_RPC_REQUEST_METHOD.GET_CLIQUES, + false, + ); + }); + + describe('getStakers', () => { + generateAPITests( + 'getStakers', + [], + mockStackersData, + JSON_RPC_REQUEST_METHOD.GET_STAKERS, + false, + ); + }); + + describe('getDatastoreEntries', () => { + const transformedInput = mockDatastoreEntryInput.map((input) => ({ + address: input.address, + key: Array.prototype.slice.call(Buffer.from(input.key)), + })); + + test('should call sendJsonRPCRequest with correct arguments', async () => { + mockSendJsonRPCRequest.mockResolvedValue( + Promise.resolve(mockDatastoreEntries), + ); + + await client.getDatastoreEntries(mockDatastoreEntryInput); + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.GET_DATASTORE_ENTRIES, + [transformedInput], + ); + }); + + test('should return correct result', async () => { + mockSendJsonRPCRequest.mockResolvedValue( + Promise.resolve(mockDatastoreEntries), + ); + + const result = await client.getDatastoreEntries(mockDatastoreEntryInput); + + expect(result).toEqual(mockDatastoreEntries); + }); + + test('should handle errors correctly', async () => { + const mockError = new Error('Error message'); + mockSendJsonRPCRequest.mockRejectedValue(mockError); + + await expect( + client.getDatastoreEntries(mockDatastoreEntryInput), + ).rejects.toThrow(mockError); + }); + + test('should call trySafeExecute if retryStrategyOn is true', async () => { + const originalRetryStrategy = (client as any).clientConfig + .retryStrategyOn; + (client as any).clientConfig.retryStrategyOn = true; + + mockSendJsonRPCRequest.mockResolvedValue( + Promise.resolve(mockDatastoreEntries), + ); + + const result = await client.getDatastoreEntries(mockDatastoreEntryInput); + + expect(mockSendJsonRPCRequest).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.GET_DATASTORE_ENTRIES, + [transformedInput], + ); + expect(result).toEqual(mockDatastoreEntries); + + (client as any).clientConfig.retryStrategyOn = originalRetryStrategy; + }); + }); +}); diff --git a/test/web3/smartContractsClient.spec.ts b/test/web3/smartContractsClient.spec.ts new file mode 100644 index 00000000..0007463b --- /dev/null +++ b/test/web3/smartContractsClient.spec.ts @@ -0,0 +1,704 @@ +/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ +import { IBalance } from '../../src/interfaces/IBalance'; +import { JSON_RPC_REQUEST_METHOD } from '../../src/interfaces/JsonRpcMethods'; +import { EOperationStatus } from '../../src/interfaces/EOperationStatus'; +import { fromMAS } from '../../src/utils/converters'; +import { PublicApiClient } from '../../src/web3/PublicApiClient'; +import { SmartContractsClient } from '../../src/web3/SmartContractsClient'; +import { WalletClient } from '../../src/web3/WalletClient'; +import { + mockClientConfig, + mockDeployerAccount, + mockCallData, + mockContractData, + mockNodeStatusInfo, + mockOpIds, + mockContractReadOperationData, + mockContractReadOperationResponse, + mockReadData, + mockOperationDataDetailed, + mockAddressesInfo, + mockEventFilter, + mockedEvents, + mockContractReadOnlyOperationResponse, + validSignature, + mockContractReadOperationDataWithError, + mockAddresses, +} from './mockData'; +import { IExecuteReadOnlyResponse } from '../../src/interfaces/IExecuteReadOnlyResponse'; + +const MAX_READ_BLOCK_GAS = BigInt(4_294_967_295); +const TX_POLL_INTERVAL_MS = 10000; +const TX_STATUS_CHECK_RETRY_COUNT = 100; + +// Mock to not wait for the timeout to finish +jest.mock('../../src/utils/time', () => { + return { + Timeout: jest.fn(), + wait: jest.fn(() => Promise.resolve()), + }; +}); + +describe('SmartContractsClient', () => { + let smartContractsClient: SmartContractsClient; + let mockPublicApiClient: PublicApiClient; + let mockWalletClient: WalletClient; + + beforeEach(() => { + // Initialize the mock objects + mockPublicApiClient = new PublicApiClient(mockClientConfig); + mockWalletClient = new WalletClient( + mockClientConfig, + mockPublicApiClient, + mockDeployerAccount, + ); + smartContractsClient = new SmartContractsClient( + mockClientConfig, + mockPublicApiClient, + mockWalletClient, + ); + + // Mock getOperations + mockPublicApiClient.getOperations = jest + .fn() + .mockImplementation((operationIds: string[]) => { + return Promise.resolve( + mockOperationDataDetailed.filter((operation) => + operationIds.includes(operation.id), + ), + ); + }); + + mockPublicApiClient.getNodeStatus = jest + .fn() + .mockResolvedValue(mockNodeStatusInfo); + // Mock the getBaseAccount function + mockWalletClient.getBaseAccount = jest + .fn() + .mockReturnValue(mockDeployerAccount); + // Mock the walletSignMessage function + WalletClient.walletSignMessage = jest + .fn() + .mockResolvedValue(validSignature); + // Mock the sendJsonRPCRequest function + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockOpIds); + }); + + describe('deploySmartContract', () => { + let consoleWarnSpy: jest.SpyInstance; + beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, 'warn'); + consoleWarnSpy.mockImplementation(() => {}); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + test('should call sendJsonRPCRequest with correct arguments', async () => { + await smartContractsClient.deploySmartContract( + mockContractData, + mockDeployerAccount, + ); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith(JSON_RPC_REQUEST_METHOD.SEND_OPERATIONS, [ + [ + { + serialized_content: expect.any(Array), + creator_public_key: mockDeployerAccount.publicKey, + signature: validSignature.base58Encoded, + }, + ], + ]); + }); + + test('should return the correct result', async () => { + const result = await smartContractsClient.deploySmartContract( + mockContractData, + ); + + expect(result).toBe(mockOpIds[0]); + }); + + // Write additional tests to handle any edge cases or error scenarios + test('should handle errors correctly', async () => { + const mockError = new Error('Error message'); + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockRejectedValue(mockError); + + await expect( + smartContractsClient.deploySmartContract(mockContractData), + ).rejects.toThrow(mockError); + }); + + test('should throw error when no executor is provided and base account is not set', async () => { + mockWalletClient.getBaseAccount = jest.fn().mockReturnValue(null); + await expect( + smartContractsClient.deploySmartContract(mockContractData), + ).rejects.toThrow(`No tx sender available`); + }); + + test('should use default account when no executor is provided', async () => { + await smartContractsClient.deploySmartContract(mockContractData); + expect(mockWalletClient.getBaseAccount).toHaveBeenCalled(); + }); + + test('should warn when contractDataBinary size exceeded half of the maximum size of a block', async () => { + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const max_block_size = mockNodeStatusInfo.config.max_block_size; + mockContractData.contractDataBinary = new Uint8Array( + max_block_size / 2 + 1, + ); // value > max_block_size / 2 + + await smartContractsClient.deploySmartContract(mockContractData); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'bytecode size exceeded half of the maximum size of a block, operation will certainly be rejected', + ); + }); + + test('should throw error when contractDataBinary does not exist', async () => { + const modifiedMockContractData = { ...mockContractData }; + delete modifiedMockContractData.contractDataBinary; + + await expect( + smartContractsClient.deploySmartContract(modifiedMockContractData), + ).rejects.toThrow( + `Expected non-null contract bytecode, but received null.`, + ); + }); + + test('should throw error when no opId is returned', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([]); + + await expect( + smartContractsClient.deploySmartContract(mockContractData), + ).rejects.toThrow( + `Deploy smart contract operation bad response. No results array in json rpc response. Inspect smart contract`, + ); + }); + }); + + describe('callSmartContract', () => { + test('should call sendJsonRPCRequest with correct arguments', async () => { + await smartContractsClient.callSmartContract( + mockCallData, + mockDeployerAccount, + ); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith(JSON_RPC_REQUEST_METHOD.SEND_OPERATIONS, [ + [ + { + serialized_content: expect.any(Array), + creator_public_key: mockDeployerAccount.publicKey, + signature: validSignature.base58Encoded, + }, + ], + ]); + }); + + test('should return the correct result', async () => { + const result = await smartContractsClient.callSmartContract( + mockCallData, + mockDeployerAccount, + ); + + expect(result).toBe(mockOpIds[0]); + }); + + test('should use default account when no executor is provided', async () => { + await smartContractsClient.callSmartContract(mockCallData); + expect(mockWalletClient.getBaseAccount).toHaveBeenCalled(); + }); + + test('should handle errors correctly', async () => { + const mockError = new Error('Error message'); + (smartContractsClient as any).sendJsonRPCRequest.mockRejectedValue( + mockError, + ); + + await expect( + smartContractsClient.callSmartContract(mockCallData), + ).rejects.toThrow(mockError); + }); + + test('should throw error when no executor is provided and base account is not set', async () => { + mockWalletClient.getBaseAccount = jest.fn().mockReturnValue(null); + await expect( + smartContractsClient.callSmartContract(mockCallData), + ).rejects.toThrow(`No tx sender available`); + }); + + test('should call trySafeExecute if retryStrategyOn is true', async () => { + const originalRetryStrategy = (smartContractsClient as any).clientConfig + .retryStrategyOn; + (smartContractsClient as any).clientConfig.retryStrategyOn = true; + + await smartContractsClient.callSmartContract(mockCallData); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith(JSON_RPC_REQUEST_METHOD.SEND_OPERATIONS, [ + [ + { + serialized_content: expect.any(Array), + creator_public_key: mockDeployerAccount.publicKey, + signature: validSignature.base58Encoded, + }, + ], + ]); + + (smartContractsClient as any).clientConfig.retryStrategyOn = + originalRetryStrategy; + }); + + test('should throw error when no opId is returned', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([]); + + await expect( + smartContractsClient.callSmartContract(mockCallData), + ).rejects.toThrow( + `Call smart contract operation bad response. No results array in json rpc response. Inspect smart contract`, + ); + }); + }); + describe('readSmartContract', () => { + test('should send the correct JSON RPC request', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationData); + + await smartContractsClient.readSmartContract(mockReadData); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith(JSON_RPC_REQUEST_METHOD.EXECUTE_READ_ONLY_CALL, [ + [ + { + max_gas: Number(mockReadData.maxGas), + target_address: mockReadData.targetAddress, + target_function: mockReadData.targetFunction, + parameter: mockReadData.parameter, + caller_address: mockReadData.callerAddress, + }, + ], + ]); + }); + + test('should return correct result', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationData); + + const result = await smartContractsClient.readSmartContract(mockReadData); + + expect(result).toEqual(mockContractReadOperationResponse); + }); + + test('should throw error when the gas submitted exceeds the maximum allowed block gas', async () => { + const mockReadDataWithLargeMaxGas = { + ...mockReadData, + maxGas: BigInt(4_294_967_296), + }; // value > MAX_READ_BLOCK_GAS + + await expect( + smartContractsClient.readSmartContract(mockReadDataWithLargeMaxGas), + ).rejects.toThrow( + `The gas submitted ${mockReadDataWithLargeMaxGas.maxGas.toString()} exceeds the max. allowed block gas of ${MAX_READ_BLOCK_GAS.toString()}`, + ); + }); + + test('should throw error when no results array in json rpc response', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([]); + + await expect( + smartContractsClient.readSmartContract(mockReadData), + ).rejects.toThrow( + `Read operation bad response. No results array in json rpc response. Inspect smart contract`, + ); + }); + + test('should throw error when json rpc response has error', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationDataWithError); + + await expect( + smartContractsClient.readSmartContract(mockReadData), + ).rejects.toThrow(mockContractReadOperationDataWithError[0].result.Error); + }); + + test('should call trySafeExecute if retryStrategyOn is true', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationData); + + const originalRetryStrategy = (smartContractsClient as any).clientConfig + .retryStrategyOn; + (smartContractsClient as any).clientConfig.retryStrategyOn = true; + + await smartContractsClient.readSmartContract(mockReadData); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith(JSON_RPC_REQUEST_METHOD.EXECUTE_READ_ONLY_CALL, [ + [ + { + max_gas: Number(mockReadData.maxGas), + target_address: mockReadData.targetAddress, + target_function: mockReadData.targetFunction, + parameter: mockReadData.parameter, + caller_address: mockReadData.callerAddress, + }, + ], + ]); + + (smartContractsClient as any).clientConfig.retryStrategyOn = + originalRetryStrategy; + }); + }); + + describe('getOperationStatus', () => { + test('should return EOperationStatus.INCLUDED_PENDING when operation is included in blocks', async () => { + const opId = mockOpIds[0]; + const status = await smartContractsClient.getOperationStatus(opId); + expect(status).toBe(EOperationStatus.INCLUDED_PENDING); + }); + + test('should return EOperationStatus.FINAL when operation is final', async () => { + const opId = mockOpIds[1]; + const status = await smartContractsClient.getOperationStatus(opId); + expect(status).toBe(EOperationStatus.FINAL); + }); + + test('should return EOperationStatus.AWAITING_INCLUSION when operation is in the pool', async () => { + const opId = mockOpIds[2]; + const status = await smartContractsClient.getOperationStatus(opId); + expect(status).toBe(EOperationStatus.AWAITING_INCLUSION); + }); + + test('should return EOperationStatus.INCONSISTENT when operation is neither in blocks nor in the pool', async () => { + const opId = mockOpIds[3]; + const status = await smartContractsClient.getOperationStatus(opId); + expect(status).toBe(EOperationStatus.INCONSISTENT); + }); + + test('should return EOperationStatus.NOT_FOUND when operation does not exist', async () => { + const opId = '0x005'; // Doesn't exist + const status = await smartContractsClient.getOperationStatus(opId); + expect(status).toBe(EOperationStatus.NOT_FOUND); + }); + }); + + describe('awaitRequiredOperationStatus', () => { + const opId = mockOpIds[0]; + const requiredStatus = EOperationStatus.FINAL; + + beforeEach(() => { + // Reset the getOperationStatus function + smartContractsClient.getOperationStatus = jest.fn(); + }); + + test('waiting for NOT_FOUND status to become the required status', async () => { + let callCount = 0; + smartContractsClient.getOperationStatus = jest + .fn() + .mockImplementation(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve(EOperationStatus.NOT_FOUND); + } else { + return Promise.resolve(requiredStatus); + } + }); + + const promise = smartContractsClient.awaitRequiredOperationStatus( + opId, + requiredStatus, + ); + + const status = await promise; + + expect(status).toBe(requiredStatus); + expect(smartContractsClient.getOperationStatus).toHaveBeenCalledTimes(2); + }); + + test('fails after reaching the error limit', async () => { + console.error = jest.fn(); + + // Always throw an error + const expectedErrorMessage = 'Test error'; + smartContractsClient.getOperationStatus = jest + .fn() + .mockRejectedValue(new Error(expectedErrorMessage)); + + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const error = await smartContractsClient + .awaitRequiredOperationStatus(opId, requiredStatus) + .catch((e) => e); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual(expectedErrorMessage); + expect(smartContractsClient.getOperationStatus).toHaveBeenCalledTimes( + 101, + ); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); + + test('fails after reaching the pending limit', async () => { + console.warn = jest.fn(); + // Always return a status other than the requiredStatus + smartContractsClient.getOperationStatus = jest + .fn() + .mockResolvedValue(EOperationStatus.NOT_FOUND); + + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + await expect( + smartContractsClient.awaitRequiredOperationStatus(opId, requiredStatus), + ).rejects.toThrow( + `Getting the tx status for operation Id ${opId} took too long to conclude. We gave up after ${ + TX_POLL_INTERVAL_MS * TX_STATUS_CHECK_RETRY_COUNT + }ms.`, + ); + + expect(smartContractsClient.getOperationStatus).toHaveBeenCalledTimes( + 1001, + ); + + // Restore console.warn + consoleWarnSpy.mockRestore(); + }); + }); + + describe('getContractBalance', () => { + const expectedBalance: IBalance = { + candidate: fromMAS(mockAddressesInfo[0].candidate_balance), + final: fromMAS(mockAddressesInfo[0].final_balance), + }; + + test('should return the correct balance when the address exists', async () => { + mockPublicApiClient.getAddresses = jest + .fn() + .mockResolvedValue(mockAddressesInfo); + + const balance = await smartContractsClient.getContractBalance( + mockAddresses[0], + ); + + expect(balance).toEqual(expectedBalance); + expect(mockPublicApiClient.getAddresses).toHaveBeenCalledWith([ + mockAddresses[0], + ]); + }); + + test('should return null when the address does not exist', async () => { + mockPublicApiClient.getAddresses = jest.fn().mockResolvedValue([]); + + const balance = await smartContractsClient.getContractBalance( + mockAddresses[0], + ); + + expect(balance).toBeNull(); + expect(mockPublicApiClient.getAddresses).toHaveBeenCalledWith([ + mockAddresses[0], + ]); + }); + }); + + describe('getFilteredScOutputEvents', () => { + test('should send the correct JSON RPC request', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockedEvents); + + await smartContractsClient.getFilteredScOutputEvents(mockEventFilter); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.GET_FILTERED_SC_OUTPUT_EVENT, + [mockEventFilter], + ); + }); + + test('should return the correct array of IEvent objects', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockedEvents); + + const result = await smartContractsClient.getFilteredScOutputEvents( + mockEventFilter, + ); + + expect(result).toEqual(mockedEvents); + }); + + test('should call trySafeExecute if retryStrategyOn is true', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockedEvents); + + const originalRetryStrategy = (smartContractsClient as any).clientConfig + .retryStrategyOn; + (smartContractsClient as any).clientConfig.retryStrategyOn = true; + + await smartContractsClient.getFilteredScOutputEvents(mockEventFilter); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.GET_FILTERED_SC_OUTPUT_EVENT, + [mockEventFilter], + ); + + (smartContractsClient as any).clientConfig.retryStrategyOn = + originalRetryStrategy; + }); + }); + + describe('executeReadOnlySmartContract', () => { + test('should throw error if contractDataBinary is missing', async () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect( + smartContractsClient.executeReadOnlySmartContract({ + ...mockContractData, + contractDataBinary: undefined, + }), + ).rejects.toThrow( + `Expected non-null contract bytecode, but received null.`, + ); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); + + test('should throw error if address is missing', async () => { + await expect( + smartContractsClient.executeReadOnlySmartContract({ + ...mockContractData, + address: undefined, + }), + ).rejects.toThrow(`Expected contract address, but received null.`); + }); + + test('should send correct request', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([{ result: { Ok: new Uint8Array([11, 22, 33]) } }]); + + await smartContractsClient.executeReadOnlySmartContract(mockContractData); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.EXECUTE_READ_ONLY_BYTECODE, + [ + [ + { + max_gas: Number(mockContractData.maxGas), + bytecode: mockContractData.contractDataBinary + ? Array.from(mockContractData.contractDataBinary) + : [], + address: mockContractData.address, + }, + ], + ], + ); + }); + + test('should return correct result', async () => { + const expectedResponse: IExecuteReadOnlyResponse = + mockContractReadOnlyOperationResponse; + + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationData); + + const result = await smartContractsClient.executeReadOnlySmartContract( + mockContractData, + ); + + expect(result).toEqual(expectedResponse); + }); + + test('should throw error if no result is returned', async () => { + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([]); + + await expect( + smartContractsClient.executeReadOnlySmartContract(mockContractData), + ).rejects.toThrow( + `Read operation bad response. No results array in json rpc response. Inspect smart contract`, + ); + }); + + test('should throw error if result contains an error', async () => { + const error = 'Some error'; + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue([{ result: { Error: error } }]); + + await expect( + smartContractsClient.executeReadOnlySmartContract(mockContractData), + ).rejects.toThrow(`Execute read-only smart contract error`); + }); + + test('should call trySafeExecute if retryStrategyOn is true', async () => { + const originalRetryStrategy = (smartContractsClient as any).clientConfig + .retryStrategyOn; + (smartContractsClient as any).clientConfig.retryStrategyOn = true; + + (smartContractsClient as any).sendJsonRPCRequest = jest + .fn() + .mockResolvedValue(mockContractReadOperationData); + + await smartContractsClient.executeReadOnlySmartContract(mockContractData); + + expect( + (smartContractsClient as any).sendJsonRPCRequest, + ).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.EXECUTE_READ_ONLY_BYTECODE, + [ + [ + { + max_gas: Number(mockContractData.maxGas), + bytecode: mockContractData.contractDataBinary + ? Array.from(mockContractData.contractDataBinary) + : [], + address: mockContractData.address, + }, + ], + ], + ); + + (smartContractsClient as any).clientConfig.retryStrategyOn = + originalRetryStrategy; + }); + }); +}); diff --git a/test/web3/walletClient.spec.ts b/test/web3/walletClient.spec.ts index 1f8d4b0e..5fe1d777 100644 --- a/test/web3/walletClient.spec.ts +++ b/test/web3/walletClient.spec.ts @@ -1,9 +1,21 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-var-requires */ import { IAccount } from '../../src/interfaces/IAccount'; import { ClientFactory } from '../../src/web3/ClientFactory'; import { WalletClient } from '../../src/web3/WalletClient'; import { Client } from '../../src/web3/Client'; +import { base58Decode } from '../../src/utils/Xbqcrypto'; import { IProvider, ProviderType } from '../../src/interfaces/IProvider'; +import { expect, test, describe, beforeEach, afterEach } from '@jest/globals'; +import * as ed from '@noble/ed25519'; +import { ISignature } from '../../src/interfaces/ISignature'; +import { IFullAddressInfo } from '../../src/interfaces/IFullAddressInfo'; +import { mockResultSendJsonRPCRequestWalletInfo } from './mockData'; +import { ITransactionData } from '../../src/interfaces/ITransactionData'; +import { OperationTypeId } from '../../src/interfaces/OperationTypes'; +import { JSON_RPC_REQUEST_METHOD } from '../../src/interfaces/JsonRpcMethods'; +import { mockAddressesInfo, mockNodeStatusInfo, mockOpIds } from './mockData'; +import { IRollsData } from '../../src/interfaces/IRollsData'; +import { fromMAS } from '../../src/utils/converters'; // TODO: Use env variables and say it in the CONTRIBUTING.md const deployerPrivateKey = @@ -11,8 +23,10 @@ const deployerPrivateKey = const receiverPrivateKey = 'S1eK3SEXGDAWN6pZhdr4Q7WJv6UHss55EB14hPy4XqBpiktfPu6'; -const publicApi = 'http://127.0.0.1:33035'; -const privateApi = 'http://127.0.0.1:33034'; +// for CI testing: +const publicApi = 'https://mock-public-api.com'; +const privateApi = 'https://mock-private-api.com'; + const MAX_WALLET_ACCOUNTS = 256; export async function initializeClient() { @@ -30,7 +44,36 @@ export async function initializeClient() { return web3Client; } -describe.skip('WalletClient', () => { +function createFullAddressInfo( + address: string | null, + publicKey: string | null, + secretKey: string | null, +): IFullAddressInfo { + if (!address || !publicKey || !secretKey) { + throw new Error('Invalid address, public key or secret key'); + } + return { + address, + candidate_balance: '0', + candidate_datastore_keys: [], + candidate_roll_count: 0, + created_blocks: [], + created_endorsements: [], + created_operations: [], + cycle_infos: [], + deferred_credits: [], + final_balance: '0', + final_datastore_keys: [], + final_roll_count: 0, + next_block_draws: [], + next_endorsement_draws: [], + thread: 0, + publicKey, + secretKey, + }; +} + +describe('WalletClient', () => { let web3Client: Client; // let walletClient: WalletClient; let baseAccount: IAccount = { @@ -45,7 +88,7 @@ describe.skip('WalletClient', () => { }); afterEach(async () => { - await web3Client.wallet().cleanWallet(); + web3Client.wallet().cleanWallet(); }); describe('setBaseAccount', () => { @@ -54,13 +97,13 @@ describe.skip('WalletClient', () => { receiverPrivateKey, ); await web3Client.wallet().setBaseAccount(account); - const baseAccount = await web3Client.wallet().getBaseAccount(); + const baseAccount = web3Client.wallet().getBaseAccount(); expect(baseAccount).not.toBeNull(); expect(baseAccount?.address).toEqual(account.address); }); test('should throw error if account is not valid', async () => { - await web3Client.wallet().cleanWallet(); + web3Client.wallet().cleanWallet(); await expect( web3Client.wallet().setBaseAccount({} as IAccount), ).rejects.toThrow(); @@ -79,14 +122,14 @@ describe.skip('WalletClient', () => { const firstAccount = await WalletClient.getAccountFromSecretKey( receiverPrivateKey, ); - await web3Client.wallet().setBaseAccount(firstAccount); + web3Client.wallet().setBaseAccount(firstAccount); const secondAccount = await WalletClient.getAccountFromSecretKey( deployerPrivateKey, ); - await web3Client.wallet().setBaseAccount(secondAccount); + web3Client.wallet().setBaseAccount(secondAccount); - const baseAccount = await web3Client.wallet().getBaseAccount(); + const baseAccount = web3Client.wallet().getBaseAccount(); expect(baseAccount).not.toBeNull(); expect(baseAccount?.address).toEqual(secondAccount.address); }); @@ -94,14 +137,14 @@ describe.skip('WalletClient', () => { describe('getBaseAccount', () => { test('should return the base account', async () => { - const fetchedBaseAccount = await web3Client.wallet().getBaseAccount(); + const fetchedBaseAccount = web3Client.wallet().getBaseAccount(); expect(fetchedBaseAccount).not.toBeNull(); expect(fetchedBaseAccount?.address).toEqual(baseAccount.address); }); test('should return null if base account is not set', async () => { - await web3Client.wallet().cleanWallet(); - const fetchedBaseAccount = await web3Client.wallet().getBaseAccount(); + web3Client.wallet().cleanWallet(); + const fetchedBaseAccount = web3Client.wallet().getBaseAccount(); expect(fetchedBaseAccount).toBeNull(); }); }); @@ -119,7 +162,6 @@ describe.skip('WalletClient', () => { const targetAccount = accounts[1]; // Assume we want to find the second account const fetchedAccount = web3Client .wallet() - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .getWalletAccountByAddress(targetAccount.address!); expect(fetchedAccount).not.toBeNull(); @@ -144,7 +186,6 @@ describe.skip('WalletClient', () => { const upperCaseAddress = targetAccount.address?.toUpperCase(); const fetchedAccount = web3Client .wallet() - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .getWalletAccountByAddress(upperCaseAddress!); expect(fetchedAccount).not.toBeNull(); @@ -173,7 +214,7 @@ describe.skip('WalletClient', () => { const addedAccounts = await web3Client .wallet() .addSecretKeysToWallet(secretKeys); - const walletAccounts = await web3Client.wallet().getWalletAccounts(); + const walletAccounts = web3Client.wallet().getWalletAccounts(); expect([baseAccount, addedAccounts[0]]).toStrictEqual(walletAccounts); expect(addedAccounts.length).toBe(1); // only one unique account should be added expect(web3Client.wallet().getWalletAccounts().length).toBe(2); // only one unique account should be added @@ -183,7 +224,7 @@ describe.skip('WalletClient', () => { const addedAccounts = await web3Client .wallet() .addSecretKeysToWallet([deployerPrivateKey, receiverPrivateKey]); - const walletAccounts = await web3Client.wallet().getWalletAccounts(); + const walletAccounts = web3Client.wallet().getWalletAccounts(); // only receiver account should be added expect(addedAccounts[0].secretKey).toBe(receiverPrivateKey); @@ -198,9 +239,7 @@ describe.skip('WalletClient', () => { await WalletClient.walletGenerateNewAccount(), ]; const secretKeys: string[] = [ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion accounts[0].secretKey!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion accounts[1].secretKey!, ]; @@ -231,7 +270,7 @@ describe.skip('WalletClient', () => { ]; await web3Client.wallet().addAccountsToWallet(accounts); - const walletAccounts = await web3Client.wallet().getWalletAccounts(); + const walletAccounts = web3Client.wallet().getWalletAccounts(); expect(walletAccounts.length).toBe(4); // 3 generated + 1 base account }); @@ -254,4 +293,676 @@ describe.skip('WalletClient', () => { ); }); }); + + describe('walletInfo', () => { + test('should return an empty array if the wallet is empty', async () => { + web3Client.wallet().cleanWallet(); // Make sure the wallet is empty + const walletInfo = await web3Client.wallet().walletInfo(); + expect(walletInfo).toEqual([]); + }); + + test('should throw an error if the number of retrieved wallets does not match the number of addresses in the wallet', async () => { + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(web3Client.wallet() as any, 'getWalletAddressesInfo') + .mockImplementation(async () => { + return [ + /* return fewer or more addresses than in the wallet */ + ]; + }); + + await expect(web3Client.wallet().walletInfo()).rejects.toThrow( + /Requested wallets not fully retrieved./, + ); + }); + + test('should return IFullAddressInfo objects that include information from the corresponding IAddressInfo', async () => { + const accounts = [ + baseAccount, + await WalletClient.walletGenerateNewAccount(), + ]; + await web3Client.wallet().addAccountsToWallet(accounts); // will not add the base account + + const mockAddressInfo: IFullAddressInfo[] = [ + createFullAddressInfo( + baseAccount.address, + baseAccount.publicKey, + baseAccount.secretKey, + ), + createFullAddressInfo( + accounts[1].address, + accounts[1].publicKey, + accounts[1].secretKey, + ), + ]; + + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(web3Client.wallet() as any, 'getWalletAddressesInfo') + .mockImplementation(async () => { + return mockAddressInfo; + }); + + const walletInfo = await web3Client.wallet().walletInfo(); + // check that the returned walletInfo is an array of IFullAddressInfo with correct information + walletInfo.forEach((info, index) => { + expect(info.address).toBe(mockAddressInfo[index].address); + expect(info.publicKey).toBe(accounts[index].publicKey); + expect(info.secretKey).toBe(accounts[index].secretKey); + }); + }); + }); + + describe('getWalletAddressesInfo', () => { + beforeEach(() => { + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(web3Client.wallet() as any, 'sendJsonRPCRequest') + .mockResolvedValue(mockResultSendJsonRPCRequestWalletInfo); + }); + + test('should call getWalletAddressesInfo when walletInfo is called', async () => { + const spy = jest.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + web3Client.wallet() as any, + 'getWalletAddressesInfo', + ); + const mockAddresses = [ + baseAccount.address, + await WalletClient.walletGenerateNewAccount().then( + (account) => account.address, + ), + ]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).wallet = mockAddresses.map((address) => ({ + address, + })); + + await web3Client.wallet().walletInfo(); + + expect(spy).toHaveBeenCalledWith(mockAddresses); + }); + + test('should call sendJsonRPCRequest if retryStrategyOn is false', async () => { + const sendJsonRPCRequestSpy = jest.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + web3Client.wallet() as any, + 'sendJsonRPCRequest', + ); + + const mockAddresses = [ + baseAccount.address, + await WalletClient.walletGenerateNewAccount().then( + (account) => account.address, + ), + ]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).wallet = mockAddresses.map((address) => ({ + address, + })); + + // Enable retry strategy + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalRetryStrategy = (web3Client.wallet() as any).clientConfig + .retryStrategyOn; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).clientConfig.retryStrategyOn = false; + + await web3Client.wallet().walletInfo(); + + expect(sendJsonRPCRequestSpy).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.GET_ADDRESSES, + [mockAddresses], + ); + + // Restore original retry strategy setting + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).clientConfig.retryStrategyOn = + originalRetryStrategy; + }); + }); + + describe('removeAddressesFromWallet', () => { + test('should remove specified addresses from the wallet', async () => { + const accountsToRemove = await web3Client + .wallet() + .addSecretKeysToWallet([ + receiverPrivateKey, + 'S1USr9AFUaH7taTKeWt94qGTgaS9XkpnH1SPpctRDoK3sSJkYWk', + 'S16cS2QnKmyxxiU68Bw9Lnmt2Yttva42nahDG68awziextJgBze', + ]); + let addressesToRemove = accountsToRemove.map( + (account) => account.address, + ); + + expect(addressesToRemove.length).toBe(3); + expect(addressesToRemove).not.toContain(null); + await web3Client + .wallet() + .removeAddressesFromWallet(addressesToRemove as string[]); + + const walletAccounts = web3Client.wallet().getWalletAccounts(); + addressesToRemove.forEach((address) => { + expect(walletAccounts).not.toContainEqual( + expect.objectContaining({ address }), + ); + }); + }); + }); + + describe('cleanWallet', () => { + test('remove all accounts from the wallet', async () => { + web3Client.wallet().cleanWallet(); + const walletAccounts = await web3Client.wallet().getWalletAccounts(); + expect(walletAccounts.length).toBe(0); // only base account should be left + }); + }); + + describe('walletSignMessage', () => { + test('should sign a message with a valid signer', async () => { + const data = 'Test message'; + const signer = baseAccount; + const modelSignedMessage = + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE'; + + const signedMessage = await WalletClient.walletSignMessage(data, signer); + + expect(signedMessage).toHaveProperty('base58Encoded'); + expect(typeof signedMessage.base58Encoded).toBe('string'); + expect(signedMessage.base58Encoded).toEqual(modelSignedMessage); + }); + + test('should throw an error when no private key is available', async () => { + const data = 'Test message'; + const signer = { ...baseAccount, secretKey: null }; + + await expect( + WalletClient.walletSignMessage(data, signer), + ).rejects.toThrow('No private key to sign the message with'); + }); + + test('should throw an error when no public key is available', async () => { + const data = 'Test message'; + const signer = { ...baseAccount, publicKey: null }; + + await expect( + WalletClient.walletSignMessage(data, signer), + ).rejects.toThrow('No public key to verify the signed message with'); + }); + + test('should throw an error when signature length is invalid', async () => { + const data = 'Test message'; + const signer = baseAccount; + + // Create a spy on the 'sign' function to provide an incorrect mock implementation for this test + const signSpy = jest.spyOn(ed, 'sign'); + signSpy.mockImplementation(() => Promise.resolve(Buffer.alloc(63))); // 63 instead of 64 + + await expect( + WalletClient.walletSignMessage(data, signer), + ).rejects.toThrow(/Invalid signature length. Expected 64, got/); + + // Restore the original 'sign' function after the test + signSpy.mockRestore(); + }); + + test('should correctly process Buffer data', async () => { + const data = Buffer.from('Test message'); + const modelSignedMessage = + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE'; + const signer = baseAccount; + + const signedMessage = await WalletClient.walletSignMessage(data, signer); + + expect(signedMessage).toHaveProperty('base58Encoded'); + expect(typeof signedMessage.base58Encoded).toBe('string'); + expect(signedMessage.base58Encoded).toEqual(modelSignedMessage); + }); + + test('should throw an error if the signature could not be verified with the public key', async () => { + const data = 'Test message'; + const signer = baseAccount; + + // Create a spy on the 'verify' function to provide an incorrect mock implementation for this test + const verifySpy = jest.spyOn(ed, 'verify'); + verifySpy.mockImplementation(() => Promise.resolve(false)); // always return false + + await expect( + WalletClient.walletSignMessage(data, signer), + ).rejects.toThrow( + 'Signature could not be verified with public key. Please inspect', + ); + + // Restore the original 'verify' function after the test + verifySpy.mockRestore(); + }); + }); + + describe('signMessage', () => { + test('should sign a message with a valid account', async () => { + const data = 'Test message'; + const modelSignedMessage = + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE'; + + const accountSignerAddress: string = baseAccount.address!; + + const signedMessage = await web3Client + .wallet() + .signMessage(data, accountSignerAddress); + + expect(signedMessage).toHaveProperty('base58Encoded'); + expect(typeof signedMessage.base58Encoded).toBe('string'); + expect(signedMessage.base58Encoded).toEqual(modelSignedMessage); + }); + + test('should throw an error when the account is not found', async () => { + const data = 'Test message'; + const nonExistentSignerAddress = 'nonExistentSignerAddress'; + + await expect( + web3Client.wallet().signMessage(data, nonExistentSignerAddress), + ).rejects.toThrow( + `No signer account ${nonExistentSignerAddress} found in wallet`, + ); + }); + + test('should correctly process Buffer data', async () => { + const data = Buffer.from('Test message'); + const modelSignedMessage = + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE'; + + const accountSignerAddress = baseAccount.address!; + + const signedMessage = await web3Client + .wallet() + .signMessage(data, accountSignerAddress); + + expect(signedMessage).toHaveProperty('base58Encoded'); + expect(typeof signedMessage.base58Encoded).toBe('string'); + expect(signedMessage.base58Encoded).toEqual(modelSignedMessage); + }); + }); + + describe('walletGenerateNewAccount', () => { + test('should generate a new account', async () => { + const newAccount = await WalletClient.walletGenerateNewAccount(); + // Check that the newAccount object has all necessary properties + expect(newAccount).toHaveProperty('address'); + expect(newAccount).toHaveProperty('secretKey'); + expect(newAccount).toHaveProperty('publicKey'); + expect(newAccount).toHaveProperty('createdInThread'); + + // Check that the properties are of correct type + expect(typeof newAccount.address).toBe('string'); + expect(typeof newAccount.secretKey).toBe('string'); + expect(typeof newAccount.publicKey).toBe('string'); + expect(typeof newAccount.createdInThread).toBe('number'); + + // Check that the properties are not empty or null + expect(newAccount.address).not.toBeNull(); + expect(newAccount.address).not.toBe(''); + expect(newAccount.secretKey).not.toBeNull(); + expect(newAccount.secretKey).not.toBe(''); + expect(newAccount.publicKey).not.toBeNull(); + expect(newAccount.publicKey).not.toBe(''); + + // Check that keys and address have the correct length + expect(newAccount.address?.length).toBeGreaterThanOrEqual(50); + expect(newAccount.secretKey?.length).toBeGreaterThanOrEqual(50); + expect(newAccount.publicKey?.length).toBeGreaterThanOrEqual(50); + }); + + test('should generate unique accounts each time', async () => { + const newAccount1 = await WalletClient.walletGenerateNewAccount(); + const newAccount2 = await WalletClient.walletGenerateNewAccount(); + expect(newAccount1).not.toEqual(newAccount2); + }); + }); + + describe('getAccountFromSecretKey', () => { + test('should generate an account from a secret key', async () => { + const secretKey = 'S12syP5uCVEwaJwvXLqJyD1a2GqZjsup13UnhY6uzbtyu7ExXWZS'; + const addressModel = + 'AU12KgrLq2vhMgi8aAwbxytiC4wXBDGgvTtqGTM5R7wEB9En8WBHB'; + const publicKeyModel = + 'P12c2wsKxEyAhPC4ouNsgywzM41VsNSuwH9JdMbRt9bM8ZsMLPQA'; + const createdInThreadModel = 21; + const accountFromSecretKey = await WalletClient.getAccountFromSecretKey( + secretKey, + ); + // Check that the accountFromSecretKey object has all necessary properties + expect(accountFromSecretKey).toHaveProperty('address'); + expect(accountFromSecretKey).toHaveProperty('secretKey'); + expect(accountFromSecretKey).toHaveProperty('publicKey'); + expect(accountFromSecretKey).toHaveProperty('createdInThread'); + // Check that the secretKey matches the models + expect(accountFromSecretKey.address).toEqual(addressModel); + expect(accountFromSecretKey.publicKey).toEqual(publicKeyModel); + expect(accountFromSecretKey.secretKey).toEqual(secretKey); + expect(accountFromSecretKey.createdInThread).toEqual( + createdInThreadModel, + ); + }); + + test('should throw error if invalid secret key is provided', async () => { + const invalidSecretKey = 'invalidSecretKey'; + await expect( + WalletClient.getAccountFromSecretKey(invalidSecretKey), + ).rejects.toThrow(); + + const emptySecretKey = ''; + await expect( + WalletClient.getAccountFromSecretKey(emptySecretKey), + ).rejects.toThrow(); + + const nullSecretKey = null; + await expect( + WalletClient.getAccountFromSecretKey(nullSecretKey as never), + ).rejects.toThrow(); + }); + }); + + describe('verifySignature', () => { + test('should return true for a valid signature', async () => { + const message = 'Test message'; + + const signerPublicKey = baseAccount.publicKey!; + const validSignature: ISignature = { + base58Encoded: + '1TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE', + }; + const result = await web3Client + .wallet() + .verifySignature(message, validSignature, signerPublicKey); + + expect(result).toBe(true); + }); + + test('should return false for an invalid signature', async () => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => null); + + const data = 'Test message'; + + const signerPublicKey = baseAccount.publicKey!; + const invalidSignature: ISignature = { + base58Encoded: + '2TXucC8nai7BYpAnMPYrotVcKCZ5oxkfWHb2ykKj2tXmaGMDL1XTU5AbC6Z13RH3q59F8QtbzKq4gzBphGPWpiDonownxE', // starts with 2 and not 1 + }; + const result = await web3Client + .wallet() + .verifySignature(data, invalidSignature, signerPublicKey); + + expect(result).toBe(false); + + consoleSpy.mockRestore(); + }); + }); + + describe('getAccountBalance', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return balance for a valid address', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).publicApiClient.getAddresses = jest + .fn() + .mockResolvedValue([mockAddressesInfo[2]]); + + const ACCOUNT_ADDRESS = + 'AU12WVAJoH2giHAjSxk9R1XK3YhpCw2QxmkCbtXxcr4T3XCUG55nr'; + const expectedBalance = fromMAS(50); + + const balance = await web3Client + .wallet() + .getAccountBalance(ACCOUNT_ADDRESS!); + + expect(balance).not.toBeNull(); + expect(balance).toHaveProperty('candidate'); + expect(balance).toHaveProperty('final'); + expect(balance?.candidate).toEqual(expectedBalance); + expect(balance?.final).toEqual(expectedBalance); + }); + + test('should return null for an invalid address', async () => { + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => {}); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).publicApiClient.getAddresses = jest + .fn() + .mockRejectedValue(new Error('Invalid address')); + const invalidAddress = 'invalid address'; + + const balance = await web3Client + .wallet() + .getAccountBalance(invalidAddress); + + expect(balance).toBeNull(); + + // Verify that console.error was called + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to get account balance:', + expect.any(Error), + ); + + consoleSpy.mockRestore(); + }); + }); + + describe('addAccountsToWallet', () => { + test('should throw an error when the number of accounts exceeds the maximum limit', async () => { + const accounts = new Array(MAX_WALLET_ACCOUNTS + 1).fill(baseAccount); + await expect( + web3Client.wallet().addAccountsToWallet(accounts), + ).rejects.toThrow( + new RegExp( + `Maximum number of allowed wallet accounts exceeded ${MAX_WALLET_ACCOUNTS}`, + ), + ); + }); + test('should throw an error when an account private key is missing', async () => { + const accountWithoutKey = { ...baseAccount, secretKey: null }; + await expect( + web3Client.wallet().addAccountsToWallet([accountWithoutKey]), + ).rejects.toThrow(new Error('Missing account private key')); + }); + + test('should throw an error when the submitted public key does not match the private key', async () => { + const accountWithMismatchedPublicKey = { + ...baseAccount, + publicKey: 'mismatchedPublicKey', + }; + await expect( + web3Client + .wallet() + .addAccountsToWallet([accountWithMismatchedPublicKey]), + ).rejects.toThrow( + new Error( + 'Public key does not correspond the the private key submitted', + ), + ); + }); + + test('should throw an error when the account address does not match the private key-derived address', async () => { + const accountWithMismatchedAddress = { + ...baseAccount, + address: 'mismatchedAddress', + }; + await expect( + web3Client.wallet().addAccountsToWallet([accountWithMismatchedAddress]), + ).rejects.toThrow( + new Error('Account address not correspond the the address submitted'), + ); + }); + + test('should not add duplicate accounts to the wallet', async () => { + await web3Client.wallet().addAccountsToWallet([baseAccount, baseAccount]); + const walletAccounts = web3Client.wallet().getWalletAccounts(); + expect(walletAccounts.length).toBe(1); // only one unique account should be added + }); + + test('should correctly add accounts to the wallet', async () => { + const anotherAccount = await WalletClient.walletGenerateNewAccount(); + const anotherAccountBis = await WalletClient.walletGenerateNewAccount(); + const addedAccounts = await web3Client + .wallet() + .addAccountsToWallet([baseAccount, anotherAccount, anotherAccountBis]); + expect(addedAccounts.length).toBe(2); + // baseAccount should be ignored as it is already in the wallet + expect(addedAccounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ address: anotherAccount.address }), + expect.objectContaining({ address: anotherAccountBis.address }), + ]), + ); + }); + }); + + describe('getThreadNumber', () => { + test('should correctly compute the thread number for an account', async () => { + // create an account without providing the 'createdInThread' field + const account = await WalletClient.getAccountFromSecretKey( + receiverPrivateKey, + ); + delete account.createdInThread; + + await web3Client.wallet().setBaseAccount(account); + + // get the updated account (now with 'createdInThread' field) + const baseAccount = web3Client.wallet().getBaseAccount(); + + // manually compute the expected thread number + if (!account.address) { + throw new Error('Missing account address'); + } + const pubKeyHash = base58Decode(account.address.slice(2)); + const expectedThreadNumber = pubKeyHash.slice(1).readUInt8(0) >> 3; + + expect(baseAccount).not.toBeNull(); + expect(baseAccount?.createdInThread).toEqual(expectedThreadNumber); + }); + }); + + describe('sendTransaction, buyRolls & sellRolls', () => { + let receiverAccount: IAccount; + let mockTxData: ITransactionData; + let mockRollsData: IRollsData; + + // function to generate tests for sendTransaction, buyRolls & sellRolls to avoid code duplication + function generateTests( + operation: string, + operationTypeId: OperationTypeId, + data: () => IRollsData | ITransactionData, + ) { + beforeEach(async () => { + const spyGetNodeStatus = jest.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (web3Client.wallet() as any).publicApiClient, + 'getNodeStatus', + ); + spyGetNodeStatus.mockReturnValue(mockNodeStatusInfo); + + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(web3Client.wallet() as any, 'sendJsonRPCRequest') + .mockResolvedValue(mockOpIds); + }); + + test('should throw an error if no sender account is available for the transaction', async () => { + jest.spyOn(web3Client.wallet(), 'getBaseAccount').mockReturnValue(null); + + await expect(web3Client.wallet()[operation](data())).rejects.toThrow( + 'No tx sender available', + ); + }); + + test('should call compactBytesForOperation with correct arguments', async () => { + const spy = jest.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + web3Client.wallet() as any, + 'compactBytesForOperation', + ); + + await web3Client.wallet()[operation](data()); + + expect(spy).toHaveBeenCalledWith( + data(), + operationTypeId, + expect.any(Number), // expiryPeriod + ); + }); + + test('should call walletSignMessage with correct arguments', async () => { + const spy = jest.spyOn(WalletClient, 'walletSignMessage'); + + await web3Client.wallet()[operation](data()); + + expect(spy).toHaveBeenCalledWith( + expect.any(Buffer), // Buffer.concat([bytesPublicKey, bytesCompact]) + expect.any(Object), // sender + ); + }); + + test('should call sendJsonRPCRequest with correct arguments', async () => { + const spy = jest.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + web3Client.wallet() as any, + 'sendJsonRPCRequest', + ); + + await web3Client.wallet()[operation](data()); + + expect(spy).toHaveBeenCalledWith( + JSON_RPC_REQUEST_METHOD.SEND_OPERATIONS, + expect.any(Array), // [[data]] + ); + }); + + test('should return an array of operation ids', async () => { + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(web3Client.wallet() as any, 'sendJsonRPCRequest') + .mockResolvedValue(mockOpIds); + + const opIds = await web3Client.wallet()[operation](data()); + + expect(opIds).toEqual(mockOpIds); + }); + } + + beforeAll(async () => { + receiverAccount = await WalletClient.walletGenerateNewAccount(); + }); + + beforeEach(() => { + mockTxData = { + fee: 1n, + amount: 100n, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + recipientAddress: receiverAccount.address!, + }; + mockRollsData = { + fee: 1n, + amount: 100n, + }; + }); + + describe('sendTransaction', () => { + generateTests( + 'sendTransaction', + OperationTypeId.Transaction, + () => mockTxData, + ); + }); + + describe('buyRolls', () => { + generateTests('buyRolls', OperationTypeId.RollBuy, () => mockRollsData); + }); + + describe('sellRolls', () => { + generateTests('sellRolls', OperationTypeId.RollSell, () => mockRollsData); + }); + }); }); diff --git a/webpack.config.js b/webpack.config.js index 6424f4eb..a24a587a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,8 @@ const baseConfig = { fallback: { // Fallback for the buffer module, using the buffer module buffer: require.resolve('buffer/'), + fs: false, + path: false, }, // Extensions that are used to resolve modules extensions: ['.ts', '.js'],