diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index 48e2fa5de..35cd31c9b 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -20,7 +20,7 @@ jobs: token: ${{ secrets.ARGENTBOT_GITHUB_PAT }} - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "18.x" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index de3fb696e..d01ef95e8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -61,7 +61,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -84,7 +84,12 @@ jobs: run: pnpm run setup - name: Build extension for ${{ matrix.extension_type }} - run: pnpm run build:extension + run: | + if [[ "${{ matrix.extension_type }}" == "firefox" ]]; then + MANIFEST_VERSION=v2 pnpm run build:extension + else + MANIFEST_VERSION=v3 pnpm run build:extension + fi - name: Check bundlesize for ${{ matrix.extension_type }} run: pnpm bundlewatch @@ -136,7 +141,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -201,7 +206,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -258,7 +263,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "16" cache: "pnpm" @@ -302,7 +307,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -342,7 +347,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -403,7 +408,7 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" @@ -423,6 +428,8 @@ jobs: - name: Run tests run: | pnpm run --filter @argent/web start:ci & + sleep 10 && + curl http://localhost:3005 && pnpm run test:e2e:webwallet - name: Upload artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a45da55be..4c3f0e75d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,6 @@ on: branches: - release/* tags: - # - "@argent-x/extension@*" - "**" env: @@ -56,12 +55,14 @@ jobs: version: 8 run_install: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" cache: "pnpm" - run: pnpm run setup - - run: pnpm --filter=\!@argent/web build + + - name: Build Chrome version + run: pnpm --filter=\!@argent/web build - run: pnpm --filter @argent-x/dapp export diff --git a/.gitignore b/.gitignore index 50a60fe98..fc0045245 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ license-report.md **/.parcel-cache -vite.config.ts.timestamp-* \ No newline at end of file +vite.config.ts.timestamp-* + +report \ No newline at end of file diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 000000000..dbad11c63 --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,13 @@ +{ + "reporters": ["html", "console"], + "ignore": [ + "**/__snapshots__/**", + "**/__fixtures__/**", + "**/__tests__/**", + "**/*.test.*", + "*.json", + "*.svg" + ], + "minTokens": 75, + "absolute": true +} diff --git a/.nvmrc b/.nvmrc index 67a228a44..f6610cade 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.18.0 \ No newline at end of file +18.18.1 diff --git a/License.md b/License.md index 40d5de608..783a5a361 100644 --- a/License.md +++ b/License.md @@ -1,8 +1,9 @@ -All intellectual property rights, interests and titles in the code, application, software and documents (“Works”) are owned by Argent Labs Ltd (“Argent”). You are granted a non-exclusive, non-transferable licence to copy, modify, publish, adapt, and create derivative outputs of the Works (“Generated Works”) solely for non-commercial purposes. Non-commercial purposes means, at the sole discretion of Argent: -* Personal use without any commercial exploitation -* Public educational, public research or charitable use -* Use by any organisation provided that the monthly active users are less than 1000. +All intellectual property rights, interests and titles in the code, application, software and documents (“Works”) are owned by Argent Labs Ltd (“Argent”). You are granted a non-exclusive, non-transferable licence to copy, modify, publish, adapt, and create derivative outputs of the Works (“Generated Works”) solely for non-commercial purposes. Non-commercial purposes means, at the sole discretion of Argent: + +- Personal use without any commercial exploitation +- Public educational, public research or charitable use +- Use by any organisation provided that the monthly active users are less than 1000. + +A notice must be present in any Generated Works stating that the Works are owned by Argent and subject to these licence terms. You may not use, process or otherwise interact with the Works in any way not expressly permitted here. -A notice must be present in any Generated Works stating that the Works are owned by Argent and subject to these licence terms. You may not use, process or otherwise interact with the Works in any way not expressly permitted here. - If you have any questions, please contact us at legal@argent.xyz. diff --git a/Readme.md b/Readme.md index f25ac225a..2806f8d11 100644 --- a/Readme.md +++ b/Readme.md @@ -44,24 +44,19 @@ The example dapp is also contained in this repository. ## 🌐 Usage with your dapp -If you want to use this StarkNet Wallet extension with your dapp, the easiest way is to checkout the `@argent/get-starknet` package developed in this repo by running: +If you want to use this StarkNet Wallet extension with your dapp, the easiest way is to checkout the [starknetkit](https://github.com/argentlabs/starknetkit) package ```bash # starknet.js is a peer dependency -pnpm add @argent/get-starknet starknet +pnpm add starknetkit starknet ``` -The package is a light wrapper around [starknet.js](https://github.com/0xs34n/starknet.js) to interact with the wallet extension. You can then use it like the following: - ```javascript -import { connect } from "@argent/get-starknet" +import { connect } from "starknetkit" // Let the user pick a wallet (on button click) const starknet = connect() -// or try to connect to an approved wallet silently (on mount probably) -const starknet = connect({ showList: false }) - if (!starknet) { throw Error("User rejected wallet selection or silent connect found nothing") } diff --git a/diff-patches/briq-dapp.diff b/diff-patches/briq-dapp.diff deleted file mode 100644 index f5f28cc2b..000000000 --- a/diff-patches/briq-dapp.diff +++ /dev/null @@ -1,2481 +0,0 @@ -From 1991a84c11e41f8408d0c6ac3ecbe5df6f3fe7cc Mon Sep 17 00:00:00 2001 -From: Simon Heys -Date: Wed, 23 Nov 2022 14:48:14 +0000 -Subject: [PATCH] chore: invoke slow briq transaction - ---- - packages/dapp/abi/Briq.json | 703 +++++++++ - packages/dapp/abi/BriqProxy.json | 90 ++ - packages/dapp/src/components/TokenDapp.tsx | 3 + - packages/dapp/src/services/shape.json | 1578 +++++++++++++++++++ - packages/dapp/src/services/token.service.ts | 36 + - 5 files changed, 2410 insertions(+) - create mode 100644 packages/dapp/abi/Briq.json - create mode 100644 packages/dapp/abi/BriqProxy.json - create mode 100644 packages/dapp/src/services/shape.json - -diff --git a/packages/dapp/abi/Briq.json b/packages/dapp/abi/Briq.json -new file mode 100644 -index 00000000..0f3ac47c ---- /dev/null -+++ b/packages/dapp/abi/Briq.json -@@ -0,0 +1,703 @@ -+[ -+ { -+ "name": "Uint256", -+ "size": 2, -+ "type": "struct", -+ "members": [ -+ { -+ "name": "low", -+ "type": "felt", -+ "offset": 0 -+ }, -+ { -+ "name": "high", -+ "type": "felt", -+ "offset": 1 -+ } -+ ] -+ }, -+ { -+ "name": "FTSpec", -+ "size": 2, -+ "type": "struct", -+ "members": [ -+ { -+ "name": "token_id", -+ "type": "felt", -+ "offset": 0 -+ }, -+ { -+ "name": "qty", -+ "type": "felt", -+ "offset": 1 -+ } -+ ] -+ }, -+ { -+ "name": "ShapeItem", -+ "size": 2, -+ "type": "struct", -+ "members": [ -+ { -+ "name": "color_nft_material", -+ "type": "felt", -+ "offset": 0 -+ }, -+ { -+ "name": "x_y_z", -+ "type": "felt", -+ "offset": 1 -+ } -+ ] -+ }, -+ { -+ "data": [ -+ { -+ "name": "implementation", -+ "type": "felt" -+ } -+ ], -+ "keys": [], -+ "name": "Upgraded", -+ "type": "event" -+ }, -+ { -+ "data": [ -+ { -+ "name": "previousAdmin", -+ "type": "felt" -+ }, -+ { -+ "name": "newAdmin", -+ "type": "felt" -+ } -+ ], -+ "keys": [], -+ "name": "AdminChanged", -+ "type": "event" -+ }, -+ { -+ "name": "getAdmin_", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "admin", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "getImplementation_", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "implementation", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "upgradeImplementation_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "new_implementation", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "setRootAdmin_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "new_admin", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "getBriqAddress_", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "address", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "setBriqAddress_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "address", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "getAttributesRegistryAddress_", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "address", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "setAttributesRegistryAddress_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "address", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "data": [ -+ { -+ "name": "from_", -+ "type": "felt" -+ }, -+ { -+ "name": "to_", -+ "type": "felt" -+ }, -+ { -+ "name": "token_id_", -+ "type": "Uint256" -+ } -+ ], -+ "keys": [], -+ "name": "Transfer", -+ "type": "event" -+ }, -+ { -+ "name": "approve_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "to", -+ "type": "felt" -+ }, -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "setApprovalForAll_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "approved_address", -+ "type": "felt" -+ }, -+ { -+ "name": "is_approved", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "getApproved_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "approved", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "isApprovedForAll_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "on_behalf_of", -+ "type": "felt" -+ }, -+ { -+ "name": "address", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "is_approved", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "ownerOf_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "balanceOf_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "balance", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "balanceDetailsOf_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "token_ids_len", -+ "type": "felt" -+ }, -+ { -+ "name": "token_ids", -+ "type": "felt*" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "tokenOfOwnerByIndex_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ }, -+ { -+ "name": "index", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "transferFrom_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "sender", -+ "type": "felt" -+ }, -+ { -+ "name": "recipient", -+ "type": "felt" -+ }, -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "data": [ -+ { -+ "name": "_value_len", -+ "type": "felt" -+ }, -+ { -+ "name": "_value", -+ "type": "felt*" -+ }, -+ { -+ "name": "_id", -+ "type": "Uint256" -+ } -+ ], -+ "keys": [], -+ "name": "URI", -+ "type": "event" -+ }, -+ { -+ "name": "tokenURI_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "token_id", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "uri_len", -+ "type": "felt" -+ }, -+ { -+ "name": "uri", -+ "type": "felt*" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "assemble_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ }, -+ { -+ "name": "token_id_hint", -+ "type": "felt" -+ }, -+ { -+ "name": "name_len", -+ "type": "felt" -+ }, -+ { -+ "name": "name", -+ "type": "felt*" -+ }, -+ { -+ "name": "description_len", -+ "type": "felt" -+ }, -+ { -+ "name": "description", -+ "type": "felt*" -+ }, -+ { -+ "name": "fts_len", -+ "type": "felt" -+ }, -+ { -+ "name": "fts", -+ "type": "FTSpec*" -+ }, -+ { -+ "name": "nfts_len", -+ "type": "felt" -+ }, -+ { -+ "name": "nfts", -+ "type": "felt*" -+ }, -+ { -+ "name": "shape_len", -+ "type": "felt" -+ }, -+ { -+ "name": "shape", -+ "type": "ShapeItem*" -+ }, -+ { -+ "name": "attributes_len", -+ "type": "felt" -+ }, -+ { -+ "name": "attributes", -+ "type": "felt*" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "disassemble_", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ }, -+ { -+ "name": "token_id", -+ "type": "felt" -+ }, -+ { -+ "name": "fts_len", -+ "type": "felt" -+ }, -+ { -+ "name": "fts", -+ "type": "FTSpec*" -+ }, -+ { -+ "name": "nfts_len", -+ "type": "felt" -+ }, -+ { -+ "name": "nfts", -+ "type": "felt*" -+ }, -+ { -+ "name": "attributes_len", -+ "type": "felt" -+ }, -+ { -+ "name": "attributes", -+ "type": "felt*" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "name", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "name", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "symbol", -+ "type": "function", -+ "inputs": [], -+ "outputs": [ -+ { -+ "name": "symbol", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "tokenURI", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "tokenId", -+ "type": "Uint256" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "tokenURI_len", -+ "type": "felt" -+ }, -+ { -+ "name": "tokenURI", -+ "type": "felt*" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "tokenOfOwnerByIndex", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ }, -+ { -+ "name": "index", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "token_id", -+ "type": "Uint256" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "balanceOf", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "balance", -+ "type": "Uint256" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "balanceDetailsOf", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "tokenIds_len", -+ "type": "felt" -+ }, -+ { -+ "name": "tokenIds", -+ "type": "felt*" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "ownerOf", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "tokenId", -+ "type": "Uint256" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "transferFrom", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "from_", -+ "type": "felt" -+ }, -+ { -+ "name": "to", -+ "type": "felt" -+ }, -+ { -+ "name": "tokenId", -+ "type": "Uint256" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "approve", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "approved", -+ "type": "felt" -+ }, -+ { -+ "name": "tokenId", -+ "type": "Uint256" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "setApprovalForAll", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "operator", -+ "type": "felt" -+ }, -+ { -+ "name": "approved", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "getApproved", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "tokenId", -+ "type": "Uint256" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "approved", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ }, -+ { -+ "name": "isApprovedForAll", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "owner", -+ "type": "felt" -+ }, -+ { -+ "name": "operator", -+ "type": "felt" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "isApproved", -+ "type": "felt" -+ } -+ ], -+ "stateMutability": "view" -+ } -+] -diff --git a/packages/dapp/abi/BriqProxy.json b/packages/dapp/abi/BriqProxy.json -new file mode 100644 -index 00000000..e91201c8 ---- /dev/null -+++ b/packages/dapp/abi/BriqProxy.json -@@ -0,0 +1,90 @@ -+[ -+ { -+ "data": [ -+ { -+ "name": "implementation", -+ "type": "felt" -+ } -+ ], -+ "keys": [], -+ "name": "Upgraded", -+ "type": "event" -+ }, -+ { -+ "data": [ -+ { -+ "name": "previousAdmin", -+ "type": "felt" -+ }, -+ { -+ "name": "newAdmin", -+ "type": "felt" -+ } -+ ], -+ "keys": [], -+ "name": "AdminChanged", -+ "type": "event" -+ }, -+ { -+ "name": "constructor", -+ "type": "constructor", -+ "inputs": [ -+ { -+ "name": "admin", -+ "type": "felt" -+ }, -+ { -+ "name": "implentation_hash", -+ "type": "felt" -+ } -+ ], -+ "outputs": [] -+ }, -+ { -+ "name": "__default__", -+ "type": "function", -+ "inputs": [ -+ { -+ "name": "selector", -+ "type": "felt" -+ }, -+ { -+ "name": "calldata_size", -+ "type": "felt" -+ }, -+ { -+ "name": "calldata", -+ "type": "felt*" -+ } -+ ], -+ "outputs": [ -+ { -+ "name": "retdata_size", -+ "type": "felt" -+ }, -+ { -+ "name": "retdata", -+ "type": "felt*" -+ } -+ ] -+ }, -+ { -+ "name": "__l1_default__", -+ "type": "l1_handler", -+ "inputs": [ -+ { -+ "name": "selector", -+ "type": "felt" -+ }, -+ { -+ "name": "calldata_size", -+ "type": "felt" -+ }, -+ { -+ "name": "calldata", -+ "type": "felt*" -+ } -+ ], -+ "outputs": [] -+ } -+] -diff --git a/packages/dapp/src/components/TokenDapp.tsx b/packages/dapp/src/components/TokenDapp.tsx -index b08c29f5..b4e8d0a5 100644 ---- a/packages/dapp/src/components/TokenDapp.tsx -+++ b/packages/dapp/src/components/TokenDapp.tsx -@@ -7,6 +7,7 @@ import Erc20Abi from "../../abi/ERC20.json" - import { truncateAddress, truncateHex } from "../services/address.service" - import { - getErc20TokenAddress, -+ mintBriq, - mintToken, - parseInputAmountToUint256, - transfer, -@@ -187,6 +188,8 @@ export const TokenDapp: FC<{ - - return ( - <> -+ -+ -

- Transaction status: {transactionStatus} -

-diff --git a/packages/dapp/src/services/shape.json b/packages/dapp/src/services/shape.json -new file mode 100644 -index 00000000..947d42a8 ---- /dev/null -+++ b/packages/dapp/src/services/shape.json -@@ -0,0 +1,1578 @@ -+[ -+ { -+ "x_y_z": "3138550867693340379025494592775856268658610419246261338109", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340379025494592775856268658610419246261338110", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340379025494592775856268658610419246261338111", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549561", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549562", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549563", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549564", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549565", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549566", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549567", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549568", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549569", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379365776959696794732121985026678029549570", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195566912890036088209405", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195566912890036088209406", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195566912890036088209407", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195566912890036088209408", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195566912890036088209409", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761014", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761015", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761016", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761017", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761018", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761019", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761020", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761026", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761027", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195585359634109797761028", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195603806378183507312637", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195603806378183507312638", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195603806378183507312639", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195603806378183507312640", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340379706059326617733195603806378183507312641", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659011840753394146869245", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659011840753394146869246", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659011840753394146869247", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659011840753394146869248", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659011840753394146869249", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659030287497467856420859", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659030287497467856420860", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659030287497467856420866", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659030287497467856420867", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972466", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972467", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972468", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972469", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972470", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972471", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972472", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972473", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972474", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972484", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659048734241541565972485", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659067180985615275524090", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659067180985615275524091", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659067180985615275524092", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659067180985615275524098", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659067180985615275524099", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659085627729688985075709", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659085627729688985075710", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659085627729688985075711", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659085627729688985075712", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380046341693538671659085627729688985075713", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080701", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080702", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080703", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080704", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080705", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080706", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122475215360825915080707", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122493662104899624632315", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122493662104899624632316", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122493662104899624632324", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183918", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183919", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183920", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183921", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183922", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183923", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183924", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183925", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183926", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183927", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183928", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183929", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183930", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183941", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122512108848973334183942", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122530555593047043735544", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122530555593047043735545", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122530555593047043735546", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122530555593047043735555", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122530555593047043735556", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122549002337120753287163", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122549002337120753287164", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122549002337120753287165", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122549002337120753287169", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122549002337120753287170", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122567449081194462838782", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122567449081194462838783", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380386624060459610122567449081194462838784", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292157", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292158", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292159", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292160", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292161", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292162", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585938589968257683292163", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585957036712331392843772", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585957036712331392843780", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395383", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395384", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395385", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395386", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395387", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395397", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395398", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395399", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585975483456405102395400", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947003", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947004", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947011", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947012", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947013", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947014", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947015", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947016", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548585993930200478811947017", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498621", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498625", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498626", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498627", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498628", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498629", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498630", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498631", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498632", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498633", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586012376944552521498634", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050238", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050239", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050240", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050244", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050245", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050246", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050247", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050248", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050249", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050250", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586030823688626231050251", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601862", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601863", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601864", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601865", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601866", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601867", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340380726906427380548586049270432699940601868", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049401964575689451503614", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049401964575689451503615", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049420411319763161055229", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049420411319763161055232", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049420411319763161055233", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049420411319763161055234", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049420411319763161055235", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049438858063836870606843", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049438858063836870606844", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049438858063836870606852", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049438858063836870606853", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049457304807910580158461", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049457304807910580158465", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049457304807910580158466", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049457304807910580158467", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049475751551984289710078", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049475751551984289710079", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340381067188794301487049475751551984289710080", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512883785927194929266686", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512883785927194929266687", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818300", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818301", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818304", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818305", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818306", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818307", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818308", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512902232671268638818309", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512920679415342348369918", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512920679415342348369919", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381407471161222425512920679415342348369920", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976365607278700407029757", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976365607278700407029758", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976365607278700407029759", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976365607278700407029760", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976365607278700407029761", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976384054022774116581374", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340381747753528143363976384054022774116581375", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382088035895064302439828981886132175241213", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382088035895064302439828981886132175241214", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382088035895064302439828981886132175241215", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382088035895064302439828981886132175241216", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903292356493563943452669", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903292356493563943452670", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903292356493563943452671", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903292356493563943452672", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903292356493563943452673", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903310803237637653004286", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382428318261985240903310803237637653004287", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366737284356922002112510", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366737284356922002112511", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664124", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664125", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664128", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664129", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664130", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664131", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664132", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366755731100995711664133", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366774177845069421215742", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366774177845069421215743", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340382768600628906179366774177845069421215744", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830182212220280060772350", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830182212220280060772351", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830200658964353770323965", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830200658964353770323968", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830200658964353770323969", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830200658964353770323970", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830200658964353770323971", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830219105708427479875579", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830219105708427479875580", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830219105708427479875588", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830219105708427479875589", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830237552452501189427197", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830237552452501189427201", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830237552452501189427202", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830237552452501189427203", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830255999196574898978814", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830255999196574898978815", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383108882995827117830255999196574898978816", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983805", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983806", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983807", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983808", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983809", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983810", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293645586827711828983811", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293664033571785538535420", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293664033571785538535428", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087031", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087032", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087033", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087034", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087035", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087045", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087046", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087047", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293682480315859248087048", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638651", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638652", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638659", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638660", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638661", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638662", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638663", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638664", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293700927059932957638665", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190269", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190273", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190274", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190275", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190276", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190277", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190278", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190279", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190280", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190281", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293719373804006667190282", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741886", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741887", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741888", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741892", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741893", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741894", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741895", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741896", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741897", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741898", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293737820548080376741899", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293510", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293511", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293512", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293513", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293514", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293515", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383449165362748056293756267292154086293516", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195261", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195262", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195263", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195264", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195265", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195266", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757108961435143597195267", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757127408179217306746875", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757127408179217306746876", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757127408179217306746884", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298478", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298479", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298480", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298481", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298482", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298483", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298484", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298485", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298486", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298487", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298488", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298489", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298490", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298501", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757145854923291016298502", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757164301667364725850104", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757164301667364725850105", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757164301667364725850106", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757164301667364725850115", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757164301667364725850116", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757182748411438435401723", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757182748411438435401724", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757182748411438435401725", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757182748411438435401729", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757182748411438435401730", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757201195155512144953342", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757201195155512144953343", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340383789447729668994757201195155512144953344", -+ "color_nft_material": "863388526356616886719547138584847759673775999368590000129" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220572336042575365406717", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220572336042575365406718", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220572336042575365406719", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220572336042575365406720", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220572336042575365406721", -+ "color_nft_material": "862928631256424205058307739696806570879756044528328376321" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220590782786649074958331", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220590782786649074958332", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220590782786649074958338", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220590782786649074958339", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509938", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509939", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509940", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509941", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509942", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509943", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509944", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509945", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509946", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509956", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220609229530722784509957", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220627676274796494061562", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220627676274796494061563", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220627676274796494061564", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220627676274796494061570", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220627676274796494061571", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220646123018870203613181", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220646123018870203613182", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220646123018870203613183", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220646123018870203613184", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384129730096589933220646123018870203613185", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684054157394080843169789", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684054157394080843169790", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684054157394080843169791", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684054157394080843169792", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684054157394080843169793", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721398", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721399", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721400", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721401", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721402", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721403", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721404", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721410", -+ "color_nft_material": "863007951083702974962131122333181929676333928759399809025" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721411", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684072604138154552721412", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684091050882228262273021", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684091050882228262273022", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684091050882228262273023", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684091050882228262273024", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384470012463510871684091050882228262273025", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932857", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932858", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932859", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932860", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932861", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932862", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932863", -+ "color_nft_material": "863406046820051547330024641565735021191813180603506884609" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932864", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932865", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340384810294830431810147535978745586320932866", -+ "color_nft_material": "867699793691671751021422106963715347039123285304720490497" -+ }, -+ { -+ "x_y_z": "3138550867693340385150577197352748610999353353018089144317", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340385150577197352748610999353353018089144318", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ }, -+ { -+ "x_y_z": "3138550867693340385150577197352748610999353353018089144319", -+ "color_nft_material": "867986321097950682513628897842184107329320212870717243393" -+ } -+] -diff --git a/packages/dapp/src/services/token.service.ts b/packages/dapp/src/services/token.service.ts -index f9d8f71e..71e0ec8c 100644 ---- a/packages/dapp/src/services/token.service.ts -+++ b/packages/dapp/src/services/token.service.ts -@@ -2,7 +2,9 @@ import { getStarknet } from "@argent/get-starknet" - import { utils } from "ethers" - import { Abi, Contract, number, uint256 } from "starknet" - -+import Briq from "../../abi/Briq.json" - import Erc20Abi from "../../abi/ERC20.json" -+import BriqShape from "./shape.json" - - export const erc20TokenAddressByNetwork = { - "goerli-alpha": -@@ -68,3 +70,37 @@ export const transfer = async ( - parseInputAmountToUint256(transferAmount), - ) - } -+ -+export const mintBriq = async (): Promise => { -+ const starknet = getStarknet() -+ if (!starknet?.isConnected) { -+ throw Error("starknet wallet not connected") -+ } -+ const briq20Contract = new Contract( -+ Briq as Abi, -+ "0x065ee60db9e38ecdf4afb9e070466b81984ffbcd06bc8650b1a21133310255c8", // see https://testnet-2.starkscan.co/tx/0x0226c56673877b5dbaf51397100cf6f7dd9ab9855a12ce34b5143e8f8b787f16#overview -+ starknet.account as any, -+ ) -+ -+ const address = starknet.selectedAddress -+ -+ return briq20Contract.assemble_( -+ address, -+ "213501144513542153986070236384746678213", -+ ["1326866665790172270481434110879090"], -+ [ -+ "438574892578094810498916885946000485", -+ "229118056193845898057515121483212393", -+ "32199680925853042", -+ ], -+ [ -+ { -+ qty: "394", -+ token_id: "1", -+ }, -+ ], -+ [], -+ BriqShape, -+ ["25108406941546723055343157692830665664409421777856138051585", "2"], -+ ) -+} --- -2.37.0 - diff --git a/package.json b/package.json index 17d90ff86..f17db27d4 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,12 @@ "homepage": "https://github.com/argentlabs/argent-x/#readme", "devDependencies": { "@changesets/cli": "^2.26.1", - "@lavamoat/allow-scripts": "^2.3.1", - "@lavamoat/preinstall-always-fail": "^1.0.0", + "@lavamoat/preinstall-always-fail": "^2.0.0", + "@lavamoat/allow-scripts": "^3.0.0", "bundlewatch": "^0.3.3", "husky": "^8.0.3", "import-sort-style-module": "^6.0.0", - "lint-staged": "^14.0.0", + "lint-staged": "^15.0.0", "nx": "^16.2.2", "patch-package": "^8.0.0", "prettier": ">=2.8.8", diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 8dddfe69c..bc6180154 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -13,19 +13,18 @@ "@argent/shared": "^6.3.1", "@argent/get-starknet": "^6.3.1", "@argent/x-sessions": "^6.3.1", - "ethers": "^5.5.1", "next": "^13.4.6", "react": "^18.0.0", "react-dom": "^18.0.0", - "starknet": "5.18.0", + "starknet": "5.19.5", "micro-starknet": "^0.2.3" }, "devDependencies": { - "@types/node": "20.6.5", + "@types/node": "20.8.4", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "eslint": "8", - "eslint-config-next": "13.5.3", + "eslint-config-next": "13.5.4", "typescript": "^5.0.4" } } diff --git a/packages/dapp/src/components/TokenDapp.tsx b/packages/dapp/src/components/TokenDapp.tsx index 501f52d54..a30235b93 100644 --- a/packages/dapp/src/components/TokenDapp.tsx +++ b/packages/dapp/src/components/TokenDapp.tsx @@ -130,13 +130,12 @@ export const TokenDapp: FC<{ } } - const handleSignSubmit = async (e: React.FormEvent) => { + const handleSignSubmit = async (skipDeploy: boolean) => { try { - e.preventDefault() setTransactionStatus("approve") console.log("sign", shortText) - const result = await signMessage(shortText) + const result = await signMessage(shortText, skipDeploy) console.log(result) setLastSig(stark.formatSignature(result)) @@ -334,7 +333,12 @@ export const TokenDapp: FC<{
-
+ { + e.preventDefault() + handleSignSubmit(false) + }} + >

Sign Message

@@ -346,7 +350,16 @@ export const TokenDapp: FC<{ onChange={(e) => setShortText(e.target.value)} /> - +
+ + { + handleSignSubmit(true) + }} + /> +

Sign results

diff --git a/packages/dapp/src/services/token.service.ts b/packages/dapp/src/services/token.service.ts index a46ffcffb..549f6636e 100644 --- a/packages/dapp/src/services/token.service.ts +++ b/packages/dapp/src/services/token.service.ts @@ -18,7 +18,7 @@ export function parseInputAmountToUint256( input: string, decimals: number = 18, ) { - return getUint256CalldataFromBN(bigDecimal.parseUnits(input, decimals)) + return getUint256CalldataFromBN(bigDecimal.parseUnits(input, decimals).value) } export const mintToken = async (mintAmount: string): Promise => { diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts index 245a5ef93..0ad0c2bfd 100644 --- a/packages/dapp/src/services/wallet.service.ts +++ b/packages/dapp/src/services/wallet.service.ts @@ -76,31 +76,35 @@ export const getChainId = async ( } catch {} } -export const signMessage = async (message: string) => { +export const signMessage = async (message: string, skipDeploy = false) => { if (!windowStarknet?.isConnected) throw Error("starknet wallet not connected") if (!shortString.isShortString(message)) { throw Error("message must be a short string") } - return windowStarknet.account.signMessage({ - domain: { - name: "Example DApp", - chainId: windowStarknet.chainId, - version: "0.0.1", - }, - types: { - StarkNetDomain: [ - { name: "name", type: "felt" }, - { name: "chainId", type: "felt" }, - { name: "version", type: "felt" }, - ], - Message: [{ name: "message", type: "felt" }], - }, - primaryType: "Message", - message: { - message, + return windowStarknet.account.signMessage( + { + domain: { + name: "Example DApp", + chainId: windowStarknet.chainId, + version: "0.0.1", + }, + types: { + StarkNetDomain: [ + { name: "name", type: "felt" }, + { name: "chainId", type: "felt" }, + { name: "version", type: "felt" }, + ], + Message: [{ name: "message", type: "felt" }], + }, + primaryType: "Message", + message: { + message, + }, }, - }) + // @ts-ignore + { skipDeploy }, + ) } export const waitForTransaction = async (hash: string) => { diff --git a/packages/e2e/extension/src/config.ts b/packages/e2e/extension/src/config.ts index 45e6b3cf1..6a6335411 100644 --- a/packages/e2e/extension/src/config.ts +++ b/packages/e2e/extension/src/config.ts @@ -1,4 +1,11 @@ import path from "path" +import dotenv from "dotenv" +import fs from "fs" + +const envPath = path.resolve(__dirname, "../../.env") +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) +} export default { password: "MyP@ss3!", diff --git a/packages/e2e/extension/src/languages/ILanguage.ts b/packages/e2e/extension/src/languages/ILanguage.ts index 21f21beb4..86f15fd62 100644 --- a/packages/e2e/extension/src/languages/ILanguage.ts +++ b/packages/e2e/extension/src/languages/ILanguage.ts @@ -35,6 +35,10 @@ export interface ILanguage { confirmTheSeedPhrase: string showAccountRecovery: string wrongPassword: string + invalidStarkIdError: string + shortAddressError: string + invalidCheckSumError: string + invalidAddress: string } wallet: { //first screen diff --git a/packages/e2e/extension/src/languages/en/index.ts b/packages/e2e/extension/src/languages/en/index.ts index 8bae6b2ef..5472f525e 100644 --- a/packages/e2e/extension/src/languages/en/index.ts +++ b/packages/e2e/extension/src/languages/en/index.ts @@ -37,6 +37,10 @@ const texts = { recipientAddress: "Recipient's address", saveAddress: "Save address", wrongPassword: "Incorrect password", + invalidStarkIdError: " not found", + shortAddressError: "Address must be 66 characters long", + invalidCheckSumError: "Invalid address (checksum error)", + invalidAddress: "Invalid address", }, wallet: { //first screen diff --git a/packages/e2e/extension/src/page-objects/Account.ts b/packages/e2e/extension/src/page-objects/Account.ts index 711ddd7a1..baeea7d65 100644 --- a/packages/e2e/extension/src/page-objects/Account.ts +++ b/packages/e2e/extension/src/page-objects/Account.ts @@ -18,7 +18,7 @@ export default class Account extends Navigation { accountName2 = "Account 2" get noAccountBanner() { - return this.page.locator(`div h5:text-is("${lang.account.noAccounts}")`) + return this.page.locator(`div h5:has-text("${lang.account.noAccounts}")`) } get createAccount() { @@ -107,6 +107,30 @@ export default class Account extends Navigation { return this.page.locator('[data-testid="account-tokens"] h2') } + invalidStarkIdError(id: string) { + return this.page.locator( + `form label:has-text('${id}${lang.account.invalidStarkIdError}')`, + ) + } + + get shortAddressError() { + return this.page.locator( + `form label:has-text('${lang.account.shortAddressError}')`, + ) + } + + get invalidCheckSumError() { + return this.page.locator( + `form label:has-text('${lang.account.invalidCheckSumError}')`, + ) + } + + get invalidAddress() { + return this.page.locator( + `form label:has-text('${lang.account.invalidAddress}')`, + ) + } + async addAccountMainnet({ firstAccount = true }: { firstAccount?: boolean }) { if (firstAccount) { await this.createAccount.click() @@ -168,14 +192,14 @@ export default class Account extends Navigation { } return assetsList } - ////*[text() = 'Ethereum']/following-sibling::div + async ensureAsset(accountName: string, name: "Ethereum", value: string) { await this.ensureSelectedAccount(accountName) await expect( this.page.locator( `//*[text() = '${name}']/following-sibling::div/p[text() = '${value}']`, ), - ).toBeVisible({ timeout: 90000 }) + ).toBeVisible({ timeout: 1000 * 60 * 4 }) } async getTotalFeeValue() { @@ -189,6 +213,46 @@ export default class Account extends Navigation { return parseFloat(fee.split(" ")[0]) } + async transferAmount() { + /* + https://argent.atlassian.net/browse/BLO-1713 + const sendTitleText = await this.page + .locator('[data-testid="send-title"]') + .innerText() + const sendTitleAmount = sendTitleText.split(" ")[1] + const balanceChange = await this.page + .locator("[data-value]") + .first() + .getAttribute("data-value") + expect(balanceChange).toBe(sendTitleAmount) + */ + const amount = await this.page + .locator('[data-testid="send-title"]') + .getAttribute("data-value") + return amount + } + + async fillRecipientAddress({ + recipientAddress, + fillRecipientAddress = "paste", + validAddress = true, + }: { + recipientAddress: string + fillRecipientAddress?: "typing" | "paste" + validAddress?: boolean + }) { + fillRecipientAddress === "paste" + ? await this.recipientAddressQuery.fill(recipientAddress) + : await this.recipientAddressQuery.type(recipientAddress) + if (validAddress) { + if (recipientAddress.endsWith("stark")) { + await this.page.click(`button:has-text("${recipientAddress}")`) + } else { + await this.recipientAddressQuery.focus() + await this.page.keyboard.press("Enter") + } + } + } async transfer({ originAccountName, recipientAddress, @@ -206,15 +270,7 @@ export default class Account extends Navigation { }) { await this.ensureSelectedAccount(originAccountName) await this.token(tokenName).click() - fillRecipientAddress === "paste" - ? await this.recipientAddressQuery.fill(recipientAddress) - : await this.recipientAddressQuery.type(recipientAddress) - if (recipientAddress.endsWith("stark")) { - await this.page.click(`button:has-text("${recipientAddress}")`) - } else { - await this.recipientAddressQuery.focus() - await this.page.keyboard.press("Enter") - } + await this.fillRecipientAddress({ recipientAddress, fillRecipientAddress }) if (amount === "MAX") { await expect(this.balance).toBeVisible() await expect(this.sendMax).toBeVisible() @@ -224,7 +280,11 @@ export default class Account extends Navigation { } await this.reviewSend.click() - submit ?? (await this.approve.click()) + const trxAmount = await this.transferAmount() + if (submit) { + await this.approve.click() + } + return Math.abs(parseFloat(trxAmount!)) } async ensureTokenBalance({ diff --git a/packages/e2e/extension/src/page-objects/Activity.ts b/packages/e2e/extension/src/page-objects/Activity.ts index 978590eaa..dd229d9d7 100644 --- a/packages/e2e/extension/src/page-objects/Activity.ts +++ b/packages/e2e/extension/src/page-objects/Activity.ts @@ -16,6 +16,14 @@ export default class Activity extends Navigation { ).toBeVisible() } + ensureNoPendingTransactions() { + return expect( + this.page.locator( + `h6 div:text-is("${lang.account.pendingTransactions}") >> div`, + ), + ).not.toBeVisible({ timeout: 60000 }) + } + activityByDestination(destination: string) { return this.page.locator( `//button//p[contains(text()[1], 'To: ') and contains(text()[2], '${destination}')]`, @@ -28,4 +36,12 @@ export default class Activity extends Navigation { this.ensurePendingTransactions(nbr), ]) } + + async activityTxHashs() { + await expect( + this.page.locator("button[data-tx-hash]").first(), + ).toBeVisible() + const loc = await this.page.locator("button[data-tx-hash]").all() + return Promise.all(loc.map((el) => el.getAttribute("data-tx-hash"))) + } } diff --git a/packages/e2e/extension/src/page-objects/ExtensionPage.ts b/packages/e2e/extension/src/page-objects/ExtensionPage.ts index e0cf8a868..6f0dc5cf3 100644 --- a/packages/e2e/extension/src/page-objects/ExtensionPage.ts +++ b/packages/e2e/extension/src/page-objects/ExtensionPage.ts @@ -11,7 +11,7 @@ import Network from "./Network" import Settings from "./Settings" import Wallet from "./Wallet" import config from "../config" -import { balanceEther, transferEth, AccountsToSetup } from "../utils/account" +import { transferEth, AccountsToSetup, validateTx } from "../utils/account" export default class ExtensionPage { page: Page @@ -140,14 +140,11 @@ export default class ExtensionPage { return { accountAddresses, seed } } - async validateTx(address: string) { - const initialBalance = await balanceEther(address) - await this.navigation.approve.click() - await this.activity.checkActivity(1) - await expect( - this.navigation.menuPendingTransactionsIndicator, - ).not.toBeVisible({ timeout: 60000 }) - const finalBalance = await balanceEther(address) - expect(parseFloat(finalBalance)).toBeGreaterThan(parseFloat(initialBalance)) + async validateTx(reciever: string, amount?: number) { + console.log(reciever, amount) + await this.navigation.menuActivity.click() + await this.activity.ensureNoPendingTransactions() + const txs = await this.activity.activityTxHashs() + await validateTx(txs[0]!, reciever, amount) } } diff --git a/packages/e2e/extension/src/page-objects/Network.ts b/packages/e2e/extension/src/page-objects/Network.ts index 0a761f319..fb52427c6 100644 --- a/packages/e2e/extension/src/page-objects/Network.ts +++ b/packages/e2e/extension/src/page-objects/Network.ts @@ -58,4 +58,8 @@ export default class Network { throw new Error(`Unknown ARGENTX_Network: ${defaultNetworkId}`) } } + + ensureSelectedNetwork(networkName: NetworkName) { + return expect(this.networkSelector).toHaveText(networkName) + } } diff --git a/packages/e2e/extension/src/specs/addressBook.spec.ts b/packages/e2e/extension/src/specs/addressBook.spec.ts index dca2eecad..71b7ac37c 100644 --- a/packages/e2e/extension/src/specs/addressBook.spec.ts +++ b/packages/e2e/extension/src/specs/addressBook.spec.ts @@ -41,6 +41,7 @@ test.describe("Address Book", () => { await extension.addressBook.addressByName("New name").click() await extension.account.sendMax.click() await extension.navigation.reviewSend.click() + await extension.navigation.confirm.click() await extension.validateTx(config.account1Seed2!) @@ -76,6 +77,7 @@ test.describe("Address Book", () => { await extension.account.sendMax.click() await extension.navigation.reviewSend.click() + await extension.navigation.confirm.click() await extension.validateTx(config.account1Seed2!) }) @@ -102,6 +104,7 @@ test.describe("Address Book", () => { await extension.account.sendMax.click() await extension.navigation.reviewSend.click() + await extension.navigation.confirm.click() await extension.validateTx(config.account1Seed2!) }) diff --git a/packages/e2e/extension/src/specs/dappsBanner.spec.ts b/packages/e2e/extension/src/specs/dappsBanner.spec.ts index b4af268ce..f2d3c340a 100644 --- a/packages/e2e/extension/src/specs/dappsBanner.spec.ts +++ b/packages/e2e/extension/src/specs/dappsBanner.spec.ts @@ -1,11 +1,14 @@ import { expect } from "@playwright/test" import test from "../test" +import config from "../config" test.describe("Banner", () => { test("dapps banner should be visible after login", async ({ extension }) => { - await extension.wallet.newWalletOnboarding() - await extension.open() + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await expect(extension.network.networkSelector).toBeVisible() await expect(extension.account.dappsBanner).toBeVisible() let href = await extension.account.dappsBanner.getAttribute("href") @@ -19,8 +22,10 @@ test.describe("Banner", () => { test("dapps banner should not be visible after dismissed", async ({ extension, }) => { - await extension.wallet.newWalletOnboarding() - await extension.open() + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await expect(extension.network.networkSelector).toBeVisible() await expect(extension.account.dappsBanner).toBeVisible() await extension.account.dappsBannerClose.click() @@ -30,16 +35,8 @@ test.describe("Banner", () => { test("dapps banner shoud be visible after account recovery", async ({ extension, }) => { - const { seed } = await extension.setupWallet({ - accountsToSetup: [ - { - initialBalance: 0, - }, - ], - }) - - await extension.resetExtension() - await extension.recoverWallet(seed) + await extension.open() + await extension.recoverWallet(config.testNetSeed1!) await expect(extension.account.dappsBanner).toBeVisible() }) }) diff --git a/packages/e2e/extension/src/specs/invalidAddress.spec.ts b/packages/e2e/extension/src/specs/invalidAddress.spec.ts new file mode 100644 index 000000000..987616a56 --- /dev/null +++ b/packages/e2e/extension/src/specs/invalidAddress.spec.ts @@ -0,0 +1,74 @@ +import { expect } from "@playwright/test" +import test from "../test" + +test.describe("Invalid address", () => { + test("Invalid starknet id", async ({ extension }) => { + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await extension.account.ensureSelectedAccount( + extension.account.accountName1, + ) + await extension.account.token("Ethereum").click() + await extension.account.fillRecipientAddress({ + recipientAddress: "e2e-test5345346eertgegeggfgdgdgdfgdgdf.stark", + validAddress: false, + }) + await expect( + extension.account.invalidStarkIdError( + "e2e-test5345346eertgegeggfgdgdgdfgdgdf.stark", + ), + ).toBeVisible() + }) + + test("Invalid address (short address)", async ({ extension }) => { + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await extension.account.ensureSelectedAccount( + extension.account.accountName1, + ) + await extension.account.token("Ethereum").click() + await extension.account.fillRecipientAddress({ + recipientAddress: + "0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3a", + validAddress: false, + }) + await extension.page.keyboard.press("Enter") + await expect(extension.account.shortAddressError).toBeVisible() + }) + + test("Invalid address (checksum error)", async ({ extension }) => { + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await extension.account.ensureSelectedAccount( + extension.account.accountName1, + ) + await extension.account.token("Ethereum").click() + await extension.account.fillRecipientAddress({ + recipientAddress: + "0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3a3", + validAddress: false, + }) + await extension.page.keyboard.press("Enter") + await expect(extension.account.invalidCheckSumError).toBeVisible() + }) + + test("Invalid address", async ({ extension }) => { + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.0001 }], + }) + await extension.account.ensureSelectedAccount( + extension.account.accountName1, + ) + await extension.account.token("Ethereum").click() + await extension.account.fillRecipientAddress({ + recipientAddress: + "0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3aq", + validAddress: false, + }) + await extension.page.keyboard.press("Enter") + await expect(extension.account.invalidAddress).toBeVisible() + }) +}) diff --git a/packages/e2e/extension/src/specs/network.spec.ts b/packages/e2e/extension/src/specs/network.spec.ts index 461625ac8..aa87569f4 100644 --- a/packages/e2e/extension/src/specs/network.spec.ts +++ b/packages/e2e/extension/src/specs/network.spec.ts @@ -38,7 +38,7 @@ test.describe("Network", () => { await extension.navigation.back.click() await extension.navigation.back.click() await extension.navigation.close.click() - + await extension.network.ensureSelectedNetwork("Testnet") // select network await extension.network.selectNetwork("My Network") @@ -55,7 +55,9 @@ test.describe("Network", () => { await extension.navigation.back.click() await extension.navigation.back.click() await extension.navigation.close.click() - + await extension.network.ensureSelectedNetwork("My Network") + await expect(extension.account.createAccount).toBeVisible() + await expect(extension.account.noAccountBanner).toBeVisible() // select other network await extension.network.selectNetwork("Testnet") // delete network diff --git a/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts b/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts new file mode 100644 index 000000000..252606bf2 --- /dev/null +++ b/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts @@ -0,0 +1,77 @@ +import { expect } from "@playwright/test" + +import config from "../config" +import test from "../test" + +test.describe("Send funds", () => { + test("send MAX funds to other self account", async ({ extension }) => { + const { accountAddresses } = await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }], + }) + const amountTrx = await extension.account.transfer({ + originAccountName: extension.account.accountName1, + recipientAddress: accountAddresses[1], + tokenName: "Ethereum", + amount: "MAX", + }) + await extension.validateTx(accountAddresses[1], amountTrx) + await extension.navigation.menuTokens.click() + + //ensure that balance is updated + await expect(extension.account.currentBalance("Ethereum")).toContainText( + "0.00", + { timeout: 60000 }, + ) + let balance = await extension.account.currentBalance("Ethereum").innerText() + expect(parseFloat(balance)).toBeLessThan(0.01) + + await extension.account.ensureSelectedAccount( + extension.account.accountName2, + ) + await expect(extension.account.currentBalance("Ethereum")).toContainText( + "0.01", + { timeout: 60000 }, + ) + balance = await extension.account.currentBalance("Ethereum").innerText() + expect(parseFloat(balance)).toBeGreaterThan(0.0001) + }) + + test("send MAX funds to other wallet/account", async ({ extension }) => { + await extension.setupWallet({ + accountsToSetup: [{ initialBalance: 0.002 }], + }) + + const amountTrx = await extension.account.transfer({ + originAccountName: extension.account.accountName1, + recipientAddress: config.destinationAddress!, + tokenName: "Ethereum", + amount: "MAX", + }) + + await extension.validateTx(config.destinationAddress!, amountTrx) + await extension.navigation.menuTokens.click() + + //ensure that balance is updated + await expect(extension.account.currentBalance("Ethereum")).toContainText( + "0.000", + { timeout: 60000 }, + ) + const balance = await extension.account + .currentBalance("Ethereum") + .innerText() + expect(parseFloat(balance)).toBeLessThan(0.002) + }) + + test("User should be able to send funds to starknet id", async ({ + extension, + }) => { + await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] }) + const amountTrx = await extension.account.transfer({ + originAccountName: extension.account.accountName1, + recipientAddress: "e2e-test.stark", + tokenName: "Ethereum", + amount: "MAX", + }) + await extension.validateTx(config.account1Seed3!, amountTrx) + }) +}) diff --git a/packages/e2e/extension/src/specs/sendFunds.spec.ts b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts similarity index 50% rename from packages/e2e/extension/src/specs/sendFunds.spec.ts rename to packages/e2e/extension/src/specs/sendPartialFunds.spec.ts index 4b49ab77d..7eb52ff23 100644 --- a/packages/e2e/extension/src/specs/sendFunds.spec.ts +++ b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts @@ -4,81 +4,24 @@ import config from "../config" import test from "../test" test.describe("Send funds", () => { - test("send MAX funds to other self account", async ({ extension }) => { - const { accountAddresses } = await extension.setupWallet({ - accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }], - }) - await extension.account.transfer({ - originAccountName: extension.account.accountName1, - recipientAddress: accountAddresses[1], - tokenName: "Ethereum", - amount: "MAX", - submit: false, - }) - - await extension.validateTx(accountAddresses[1]) - await extension.navigation.menuTokens.click() - - //ensure that balance is updated - await expect(extension.account.currentBalance("Ethereum")).toContainText( - "0.00", - ) - let balance = await extension.account.currentBalance("Ethereum").innerText() - expect(parseFloat(balance)).toBeLessThan(0.01) - - await extension.account.ensureSelectedAccount( - extension.account.accountName2, - ) - await expect(extension.account.currentBalance("Ethereum")).toContainText( - "0.01", - ) - balance = await extension.account.currentBalance("Ethereum").innerText() - expect(parseFloat(balance)).toBeGreaterThan(0.0001) - }) - - test("send MAX funds to other wallet/account", async ({ extension }) => { - await extension.setupWallet({ - accountsToSetup: [{ initialBalance: 0.002 }], - }) - - await extension.account.transfer({ - originAccountName: extension.account.accountName1, - recipientAddress: config.destinationAddress!, - tokenName: "Ethereum", - amount: "MAX", - submit: false, - }) - - await extension.validateTx(config.destinationAddress!) - await extension.navigation.menuTokens.click() - - //ensure that balance is updated - await expect(extension.account.currentBalance("Ethereum")).toContainText( - "0.000", - ) - const balance = await extension.account - .currentBalance("Ethereum") - .innerText() - expect(parseFloat(balance)).toBeLessThan(0.002) - }) - test("send partial funds to other self account", async ({ extension }) => { const { accountAddresses } = await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }], }) - await extension.account.transfer({ + const amountTrx = await extension.account.transfer({ originAccountName: extension.account.accountName1, recipientAddress: accountAddresses[1], tokenName: "Ethereum", amount: 0.005, - submit: false, }) - await extension.validateTx(accountAddresses[1]) + expect(amountTrx).toBe(0.005) + await extension.validateTx(accountAddresses[1], amountTrx) await extension.navigation.menuTokens.click() //ensure that balance is updated await expect(extension.account.currentBalance("Ethereum")).toContainText( "0.00", + { timeout: 60000 }, ) const balance = await extension.account .currentBalance("Ethereum") @@ -90,25 +33,27 @@ test.describe("Send funds", () => { ) await expect(extension.account.currentBalance("Ethereum")).toContainText( "0.005", + { timeout: 60000 }, ) }) test("send partial funds to other wallet/account", async ({ extension }) => { await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] }) - await extension.account.transfer({ + const amountTrx = await extension.account.transfer({ originAccountName: extension.account.accountName1, recipientAddress: config.destinationAddress!, tokenName: "Ethereum", amount: 0.005, fillRecipientAddress: "typing", - submit: false, }) - await extension.validateTx(config.destinationAddress!) + expect(amountTrx).toBe(0.005) + await extension.validateTx(config.destinationAddress!, amountTrx) await extension.navigation.menuTokens.click() //ensure that balance is updated await expect(extension.account.currentBalance("Ethereum")).toContainText( "0.00", + { timeout: 60000 }, ) const balance = await extension.account .currentBalance("Ethereum") @@ -128,14 +73,12 @@ test.describe("Send funds", () => { extension, }) => { await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] }) - - await extension.account.transfer({ + const amountTrx = await extension.account.transfer({ originAccountName: extension.account.accountName1, recipientAddress: "e2e-test.stark", tokenName: "Ethereum", amount: "MAX", - submit: false, }) - await extension.validateTx(config.account1Seed3!) + await extension.validateTx(config.account1Seed3!, amountTrx) }) }) diff --git a/packages/e2e/extension/src/test.ts b/packages/e2e/extension/src/test.ts index 8d511ce9b..1b2bbbba2 100644 --- a/packages/e2e/extension/src/test.ts +++ b/packages/e2e/extension/src/test.ts @@ -19,6 +19,15 @@ import ExtensionPage from "./page-objects/ExtensionPage" const isCI = Boolean(process.env.CI) const isExtensionURL = (url: string) => url.startsWith("chrome-extension://") let browserCtx: ChromiumBrowserContext +const outputFolder = (testInfo: TestInfo) => + testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "") +const artifactFilename = (testInfo: TestInfo) => + `${testInfo.retry}-${testInfo.status}-${pageId++}-${testInfo.workerIndex}` +const keepArtifacts = (testInfo: TestInfo) => + testInfo.config.preserveOutput === "always" || + (testInfo.config.preserveOutput === "failures-only" && + testInfo.status === "failed") || + testInfo.status === "timedOut" const closePages = async (browserContext: ChromiumBrowserContext) => { const pages = browserContext?.pages() || [] for (const page of pages) { @@ -29,33 +38,28 @@ const closePages = async (browserContext: ChromiumBrowserContext) => { } } -const keepArtifacts = async (testInfo: TestInfo, page: Page) => { - if ( - testInfo.config.preserveOutput === "always" || - (testInfo.config.preserveOutput === "failures-only" && - testInfo.status !== "passed") - ) { - //save HTML - const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "") - const filename = `${testInfo.retry}-${testInfo.status}-${pageId}-${testInfo.workerIndex}.html` - try { - const htmlContent = await page.content() - await fs.promises - .mkdir(path.resolve(config.artifactsDir, folder), { recursive: true }) - .catch((error) => { - console.error(error) - }) - await fs.promises - .writeFile( - path.resolve(config.artifactsDir, folder, filename), - htmlContent, - ) - .catch((error) => { - console.error(error) - }) - } catch (error) { - console.error("Error while saving HTML content", error) - } +const keepHtml = async (testInfo: TestInfo, page: Page) => { + if (keepArtifacts(testInfo)) { + const htmlContent = await page.content() + await fs.promises + .mkdir(path.resolve(config.artifactsDir, outputFolder(testInfo)), { + recursive: true, + }) + .catch((error) => { + console.error(error) + }) + await fs.promises + .writeFile( + path.resolve( + config.artifactsDir, + outputFolder(testInfo), + `${artifactFilename(testInfo)}.html`, + ), + htmlContent, + ) + .catch((error) => { + console.error(error) + }) } } @@ -69,8 +73,9 @@ const createBrowserContext = () => { "--ipc=host", `--disable-extensions-except=${config.distDir}`, `--load-extension=${config.distDir}`, - "--disable-gp", + "--disable-gpu", ], + ignoreDefaultArgs: ["--disable-component-extensions-with-background-pages"], recordVideo: { dir: config.artifactsDir, size: { @@ -94,25 +99,21 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => { }) page.on("close", async (page) => { - if ( - testInfo.config.preserveOutput === "always" || - (testInfo.config.preserveOutput === "failures-only" && - testInfo.status === "failed") || - testInfo.status === "timedOut" - ) { - const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "") - const filename = `${testInfo.retry}-${testInfo.status}-${pageId++}-${ - testInfo.workerIndex - }.webm` - + if (keepArtifacts(testInfo)) { await page .video() - ?.saveAs(path.resolve(config.artifactsDir, folder, filename)) + ?.saveAs( + path.resolve( + config.artifactsDir, + outputFolder(testInfo), + `${artifactFilename(testInfo)}.webm`, + ), + ) .catch((error) => { console.error(error) }) } - page + await page .video() ?.delete() .catch((error) => { @@ -157,16 +158,30 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => { return { browserContext, extensionURL, page } } +//delete videos related with chrome://extensions/ page +function cleanArtifactDir() { + try { + fs.readdirSync(config.artifactsDir) + .filter((f) => f.endsWith("webm")) + .forEach((fileToDelete) => + fs.rmSync(`${config.artifactsDir}/${fileToDelete}`), + ) + } catch (error) { + console.log(error) + } +} + function createExtension() { return async ({}, use: any, testInfo: TestInfo) => { const { browserContext, page, extensionURL } = await initBrowserWithExtension(testInfo) const extension = new ExtensionPage(page, extensionURL) - await keepArtifacts(testInfo, page) await closePages(browserContext) browserCtx = browserContext await use(extension) + await keepHtml(testInfo, page) await browserContext.close() + cleanArtifactDir() } } diff --git a/packages/e2e/extension/src/utils/account.ts b/packages/e2e/extension/src/utils/account.ts index 9c805ea9f..c248aad4b 100644 --- a/packages/e2e/extension/src/utils/account.ts +++ b/packages/e2e/extension/src/utils/account.ts @@ -1,7 +1,16 @@ -import { Account, SequencerProvider, constants, uint256 } from "starknet" +import { + Account, + SequencerProvider, + constants, + uint256, + num, + GetTransactionReceiptResponse, + TransactionExecutionStatus, +} from "starknet" import { bigDecimal } from "@argent/shared" -import { Multicall } from "@argent/x-multicall" +import { getBatchProvider } from "@argent/x-multicall" import config from "../config" +import { expect } from "@playwright/test" export interface AccountsToSetup { initialBalance: number @@ -17,6 +26,22 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) const maxRetries = 4 +const isEqualAddress = (a: string, b: string) => { + try { + return ( + num.hexToDecimalString(a.toLocaleLowerCase()) === + num.hexToDecimalString(b.toLocaleLowerCase()) + ) + } catch { + // ignore parsing error + } + return false +} + +const formatAmount = (amount: string) => { + return parseInt(amount, 16) / Math.pow(10, 18) +} + const getTransaction = async (tx: string) => { return fetch( `${config.starknetTestNetUrl}/feeder_gateway/get_transaction?transactionHash=${tx}`, @@ -25,6 +50,9 @@ const getTransaction = async (tx: string) => { } export async function transferEth(amount: string, to: string) { + console.log( + "########################### transferEth ##################################", + ) const account = new Account( provider, config.senderAddr!, @@ -39,7 +67,12 @@ export async function transferEth(amount: string, to: string) { throw `Failed to tranfer: Not enought balance ${initialBalanceFormatted} < ${amount}` } let placeTXAttempt = 0 + let txHash while (placeTXAttempt < maxRetries) { + /** timemout if we don't receive a valid execution response */ + const placeTXTimeout = setTimeout(() => { + throw new Error("Place tx timed out") + }, 30 * 1000) /** 30 seconds */ try { placeTXAttempt++ const tx = await account.execute({ @@ -47,41 +80,63 @@ export async function transferEth(amount: string, to: string) { entrypoint: "transfer", calldata: [to, low, high], }) - let failed = true - let txSuccessAttempt = 0 + txHash = tx.transaction_hash let txStatusResponse - while (failed && txSuccessAttempt < maxRetries) { - txSuccessAttempt++ + let hasExecutionStatus = false + while (!hasExecutionStatus) { const txStatus = await getTransaction(tx.transaction_hash) - txStatusResponse = await txStatus.json() - if (txStatusResponse.execution_status === "REJECTED") { - console.error( - `Failed to place TX: ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}`, - ) - txSuccessAttempt = maxRetries - } else if ( - txStatusResponse.execution_status !== "SUCCEEDED" && - txStatusResponse.status !== "ACCEPTED_ON_L2" - ) { + txStatusResponse = + (await txStatus.json()) as GetTransactionReceiptResponse + hasExecutionStatus = + "execution_status" in txStatusResponse + ? Boolean(txStatusResponse.execution_status) + : false + if (!hasExecutionStatus) { console.log( - `TX not processed: hash: ${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}`, + `[TX awating execution_status] hash: ${tx.transaction_hash}, status: ${txStatusResponse.status}`, ) await sleep(5000) - } else { - failed = false } } - if (failed) { - throw `Failed to place TX: ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}` + if ( + txStatusResponse && + "execution_status" in txStatusResponse && + txStatusResponse.execution_status === + TransactionExecutionStatus.SUCCEEDED + ) { + console.log( + `[TX execution_status ${TransactionExecutionStatus.SUCCEEDED}] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`, + ) + return tx.transaction_hash } - - console.log( - `[Successful TX] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`, - ) - return tx.transaction_hash + const elements = [ + `[Failed to place TX] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`, + ] + if (txStatusResponse) { + if ("execution_status" in txStatusResponse) { + elements.push( + `execution_status: ${txStatusResponse.execution_status}`, + ) + } + if ("revert_reason" in txStatusResponse) { + elements.push(`revert_reason: ${txStatusResponse.revert_reason}`) + } + if ("transaction_failure_reason" in txStatusResponse) { + elements.push( + `transaction_failure_reason.error_message: ${txStatusResponse.transaction_failure_reason.error_message}`, + ) + } + elements.push(`status: ${txStatusResponse.status}`) + } else { + elements.push("unable to get tx status response") + } + const message = elements.join(", ") + console.error(message) } catch (e) { //for debug only - console.log("Exception: ", e) + console.log(`Exception: ${txHash}`, e) + } finally { + clearTimeout(placeTXTimeout) } console.warn("Transfer failed, going to try again ") } @@ -94,9 +149,26 @@ export async function balanceEther(accountAddress: string) { calldata: [accountAddress], } - const multicall = new Multicall(provider) - const response = await multicall.call(balanceOfCall) - const [low, high] = response + const multicall = getBatchProvider(provider) + const { result } = await multicall.callContract(balanceOfCall) + const [low, high] = result const balance = bigDecimal.formatEther(uint256.uint256ToBN({ low, high })) - return balance + return parseFloat(balance).toFixed(4) +} + +export async function validateTx( + txHash: string, + reciever: string, + amount?: number, +) { + await provider.waitForTransaction(txHash) + const txData = await provider.getTransaction(txHash) + if (!("calldata" in txData)) { + throw new Error(`Invalid transaction data: ${JSON.stringify(txData)}`) + } + const accAdd = txData.calldata[4].toString() + expect(isEqualAddress(accAdd, reciever)).toBe(true) + if (amount) { + expect(formatAmount(txData.calldata[5].toString())).toBe(amount) + } } diff --git a/packages/e2e/package.json b/packages/e2e/package.json index ea85a1d27..ae54f3966 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -6,12 +6,12 @@ "license": "MIT", "devDependencies": { "@argent/shared": "workspace:^", - "@argent/x-multicall": "workspace:^", + "@argent/x-multicall": "^7.0.8", "@playwright/test": "^1.37.1", "@types/node": "^20.5.7", "@types/uuid": "^9.0.3", "dotenv": "^16.3.1", - "starknet": "5.18.0", + "starknet": "5.19.5", "uuid": "^9.0.0" }, "scripts": { diff --git a/packages/e2e/webwallet/src/specs/login.spec.ts b/packages/e2e/webwallet/src/specs/login.spec.ts index d994d05a9..a14ccfa0c 100644 --- a/packages/e2e/webwallet/src/specs/login.spec.ts +++ b/packages/e2e/webwallet/src/specs/login.spec.ts @@ -4,11 +4,11 @@ import config from "../config" import test from "../test" test.describe(`Login page`, () => { - test.skip("can log in", async ({ webWallet }) => { + test("can log in", async ({ webWallet }) => { await webWallet.login.success() }) - test.skip("wrong password", async ({ webWallet }) => { + test("wrong password", async ({ webWallet }) => { await webWallet.login.email.fill(config.validLogin.email) await webWallet.login.fillPin(config.validLogin.pin) await webWallet.login.password.fill("VeryFake123!") diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md index c12c764a2..9779ba1da 100644 --- a/packages/extension/CHANGELOG.md +++ b/packages/extension/CHANGELOG.md @@ -1,5 +1,23 @@ # @argent-x/extension +## 6.10.1 + +### Patch Changes + +- 7c58ff5f8: Release + +## 6.10.0 + +### Minor Changes + +- 9ca673435: Release + +## 6.9.2 + +### Patch Changes + +- f00503d4e: Release + ## 6.9.1 ### Patch Changes diff --git a/packages/extension/manifest/v2.json b/packages/extension/manifest/v2.json index 406630368..4913cfbb5 100644 --- a/packages/extension/manifest/v2.json +++ b/packages/extension/manifest/v2.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/chrome-manifest.json", "name": "Argent X", "description": "The security of Ethereum with the scale of StarkNet", - "version": "5.9.1", + "version": "5.10.1", "manifest_version": 2, "browser_action": { "default_icon": { diff --git a/packages/extension/manifest/v3.json b/packages/extension/manifest/v3.json index 73f5bdf83..b177eba48 100644 --- a/packages/extension/manifest/v3.json +++ b/packages/extension/manifest/v3.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/chrome-manifest.json", "name": "Argent X", "description": "The security of Ethereum with the scale of StarkNet", - "version": "5.9.1", + "version": "5.10.1", "manifest_version": 3, "action": { "default_icon": { diff --git a/packages/extension/package.json b/packages/extension/package.json index 9138d0f0f..2cc9d651f 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@argent-x/extension", - "version": "6.9.1", + "version": "6.10.1", "main": "index.js", "private": true, "license": "MIT", @@ -15,7 +15,7 @@ "@types/lodash-es": "^4.17.6", "@types/object-hash": "^3.0.2", "@types/react": "^18.0.0", - "@types/react-copy-to-clipboard": "5.0.4", + "@types/react-copy-to-clipboard": "5.0.5", "@types/react-dom": "^18.0.0", "@types/react-measure": "^2.0.8", "@types/semver": "^7.3.10", @@ -39,7 +39,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "fetch-intercept": "^2.4.0", "file-loader": "^6.2.0", - "fork-ts-checker-webpack-plugin": "^8.0.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", "fs-extra": "^11.1.1", "happy-dom": "^12.0.0", "html-webpack-plugin": "^5.5.0", @@ -78,12 +78,13 @@ "@argent/shared": "^6.3.3", "@argent/stack-router": "^6.3.1", "@argent/ui": "^6.3.1", - "@argent/x-multicall": "^6.4.1", + "@argent/x-multicall": "^7.0.8", "@argent/x-sessions": "^6.3.1", "@argent/x-swap": "^6.3.1", "@argent/x-window": "^6.3.1", "@chakra-ui/icons": "^2.0.15", "@chakra-ui/react": "^2.6.1", + "@ethersproject/bignumber": "^5.7.0", "@extend-chrome/messages": "^1.2.2", "@google/model-viewer": "^3.0.0", "@hookform/resolvers": "^3.0.1", @@ -101,14 +102,13 @@ "@trpc/server": "^10.31.0", "@vitest/coverage-istanbul": "^0.34.0", "async-retry": "^1.3.3", - "buffer": "^6.0.3", "colord": "^2.9.3", "dexie": "^3.2.2", "dexie-react-hooks": "^1.1.1", "emittery": "^1.0.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "ethers": "^5.5.1", + "ethers": "^6.8.0", "jose": "^4.3.6", "jotai": "^2.0.4", "lodash-es": "^4.17.21", @@ -125,14 +125,14 @@ "react-router-dom": "^6.0.1", "react-select": "^5.4.0", "react-textarea-autosize": "^8.3.4", + "starknet": "5.19.5", "semver": "^7.5.2", - "starknet": "5.18.0", "starknet4": "npm:starknet@4.22.0", "starknet4-deprecated": "npm:starknet@4.4.0", "styled-components": "^5.3.5", "styled-normalize": "^8.0.7", "swr": "^1.3.0", - "trpc-browser": "^1.3.1", + "trpc-browser": "^1.3.6", "url-join": "^5.0.0", "webextension-polyfill": "^0.10.0", "yup": "^1.0.0-beta.4", diff --git a/packages/extension/src/assets/default-tokens.json b/packages/extension/src/assets/default-tokens.json index ffaab4bc9..9af9f5a69 100644 --- a/packages/extension/src/assets/default-tokens.json +++ b/packages/extension/src/assets/default-tokens.json @@ -1,5 +1,6 @@ [ { + "id": 1, "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "name": "Ether", "symbol": "ETH", @@ -9,6 +10,7 @@ "showAlways": true }, { + "id": 1, "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "name": "Ether", "symbol": "ETH", @@ -18,6 +20,7 @@ "showAlways": true }, { + "id": 1, "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "name": "Ether", "symbol": "ETH", @@ -27,6 +30,7 @@ "showAlways": true }, { + "id": 1, "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "name": "Ether", "symbol": "ETH", @@ -36,6 +40,7 @@ "showAlways": true }, { + "id": 6, "address": "0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3", "name": "DAI", "symbol": "DAI", @@ -44,6 +49,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png" }, { + "id": 6, "address": "0x03e85bfbb8e2a42b7bead9e88e9a1b19dbccf661471061807292120462396ec9", "name": "DAI", "symbol": "DAI", @@ -52,6 +58,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png" }, { + "id": 4, "address": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac", "name": "Wrapped BTC", "symbol": "WBTC", @@ -60,6 +67,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png" }, { + "id": 4, "address": "0x12d537dc323c439dc65c976fad242d5610d27cfb5f31689a0a319b8be7f3d56", "name": "Wrapped BTC", "symbol": "WBTC", @@ -68,6 +76,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png" }, { + "id": 2, "address": "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", "name": "USD Coin", "symbol": "USDC", @@ -76,6 +85,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png" }, { + "id": 2, "address": "0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426", "name": "USD Coin", "symbol": "USDC", @@ -84,6 +94,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png" }, { + "id": 3, "address": "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", "name": "Tether USD", "symbol": "USDT", @@ -92,6 +103,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png" }, { + "id": 3, "address": "0x386e8d061177f19b3b485c20e31137e6f6bc497cc635ccdfcab96fadf5add6a", "name": "Tether USD", "symbol": "USDT", @@ -116,6 +128,7 @@ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/steth.png" }, { + "id": 270, "address": "0x0319111a5037cbec2b3e638cc34a3474e2d2608299f3e62866e9cc683208c610", "name": "Rocket Pool ETH", "symbol": "rETH", diff --git a/packages/extension/src/background/__new/procedures/account/upgrade.ts b/packages/extension/src/background/__new/procedures/account/upgrade.ts index da9f7ec67..037358304 100644 --- a/packages/extension/src/background/__new/procedures/account/upgrade.ts +++ b/packages/extension/src/background/__new/procedures/account/upgrade.ts @@ -2,19 +2,24 @@ import { z } from "zod" import { argentAccountTypeSchema, - baseWalletAccountSchema, + walletAccountSchema, } from "../../../../shared/wallet.model" import { upgradeAccount } from "../../../accountUpgrade" import { openSessionMiddleware } from "../../middleware/session" import { extensionOnlyProcedure } from "../permissions" +import { getAccountClassHashFromChain } from "../../../../shared/account/details" +import { networkService } from "../../../../shared/network/service" +import { isEqualAddress } from "@argent/shared" const upgradeAccountSchema = z.object({ - account: baseWalletAccountSchema, + account: walletAccountSchema, targetImplementationType: argentAccountTypeSchema.optional(), }) + export const upgradeAccountProcedure = extensionOnlyProcedure .use(openSessionMiddleware) .input(upgradeAccountSchema) + .output(z.tuple([z.boolean(), walletAccountSchema])) .mutation( async ({ input: { account, targetImplementationType }, @@ -22,6 +27,32 @@ export const upgradeAccountProcedure = extensionOnlyProcedure services: { wallet, actionService }, }, }) => { + const [onchainAccount] = await getAccountClassHashFromChain([account]) + + const { accountClassHash } = await networkService.getById( + account.network.id, + ) + + if (!accountClassHash) { + throw new Error("Account class hash not found") + } + + const targetClassHash = + accountClassHash[targetImplementationType ?? account.type] + + if ( + onchainAccount.classHash && + isEqualAddress(onchainAccount.classHash, targetClassHash) + ) { + const updatedAccount = { + ...account, + classHash: onchainAccount.classHash, + type: onchainAccount.type, + } + + return [false, updatedAccount] // Upgrade not needed + } + // TODO ⬇ should be a service await upgradeAccount({ account, @@ -29,5 +60,7 @@ export const upgradeAccountProcedure = extensionOnlyProcedure actionService, targetImplementationType, }) + + return [true, account] // Upgrade needed, return the account as is }, ) diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts index 3a4d371f6..2d3701976 100644 --- a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts +++ b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts @@ -21,7 +21,7 @@ export const cancelEscapeProcedure = extensionOnlyProcedure }) => { try { const starknetAccount = - (await wallet.getSelectedStarknetAccount()) as Account + (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported await actionService.add( { type: "TRANSACTION", diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts index 54d76ff48..cb135e49d 100644 --- a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts +++ b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts @@ -77,14 +77,24 @@ export const escapeAndChangeGuardianProcedure = extensionOnlyProcedure /** * Call `escapeGuardian` to change guardian to this account publicKey */ + + /** Cairo 0 takes public key as argument, Cairo 1 does not */ + let calldata: string[] = [] + if (starknetAccount.cairoVersion === "0") { + calldata = [num.hexToDecimalString(publicKey)] + } + await actionService.add( { type: "TRANSACTION", payload: { transactions: { contractAddress: account.address, - entrypoint: "escapeGuardian", - calldata: [num.hexToDecimalString(publicKey)], + entrypoint: getEntryPointSafe( + "escapeGuardian", + starknetAccount.cairoVersion, + ), + calldata, }, meta: { isChangeGuardian: true, diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts index 3b2574a4d..8b2bb6802 100644 --- a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts +++ b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts @@ -1,8 +1,10 @@ import { z } from "zod" +import { Account } from "starknet" import { extensionOnlyProcedure } from "../permissions" import { baseWalletAccountSchema } from "../../../../shared/wallet.model" import { AccountMessagingError } from "../../../../shared/errors/accountMessaging" +import { getEntryPointSafe } from "../../../../shared/utils/transactions" const triggerEscapeGuardianSchema = z.object({ account: baseWalletAccountSchema, @@ -14,17 +16,22 @@ export const triggerEscapeGuardianProcedure = extensionOnlyProcedure async ({ input: { account }, ctx: { - services: { actionService }, + services: { actionService, wallet }, }, }) => { try { + const starknetAccount = + (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported await actionService.add( { type: "TRANSACTION", payload: { transactions: { contractAddress: account.address, - entrypoint: "triggerEscapeGuardian", + entrypoint: getEntryPointSafe( + "triggerEscapeGuardian", + starknetAccount.cairoVersion, + ), calldata: [], }, meta: { diff --git a/packages/extension/src/background/__new/procedures/recovery/recoverBackup.ts b/packages/extension/src/background/__new/procedures/recovery/recoverBackup.ts index c72cb0233..9139542f3 100644 --- a/packages/extension/src/background/__new/procedures/recovery/recoverBackup.ts +++ b/packages/extension/src/background/__new/procedures/recovery/recoverBackup.ts @@ -12,9 +12,9 @@ export const recoverBackupProcedure = extensionOnlyProcedure async ({ input: { backup }, ctx: { - services: { wallet }, + services: { recoveryService }, }, }) => { - await wallet.importBackup(backup) + return recoveryService.byBackup(backup) }, ) diff --git a/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts b/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts index be27d8031..7327cec5c 100644 --- a/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts +++ b/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts @@ -15,7 +15,7 @@ export const recoverSeedphraseProcedure = extensionOnlyProcedure async ({ input: { jwe }, ctx: { - services: { wallet, transactionTracker }, + services: { recoveryService }, }, }) => { const messagingKeys = await getMessagingKeys() @@ -29,8 +29,6 @@ export const recoverSeedphraseProcedure = extensionOnlyProcedure newPassword: string } = JSON.parse(bytesToUft8(plaintext)) - await wallet.restoreSeedPhrase(seedPhrase, newPassword) - void transactionTracker.loadHistory() - return { isSuccess: true } + return recoveryService.bySeedPhrase(seedPhrase, newPassword) }, ) diff --git a/packages/extension/src/background/__new/router.ts b/packages/extension/src/background/__new/router.ts index a2507f13e..7ab07b4f1 100644 --- a/packages/extension/src/background/__new/router.ts +++ b/packages/extension/src/background/__new/router.ts @@ -1,7 +1,6 @@ import { createChromeHandler } from "trpc-browser/adapter" import { getMessagingKeys } from "../keys/messagingKeys" -import { transactionTrackerWorker } from "../transactions/service/worker" import { walletSingleton } from "../walletSingleton" import { accountRouter } from "./procedures/account" import { accountMessagingRouter } from "./procedures/accountMessaging" @@ -18,6 +17,7 @@ import { backgroundActionService } from "./services/action" import { backgroundArgentAccountService } from "./services/argentAccount" import { backgroundMultisigService } from "./services/multisig" import { router } from "./trpc" +import { backgroundRecoveryService } from "./services/recovery" const appRouter = router({ account: accountRouter, @@ -42,11 +42,11 @@ createChromeHandler({ services: { // services can be shared accross requests, as we usually only handle one user at a time wallet: walletSingleton, // wallet "service" is obviously way too big and should be split up - transactionTracker: transactionTrackerWorker, actionService: backgroundActionService, messagingKeys: await getMessagingKeys(), argentAccountService: backgroundArgentAccountService, multisigService: backgroundMultisigService, + recoveryService: backgroundRecoveryService, }, }), }) diff --git a/packages/extension/src/background/__new/services/analytics/worker.ts b/packages/extension/src/background/__new/services/analytics/worker.ts index d52deff79..f8fc60224 100644 --- a/packages/extension/src/background/__new/services/analytics/worker.ts +++ b/packages/extension/src/background/__new/services/analytics/worker.ts @@ -10,7 +10,7 @@ export class AnalyticsWorker { this.backgroundUIService.emitter.on(Opened, (opened) => { if (!opened) { /** Extension was closed */ - this.activeStore.getState().update("lastClosed") + void this.activeStore.update("lastClosed") } }) } diff --git a/packages/extension/src/background/__new/services/network/index.ts b/packages/extension/src/background/__new/services/network/index.ts index c2f9d585f..27d38cd7f 100644 --- a/packages/extension/src/background/__new/services/network/index.ts +++ b/packages/extension/src/background/__new/services/network/index.ts @@ -1,3 +1,4 @@ +import { debounceService } from "../../../../shared/debounce" import { defaultNetworks } from "../../../../shared/network" import { networkStatusRepo } from "../../../../shared/network/statusStore" import { networkRepo } from "../../../../shared/network/store" @@ -18,4 +19,5 @@ export const networkWorker = new NetworkWorker( backgroundNetworkService, backgroundUIService, chromeScheduleService, + debounceService, ) diff --git a/packages/extension/src/background/__new/services/network/status.ts b/packages/extension/src/background/__new/services/network/status.ts index 749a31023..d69e673dd 100644 --- a/packages/extension/src/background/__new/services/network/status.ts +++ b/packages/extension/src/background/__new/services/network/status.ts @@ -1,75 +1,47 @@ -import urljoin from "url-join" - import { Network, NetworkStatus } from "../../../../shared/network" -import { fetchWithTimeout } from "../../../utils/fetchWithTimeout" -import { z } from "zod" -import { NetworkError } from "../../../../shared/errors/network" +import { GetNetworkStatusesFn } from "./interface" +import { + getProvider, + getProviderForRpcUrl, + shouldUseRpcProvider, +} from "../../../../shared/network/provider" -const checklyNetworkNames = { - "Goerli - Contract call": "goerli-alpha", - "Goerli 2 - Contract call": "goerli-alpha-2", - "Mainnet - Contract call": "mainnet-alpha", - "Integration - Goerli - Get state update": "integration", -} -const checklySchema = z.object({ - results: z.array( - z.object({ - id: z.string(), - name: z.string(), - status: z.object({ - hasErrors: z.boolean(), - hasFailures: z.boolean(), - isDegraded: z.boolean(), - }), - }), - ), -}) +async function getNetworkStatus(network: Network): Promise { + const provider = getProvider(network) + if (!shouldUseRpcProvider(network) || !network.rpcUrl) { + // chainId can not be used, as snjs is shallowing the network error + await provider.getBlock("latest") // throws if not connected + return "ok" + } -export const getNetworkStatuses = async () => { - try { - const response = await fetchWithTimeout( - urljoin( - "https://api.checklyhq.com/v1/status-page/4054/statuses?page=1&limit=15", - ), - { timeout: 5000, method: "GET" }, - ) - const data = await response.json() - const parsedData = checklySchema.safeParse(data) - if (!parsedData.success) { - throw new NetworkError({ - message: "NETWORK_STATUS_RESPONSE_PARSING_FAILED", - }) - } - const networkStatuses: Record = {} - parsedData.data.results.forEach((result) => { - let status: NetworkStatus = "unknown" - if (result.status.hasErrors) { - status = "error" - } else if (result.status.hasFailures) { - status = "error" - } else if (result.status.isDegraded) { - status = "degraded" - } else if (result.status.isDegraded === false) { - status = "ok" - } - if (result.name in checklyNetworkNames) { - const key = result.name as keyof typeof checklyNetworkNames - networkStatuses[checklyNetworkNames[key]] = status - } - }) + const rpcProvider = getProviderForRpcUrl(network.rpcUrl) + const sync = await rpcProvider.getSyncingStats() // throws if not connected - return networkStatuses - } catch (error) { - console.warn({ error }) - const networkStatuses: Record = {} - Object.values(checklyNetworkNames).map((value) => { - networkStatuses[value] = "unknown" - }) - // Gracefully returning unknown statuses - return networkStatuses - throw new NetworkError({ - message: "NETWORK_STATUS_RESPONSE_PARSING_FAILED", - options: { error }, - }) + if (sync === false) { + // not syncing + return "ok" } + + const blockDifference = sync.highest_block_num - sync.current_block_num + if (blockDifference <= 2) { + return "ok" + } + return "degraded" +} + +export const getNetworkStatuses: GetNetworkStatusesFn = async (networks) => { + const statuses = await Promise.allSettled( + networks.map(async (network) => getNetworkStatus(network)), + ) + + return Object.fromEntries( + networks.map(({ id }, i) => { + const promise = statuses[i] + if (promise.status === "fulfilled") { + return [id, promise.value] + } else { + return [id, "error"] + } + }), + ) } diff --git a/packages/extension/src/background/__new/services/network/worker.ts b/packages/extension/src/background/__new/services/network/worker.ts index 663283a4b..061ab8d54 100644 --- a/packages/extension/src/background/__new/services/network/worker.ts +++ b/packages/extension/src/background/__new/services/network/worker.ts @@ -1,56 +1,26 @@ import { IScheduleService } from "../../../../shared/schedule/interface" -import { IBackgroundUIService, Opened } from "../ui/interface" import { IBackgroundNetworkService } from "./interface" import { RefreshInterval } from "../../../../shared/config" +import { everyWhenOpen } from "../worker/schedule/decorators" +import { IBackgroundUIService } from "../ui/interface" +import { IDebounceService } from "../../../../shared/debounce" const TASK_ID = "NetworkWorker.updateStatuses" -const REFRESH_PERIOD_MINUTES = Math.floor(RefreshInterval.MEDIUM / 60) export class NetworkWorker { - private isUpdating = false - private lastUpdatedTimestamp = 0 - constructor( private readonly backgroundNetworkService: IBackgroundNetworkService, private readonly backgroundUIService: IBackgroundUIService, private readonly scheduleService: IScheduleService, - ) { - void this.scheduleService.registerImplementation({ - id: TASK_ID, - callback: this.updateNetworkStatuses.bind(this), - }) - - this.backgroundUIService.emitter.on(Opened, this.onOpened.bind(this)) - } - - async updateNetworkStatuses() { - if (this.isUpdating) { - return - } - this.isUpdating = true - this.lastUpdatedTimestamp = Date.now() + private readonly debounceService: IDebounceService, + ) {} + + updateNetworkStatuses = everyWhenOpen( + this.backgroundUIService, + this.scheduleService, + this.debounceService, + RefreshInterval.MEDIUM, + )(async (): Promise => { await this.backgroundNetworkService.updateStatuses() - this.isUpdating = false - } - - onOpened(opened: boolean) { - if (opened) { - const currentTimestamp = Date.now() - const differenceInMilliseconds = - currentTimestamp - this.lastUpdatedTimestamp - const differenceInMinutes = differenceInMilliseconds / (1000 * 60) - - if (differenceInMinutes > REFRESH_PERIOD_MINUTES) { - void this.updateNetworkStatuses() - } - - void this.scheduleService.every(RefreshInterval.MEDIUM, { - id: TASK_ID, - }) - } else { - void this.scheduleService.delete({ - id: TASK_ID, - }) - } - } + }) } diff --git a/packages/extension/src/shared/nft/worker/implementation.ts b/packages/extension/src/background/__new/services/nft/worker/implementation.ts similarity index 56% rename from packages/extension/src/shared/nft/worker/implementation.ts rename to packages/extension/src/background/__new/services/nft/worker/implementation.ts index f214c8aed..c2b8fbf7b 100644 --- a/packages/extension/src/shared/nft/worker/implementation.ts +++ b/packages/extension/src/background/__new/services/nft/worker/implementation.ts @@ -1,28 +1,23 @@ -import { addressSchema } from "@argent/shared" +import { addressSchema, getAccountIdentifier } from "@argent/shared" import { uniq } from "lodash-es" -import { - IBackgroundUIService, - Opened, -} from "../../../background/__new/services/ui/interface" -import { Wallet } from "../../../background/wallet" -import { Locked } from "../../../background/wallet/session/interface" -import { WalletSessionService } from "../../../background/wallet/session/session.service" -import type { WalletStorageProps } from "../../../shared/wallet/walletStore" -import { INFTService } from "../../../ui/services/nfts/interface" -import { IScheduleService } from "../../schedule/interface" -import { ArrayStorage, KeyValueStorage } from "../../storage" -import { Transaction } from "../../transactions" -import { transactionSucceeded } from "../../utils/transactionSucceeded" -import { RefreshInterval } from "../../config" +import { IBackgroundUIService, Opened } from "../../ui/interface" +import { Wallet } from "../../../../wallet" +import { Locked } from "../../../../wallet/session/interface" +import { WalletSessionService } from "../../../../wallet/session/session.service" +import { RefreshInterval } from "../../../../../shared/config" +import { INFTService } from "../../../../../shared/nft/interface" +import { IScheduleService } from "../../../../../shared/schedule/interface" +import { WalletStorageProps } from "../../../../../shared/wallet/walletStore" +import { ArrayStorage, KeyValueStorage } from "../../../../../shared/storage" +import { Transaction } from "../../../../../shared/transactions" +import { transactionSucceeded } from "../../../../../shared/utils/transactionSucceeded" +import { INFTWorkerStore } from "../../../../../shared/nft/worker/interface" const TASK_ID = "NftsWorker.updateNfts" const REFRESH_PERIOD_MINUTES = Math.floor(RefreshInterval.SLOW / 60) export class NftsWorker { - private isUpdating = false - private lastUpdatedTimestamp = 0 - constructor( private readonly nftsService: INFTService, private readonly scheduleService: IScheduleService, @@ -31,6 +26,7 @@ export class NftsWorker { private readonly transactionsStore: ArrayStorage, public readonly sessionService: WalletSessionService, private readonly backgroundUIService: IBackgroundUIService, + private store: KeyValueStorage, ) { /** udpdate on a regular refresh interval */ void this.scheduleService.registerImplementation({ @@ -62,14 +58,34 @@ export class NftsWorker { }) } - onOpened(opened: boolean) { + async getStateForCurrentAccount() { + const account = await this.walletSingleton.getSelectedAccount() + if (!account) { + return + } + const accountIdentifier = getAccountIdentifier(account) + const entry = await this.store.get(accountIdentifier) + if (!entry) { + await this.store.set(accountIdentifier, { + isUpdating: false, + lastUpdatedTimestamp: 0, + }) + } + return this.store.get(accountIdentifier) + } + + async onOpened(opened: boolean) { if (opened) { + const stateForCurrentAccount = await this.getStateForCurrentAccount() + if (!stateForCurrentAccount) { + return + } const currentTimestamp = Date.now() const differenceInMilliseconds = - currentTimestamp - this.lastUpdatedTimestamp + currentTimestamp - stateForCurrentAccount.lastUpdatedTimestamp const differenceInMinutes = differenceInMilliseconds / (1000 * 60) // Convert milliseconds to minutes - // If we haven't done a nft update in the past 5 minutes, do one on the spot when opening the extension + // If we haven't done a nft update for the current account in the past 5 minutes, do one on the spot when opening the extension if (differenceInMinutes > REFRESH_PERIOD_MINUTES) { void this.updateNfts() } @@ -90,8 +106,13 @@ export class NftsWorker { } } - async updateNfts(): Promise { - if (this.isUpdating) { + async updateNfts() { + const stateForCurrentAccount = await this.getStateForCurrentAccount() + + if (!stateForCurrentAccount) { + return + } + if (stateForCurrentAccount.isUpdating) { return } @@ -100,8 +121,12 @@ export class NftsWorker { return } - this.isUpdating = true - this.lastUpdatedTimestamp = Date.now() + const accountIdentifier = getAccountIdentifier(account) + const lastUpdatedTimestamp = Date.now() + await this.store.set(accountIdentifier, { + isUpdating: true, + lastUpdatedTimestamp, + }) try { const nfts = await this.nftsService.getAssets( @@ -132,6 +157,9 @@ export class NftsWorker { console.error(e) } - this.isUpdating = false + await this.store.set(accountIdentifier, { + isUpdating: false, + lastUpdatedTimestamp, + }) } } diff --git a/packages/extension/src/background/__new/services/nft/worker/index.ts b/packages/extension/src/background/__new/services/nft/worker/index.ts new file mode 100644 index 000000000..396f85303 --- /dev/null +++ b/packages/extension/src/background/__new/services/nft/worker/index.ts @@ -0,0 +1,19 @@ +import { nftWorkerStore } from "../../../../../shared/nft/worker/store" +import { chromeScheduleService } from "../../../../../shared/schedule" +import { old_walletStore } from "../../../../../shared/wallet/walletStore" +import { nftService } from "../../../../../ui/services/nfts" +import { transactionsStore } from "../../../../transactions/store" +import { sessionService, walletSingleton } from "../../../../walletSingleton" +import { backgroundUIService } from "../../ui" +import { NftsWorker } from "./implementation" + +export const nftsWorker = new NftsWorker( + nftService, + chromeScheduleService, + walletSingleton, + old_walletStore, + transactionsStore, + sessionService, + backgroundUIService, + nftWorkerStore, +) diff --git a/packages/extension/src/background/__new/services/onboarding/worker/implementation.test.ts b/packages/extension/src/background/__new/services/onboarding/worker/implementation.test.ts index 2c4a3e05b..bc4426a85 100644 --- a/packages/extension/src/background/__new/services/onboarding/worker/implementation.test.ts +++ b/packages/extension/src/background/__new/services/onboarding/worker/implementation.test.ts @@ -17,15 +17,17 @@ describe("OnboardingWorker", () => { } as unknown as KeyValueStorage const browser = { runtime: { + getManifest: vi.fn(() => ({ manifest_version: 3 } as any)), onInstalled: { addListener: vi.fn(), }, }, - browserAction: { + action: { onClicked: { addListener: vi.fn(), }, - }, + } as any, + browserAction: {} as any, } const onboardingWorker = new OnboardingWorker( onboardingService, @@ -45,7 +47,7 @@ describe("OnboardingWorker", () => { async () => false, ) expect(browser.runtime.onInstalled.addListener).toHaveBeenCalled() - expect(browser.browserAction.onClicked.addListener).toHaveBeenCalled() + expect(browser.action.onClicked.addListener).toHaveBeenCalled() expect(walletStore.subscribe).toHaveBeenCalled() expect(onboardingService.getOnboardingComplete).toHaveBeenCalled() await Promise.resolve() diff --git a/packages/extension/src/background/__new/services/onboarding/worker/implementation.ts b/packages/extension/src/background/__new/services/onboarding/worker/implementation.ts index e2b1c3a6d..6c7715ad4 100644 --- a/packages/extension/src/background/__new/services/onboarding/worker/implementation.ts +++ b/packages/extension/src/background/__new/services/onboarding/worker/implementation.ts @@ -1,3 +1,7 @@ +import { + MinimalActionBrowser, + getBrowserAction, +} from "../../../../../shared/browser" import type { KeyValueStorage } from "../../../../../shared/storage" import type { StorageChange } from "../../../../../shared/storage/types" import type { DeepPick } from "../../../../../shared/types/deepPick" @@ -6,8 +10,9 @@ import type { IOnboardingService } from "../interface" type MinimalBrowser = DeepPick< typeof chrome, - "runtime.onInstalled.addListener" | "browserAction.onClicked.addListener" -> + "runtime.onInstalled.addListener" +> & + MinimalActionBrowser export default class OnboardingWorker { constructor( @@ -16,7 +21,7 @@ export default class OnboardingWorker { private browser: MinimalBrowser, ) { this.browser.runtime.onInstalled.addListener(this.onInstalled.bind(this)) - this.browser.browserAction.onClicked.addListener( + getBrowserAction(this.browser).onClicked.addListener( this.onExtensionIconClick.bind(this), ) this.walletStore.subscribe( diff --git a/packages/extension/src/background/__new/services/recovery/implementation.test.ts b/packages/extension/src/background/__new/services/recovery/implementation.test.ts new file mode 100644 index 000000000..03cf26843 --- /dev/null +++ b/packages/extension/src/background/__new/services/recovery/implementation.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test, vi } from "vitest" + +import { BackgroundRecoveryService } from "./implementation" +import { IObjectStore } from "../../../../shared/storage/__new/interface" +import { IRecoveryStorage } from "../../../../shared/recovery/types" +import type { Wallet } from "../../../wallet" +import type { TransactionTrackerWorker } from "../../../transactions/service/starknet.service" + +describe("BackgroundRecoveryService", () => { + const makeService = () => { + const recoveryStore = { + set: vi.fn(), + } as unknown as IObjectStore + const wallet = { + importBackup: vi.fn(), + restoreSeedPhrase: vi.fn(), + } as unknown as Wallet + const transactionTracker = { + loadHistory: vi.fn(), + } as unknown as TransactionTrackerWorker + const backgroundRecoveryService = new BackgroundRecoveryService( + recoveryStore, + wallet, + transactionTracker, + ) + return { + recoveryStore, + wallet, + transactionTracker, + backgroundRecoveryService, + } + } + describe("importBackup", () => { + test("it should call services and update isRecovering state", async () => { + const { recoveryStore, wallet, backgroundRecoveryService } = makeService() + await backgroundRecoveryService.byBackup("foo") + expect(recoveryStore.set).toHaveBeenNthCalledWith(1, { + isRecovering: true, + }) + expect(recoveryStore.set).toHaveBeenNthCalledWith(2, { + isRecovering: false, + }) + expect(wallet.importBackup).toHaveBeenCalledWith("foo") + }) + }) + describe("bySeedPhrase", () => { + test("it should call services and update isRecovering state", async () => { + const { + recoveryStore, + wallet, + transactionTracker, + backgroundRecoveryService, + } = makeService() + await backgroundRecoveryService.bySeedPhrase("foo", "bar") + expect(recoveryStore.set).toHaveBeenNthCalledWith(1, { + isRecovering: true, + }) + expect(recoveryStore.set).toHaveBeenNthCalledWith(2, { + isRecovering: false, + }) + expect(wallet.restoreSeedPhrase).toHaveBeenCalledWith("foo", "bar") + expect(transactionTracker.loadHistory).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/extension/src/background/__new/services/recovery/implementation.ts b/packages/extension/src/background/__new/services/recovery/implementation.ts new file mode 100644 index 000000000..648354083 --- /dev/null +++ b/packages/extension/src/background/__new/services/recovery/implementation.ts @@ -0,0 +1,36 @@ +import { IRecoveryService } from "../../../../shared/recovery/service/interface" +import { IRecoveryStorage } from "../../../../shared/recovery/types" +import { IObjectStore } from "../../../../shared/storage/__new/interface" +import { TransactionTrackerWorker } from "../../../transactions/service/starknet.service" +import { Wallet } from "../../../wallet" + +export class BackgroundRecoveryService implements IRecoveryService { + constructor( + private readonly recoveryStore: IObjectStore, + private readonly wallet: Wallet, + private readonly transactionTracker: TransactionTrackerWorker, + ) {} + + private async setIsRecovering(isRecovering: boolean) { + await this.recoveryStore.set({ isRecovering }) + } + + async byBackup(backup: string) { + try { + await this.setIsRecovering(true) + await this.wallet.importBackup(backup) + } finally { + await this.setIsRecovering(false) + } + } + + async bySeedPhrase(seedPhrase: string, newPassword: string) { + try { + await this.setIsRecovering(true) + await this.wallet.restoreSeedPhrase(seedPhrase, newPassword) + void this.transactionTracker.loadHistory() + } finally { + await this.setIsRecovering(false) + } + } +} diff --git a/packages/extension/src/background/__new/services/recovery/index.ts b/packages/extension/src/background/__new/services/recovery/index.ts new file mode 100644 index 000000000..7217fa9e4 --- /dev/null +++ b/packages/extension/src/background/__new/services/recovery/index.ts @@ -0,0 +1,10 @@ +import { recoveryStore } from "../../../../shared/recovery/storage" +import { transactionTrackerWorker } from "../../../transactions/service/worker" +import { walletSingleton } from "../../../walletSingleton" +import { BackgroundRecoveryService } from "./implementation" + +export const backgroundRecoveryService = new BackgroundRecoveryService( + recoveryStore, + walletSingleton, + transactionTrackerWorker, +) diff --git a/packages/extension/src/background/__new/services/ui/background.test.ts b/packages/extension/src/background/__new/services/ui/background.test.ts index 34ab603f6..14ef2f871 100644 --- a/packages/extension/src/background/__new/services/ui/background.test.ts +++ b/packages/extension/src/background/__new/services/ui/background.test.ts @@ -62,10 +62,23 @@ describe("BackgroundUIService", () => { }, } - /** open */ + /** simulate initially open - e.g. happens when service worker restarts */ backgroundUIService.onConnectPort(port) expect(backgroundUIService.opened).toBeTruthy() expect(port.onDisconnect.addListener).toHaveBeenCalled() + + /** no emit on initial value change */ + expect(emitter.emit).not.toHaveBeenCalled() + + /** close */ + await backgroundUIService.onDisconnectPort() + expect(backgroundUIService.opened).toBeFalsy() + expect(emitter.emit).toHaveBeenCalledWith(Opened, false) + + /** re-open */ + emitter.emit.mockReset() + backgroundUIService.onConnectPort(port) + expect(backgroundUIService.opened).toBeTruthy() expect(emitter.emit).toHaveBeenCalledWith(Opened, true) /** should not fire again */ diff --git a/packages/extension/src/background/__new/services/ui/background.ts b/packages/extension/src/background/__new/services/ui/background.ts index 1651e2bc1..07f45cc56 100644 --- a/packages/extension/src/background/__new/services/ui/background.ts +++ b/packages/extension/src/background/__new/services/ui/background.ts @@ -28,6 +28,7 @@ type MinimalIWalletSessionService = Pick< export default class BackgroundUIService implements IBackgroundUIService { private _opened = false + private isInitialising = true constructor( readonly emitter: Emittery, @@ -36,6 +37,11 @@ export default class BackgroundUIService implements IBackgroundUIService { private sessionService: MinimalIWalletSessionService, ) { this.initListeners() + void (async () => { + /** initialise opened state */ + const hasTab = await this.uiService.hasTab() + this.opened = hasTab + })() } /* @@ -73,6 +79,12 @@ export default class BackgroundUIService implements IBackgroundUIService { } private set opened(opened: boolean) { + if (this.isInitialising) { + /** don't emit on initial value change */ + this.isInitialising = false + this._opened = opened + return + } if (this._opened === opened) { return } diff --git a/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts b/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts new file mode 100644 index 000000000..cd1ab2ce1 --- /dev/null +++ b/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts @@ -0,0 +1,131 @@ +import { describe } from "vitest" +import { createScheduleServiceMock } from "../../../../../shared/schedule/mock" +import { + onStartup, + onInstallAndUpgrade, + every, + onOpen, + onlyIfOpen, + debounce, + everyWhenOpen, +} from "./decorators" +import { getMockBackgroundUIService } from "./mockBackgroundUIService" +import { getMockDebounceService } from "../../../../../shared/debounce/mock" + +describe("decorators", () => { + describe("onStartup", () => { + it("should call scheduleService.onStartup with the correct arguments", () => { + const scheduleService = createScheduleServiceMock() + const fn = async () => {} + onStartup(scheduleService)(fn) + expect(scheduleService.onStartup).toHaveBeenCalledWith({ + id: "onStartup", + callback: fn, + }) + }) + }) + + describe("onInstallAndUpgrade", () => { + it("should call scheduleService.onInstallAndUpgrade with the correct arguments", () => { + const scheduleService = createScheduleServiceMock() + const fn = async () => {} + onInstallAndUpgrade(scheduleService)(fn) + expect(scheduleService.onInstallAndUpgrade).toHaveBeenCalledWith({ + id: "onInstalled", + callback: fn, + }) + }) + }) + + describe("every", () => { + it("should call scheduleService.registerImplementation and scheduleService.every with the correct arguments", async () => { + const scheduleService = createScheduleServiceMock() + const fn = async () => {} + every(scheduleService, 1)(fn) + expect(scheduleService.registerImplementation).toHaveBeenCalledWith({ + id: expect.stringContaining("every@1s:"), + callback: fn, + }) + // flush the promise + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(scheduleService.every).toHaveBeenCalledWith(1, { + id: expect.stringContaining("every@1s:"), + }) + }) + }) + + describe("onOpen", async () => { + test("should call the function when the background ui service is opened", async () => { + const [mockBackgroundUIServiceManager, mockBackgroundUIService] = + getMockBackgroundUIService() + const fn = vi.fn() + onOpen(mockBackgroundUIService)(fn) + expect(fn).toHaveBeenCalledTimes(0) + await mockBackgroundUIServiceManager.setOpened(true) + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(false) + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(true) + expect(fn).toHaveBeenCalledTimes(2) + }) + }) + + describe("onOpen", async () => { + test("should call the function when the background ui service is opened", async () => { + const [mockBackgroundUIServiceManager, mockBackgroundUIService] = + getMockBackgroundUIService() + const fn = vi.fn() + const oioFn = onlyIfOpen(mockBackgroundUIService)(fn) + await oioFn() + expect(fn).toHaveBeenCalledTimes(0) + await mockBackgroundUIServiceManager.setOpened(true) + await oioFn() + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(false) + await oioFn() + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(true) + await oioFn() + expect(fn).toHaveBeenCalledTimes(2) + }) + }) + + describe("debounce", () => { + test("should call the function when not in debounce interval", async () => { + const debounceService = getMockDebounceService() + const fn = vi.fn() + const debouncedFn = debounce(debounceService, 1)(fn) + expect(fn).toHaveBeenCalledTimes(0) + expect(debounceService.debounce).toHaveBeenCalledTimes(0) + await debouncedFn() + expect(debounceService.debounce).toHaveBeenCalledWith({ + id: expect.stringContaining("debounce@1s:"), + debounce: 1, + callback: fn, + }) + }) + }) + + describe("everyWhenOpen", () => { + test("should call the function when the background ui service is opened", async () => { + const [mockBackgroundUIServiceManager, mockBackgroundUIService] = + getMockBackgroundUIService() + const scheduleService = createScheduleServiceMock() + const debounceService = getMockDebounceService() + const fn = vi.fn() + everyWhenOpen( + mockBackgroundUIService, + scheduleService, + debounceService, + 1, + )(fn) + expect(fn).toHaveBeenCalledTimes(0) + await mockBackgroundUIServiceManager.setOpened(true) + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(false) + expect(fn).toHaveBeenCalledTimes(1) + await mockBackgroundUIServiceManager.setOpened(true) + expect(fn).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/extension/src/background/__new/services/worker/schedule/decorators.ts b/packages/extension/src/background/__new/services/worker/schedule/decorators.ts new file mode 100644 index 000000000..7727c3f66 --- /dev/null +++ b/packages/extension/src/background/__new/services/worker/schedule/decorators.ts @@ -0,0 +1,141 @@ +import { IDebounceService } from "../../../../../shared/debounce" +import { IScheduleService } from "../../../../../shared/schedule/interface" +import { IBackgroundUIService, Opened } from "../../ui/interface" +import { pipe } from "./pipe" +import { nanoid } from "nanoid" + +type Fn = (...args: unknown[]) => Promise + +/** + * Function to schedule a task on startup. + * @param {IScheduleService} scheduleService - The schedule service. + * @returns {Function} The scheduled function. + */ +export const onStartup = + (scheduleService: IScheduleService) => + (fn: T): T => { + const id = `onStartup` + void scheduleService.onStartup({ + id, + callback: fn, + }) + + return fn + } + +/** + * Function to schedule a task on install and upgrade. + * @param {IScheduleService} scheduleService - The schedule service. + * @returns {Function} The scheduled function. + */ +export const onInstallAndUpgrade = + (scheduleService: IScheduleService) => + (fn: T): T => { + const id = `onInstalled` + void scheduleService.onInstallAndUpgrade({ + id, + callback: fn, + }) + + return fn + } + +/** + * Function to schedule a task to run every specified seconds. + * @param {IScheduleService} scheduleService - The schedule service. + * @param {number} seconds - The interval in seconds. + * @returns {Function} The scheduled function. + */ +export const every = + (scheduleService: IScheduleService, seconds: number) => + (fn: T): T => { + const id = `every@${seconds}s:${nanoid()}` + void scheduleService + .registerImplementation({ + id, + callback: fn, + }) + .then(() => scheduleService.every(seconds, { id })) + + return fn + } + +export type MinimalIBackgroundUIService = Pick< + IBackgroundUIService, + "opened" | "emitter" +> + +/** + * Function to schedule a task to run when the UI is opened. + * @param {IBackgroundUIService} backgroundUIService - The background UI service. + * @returns {Function} The scheduled function. + */ +export const onOpen = + (backgroundUIService: MinimalIBackgroundUIService) => + (fn: T): T => { + backgroundUIService.emitter.on(Opened, async (open) => { + if (open) { + await fn() + } + }) + + return fn + } + +function noopAs(_fn: T): T { + const noop = () => {} + return noop as T +} +/** + * only run the decorated function if the UI is open + * + * @dev this decorator needs to go last in most cases! + * @param backgroundUIService - the background UI service + * @returns the decorated function + */ +export const onlyIfOpen = + (backgroundUIService: MinimalIBackgroundUIService) => + (fn: T): T => { + return ((...args: unknown[]) => { + return backgroundUIService.opened ? fn(...args) : noopAs(fn)(...args) + }) as T + } + +/** + * Function to debounce a task. + * @param {IDebounceService} debounceService - The debounce service. + * @param {number} seconds - The debounce time in seconds. + * @returns {Promise} The debounced function. + */ +export const debounce = + (debounceService: IDebounceService, seconds: number) => + (fn: T): (() => Promise) => { + const id = `debounce@${seconds}s:${nanoid()}` + const task = { id, callback: fn, debounce: seconds } + + return () => { + return debounceService.debounce(task) + } + } + +/** + * Function to schedule a task to run every specified seconds when the UI is opened. + * @param {IBackgroundUIService} backgroundUIService - The background UI service. + * @param {IScheduleService} scheduleService - The schedule service. + * @param {IDebounceService} debounceService - The debounce service. + * @param {number} seconds - The interval in seconds. + * @returns {Function} The scheduled function. + */ +export const everyWhenOpen = ( + backgroundUIService: MinimalIBackgroundUIService, + scheduleService: IScheduleService, + debounceService: IDebounceService, + seconds: number, +) => { + return pipe( + onOpen(backgroundUIService), + every(scheduleService, seconds), + onlyIfOpen(backgroundUIService), + debounce(debounceService, seconds), + ) +} diff --git a/packages/extension/src/background/__new/services/worker/schedule/mockBackgroundUIService.ts b/packages/extension/src/background/__new/services/worker/schedule/mockBackgroundUIService.ts new file mode 100644 index 000000000..91fa875be --- /dev/null +++ b/packages/extension/src/background/__new/services/worker/schedule/mockBackgroundUIService.ts @@ -0,0 +1,31 @@ +import Emittery from "emittery" +import { MinimalIBackgroundUIService } from "./decorators" +import { Events, Opened } from "../../ui/interface" + +interface MockBackgroundUIServiceManager { + setOpened(opened: boolean): Promise +} + +export const getMockBackgroundUIService = (): [ + MockBackgroundUIServiceManager, + MinimalIBackgroundUIService, +] => { + const emitter = new Emittery() + let opened = false + const setOpened = (newOpened: boolean) => { + opened = newOpened + return emitter.emit(Opened, opened) + } + const backgroundUIService: MinimalIBackgroundUIService = { + get opened() { + return opened + }, + emitter, + } + return [ + { + setOpened, + }, + backgroundUIService, + ] +} diff --git a/packages/extension/src/background/__new/services/worker/schedule/pipe.test.ts b/packages/extension/src/background/__new/services/worker/schedule/pipe.test.ts new file mode 100644 index 000000000..bb2e674ee --- /dev/null +++ b/packages/extension/src/background/__new/services/worker/schedule/pipe.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest" +import { pipe } from "./pipe" + +const double = (num: number) => num * 2 +const square = (num: number) => num ** 2 +const parse = (str: string) => parseInt(str, 10) +const toString = (num: number) => `Number: ${num}` + +describe("pipe function", () => { + it("should handle an empty function array", () => { + const piped = pipe() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(piped("test")).toBe("test") + }) + + it("should handle single function", () => { + const piped = pipe(double) + expect(piped(2)).toBe(4) + }) + + it("should correctly pipe multiple functions", () => { + const piped = pipe(double, square) + expect(piped(2)).toBe(16) // (2 * 2) ** 2 + }) + + it("should ensure the returned function has the correct type", () => { + const piped = pipe(parse, double, toString) + const result: string = piped("5") + expect(result).toBe("Number: 10") + }) +}) diff --git a/packages/extension/src/background/__new/services/worker/schedule/pipe.ts b/packages/extension/src/background/__new/services/worker/schedule/pipe.ts new file mode 100644 index 000000000..450d2ee51 --- /dev/null +++ b/packages/extension/src/background/__new/services/worker/schedule/pipe.ts @@ -0,0 +1,24 @@ +type Func = (a: A) => B + +type PipeReturn[]> = ReturnType< + T extends [ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ...infer _Rest, + Func, + ] + ? Func + : never +> + +/** + * Function to pipe a series of functions together. + * @param {Function[]} fns - The functions to pipe. + * @returns {Function} The piped function. + */ +export function pipe[]>( + ...fns: T +): Func[0], PipeReturn> { + return (input: Parameters[0]): PipeReturn => { + return fns.reduce((acc, fn) => fn(acc), input) as PipeReturn + } +} diff --git a/packages/extension/src/background/__new/trpc.ts b/packages/extension/src/background/__new/trpc.ts index 1ca4b937a..84a6bc855 100644 --- a/packages/extension/src/background/__new/trpc.ts +++ b/packages/extension/src/background/__new/trpc.ts @@ -4,19 +4,19 @@ import type { IArgentAccountServiceBackground } from "../../shared/argentAccount import { BaseError } from "../../shared/errors/baseError" import type { IMultisigService } from "../../shared/multisig/service/messaging/interface" import { MessagingKeys } from "../keys/messagingKeys" -import { TransactionTrackerWorker } from "../transactions/service/starknet.service" import { Wallet } from "../wallet" import type { IBackgroundActionService } from "./services/action/interface" +import { IRecoveryService } from "../../shared/recovery/service/interface" interface Context { sender?: chrome.runtime.MessageSender services: { wallet: Wallet - transactionTracker: TransactionTrackerWorker actionService: IBackgroundActionService messagingKeys: MessagingKeys argentAccountService: IArgentAccountServiceBackground multisigService: IMultisigService + recoveryService: IRecoveryService } } diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts index d6176aa10..be839a94e 100644 --- a/packages/extension/src/background/actionHandlers.ts +++ b/packages/extension/src/background/actionHandlers.ts @@ -164,7 +164,10 @@ export const handleActionApproval = async ( } case "SIGN": { - const typedData = action.payload + const { + typedData, + options: { skipDeploy = false }, + } = action.payload if (!(await wallet.isSessionOpen())) { throw Error("you need an open session") } diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts index ba91148fb..5ded3ae6b 100644 --- a/packages/extension/src/background/index.ts +++ b/packages/extension/src/background/index.ts @@ -1,26 +1,18 @@ -import "./__new/router" -import "./transactions/service/worker" -import "./migrations" +import browser from "webextension-polyfill" +import * as Sentry from "@sentry/browser" +import { getBrowserAction } from "../shared/browser" -import { messageStream } from "../shared/messages" -import { initWorkers } from "./workers" -import { initBadgeText } from "./transactions/badgeText" -import { transactionTrackerWorker } from "./transactions/service/worker" -import { handleMessage } from "./messageHandling/handle" -import { addMessageListeners } from "./messageHandling/addMessageListeners" - -// badge shown on extension icon -initBadgeText() - -// load transaction history -transactionTrackerWorker - .loadHistory() - .catch(() => console.warn("failed to load transaction history")) - -addMessageListeners() - -// eslint-disable-next-line @typescript-eslint/no-misused-promises -messageStream.subscribe(handleMessage) - -// start workers -initWorkers() +try { + // catch any errors from init.ts + require("./init") +} catch (error) { + console.error("Fatal exception in background/init.ts", error) + Sentry.captureException(error) + // run on next event loop to override changes by any successful services + setTimeout(() => { + // clicking icon will start on the error screen + void getBrowserAction(browser).setPopup({ + popup: "index.html?goto=background-error", + }) + }, 0) +} diff --git a/packages/extension/src/background/init.ts b/packages/extension/src/background/init.ts new file mode 100644 index 000000000..1fe4f2b69 --- /dev/null +++ b/packages/extension/src/background/init.ts @@ -0,0 +1,30 @@ +import "./__new/router" +import "./migrations" + +import { messageStream } from "../shared/messages" +import { initWorkers } from "./workers" +import { initBadgeText } from "./transactions/badgeText" +import { transactionTrackerWorker } from "./transactions/service/worker" +import { handleMessage } from "./messageHandling/handle" +import { addMessageListeners } from "./messageHandling/addMessageListeners" +import { chromeScheduleService } from "../shared/schedule" + +// uncomment to check background error handling +// throw new Error("error for testing") + +// badge shown on extension icon +initBadgeText() + +// load transaction history +void chromeScheduleService.onStartup({ + id: "loadTransactionHistory", + callback: () => transactionTrackerWorker.loadHistory(), +}) + +addMessageListeners() + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +messageStream.subscribe(handleMessage) + +// start workers +initWorkers() diff --git a/packages/extension/src/background/messageHandling/addMessageListeners.ts b/packages/extension/src/background/messageHandling/addMessageListeners.ts index 640273ecb..f4eb0d243 100644 --- a/packages/extension/src/background/messageHandling/addMessageListeners.ts +++ b/packages/extension/src/background/messageHandling/addMessageListeners.ts @@ -27,11 +27,17 @@ export const addMessageListeners = () => { } case "SIGN_MESSAGE": { - const [message] = + const [typedData] = await StarknetMethodArgumentsSchemas.signMessage.parseAsync([ - msg.data, + msg.data.typedData, ]) - return handleMessage([{ ...msg, data: message }, sender], port) + return handleMessage( + [ + { ...msg, data: { typedData, options: msg.data.options } }, + sender, + ], + port, + ) } default: diff --git a/packages/extension/src/background/messageHandling/handle.ts b/packages/extension/src/background/messageHandling/handle.ts index d3170a001..5ca253219 100644 --- a/packages/extension/src/background/messageHandling/handle.ts +++ b/packages/extension/src/background/messageHandling/handle.ts @@ -7,7 +7,7 @@ import { migrateWallet } from "../../shared/wallet/storeMigration" import { backgroundActionService } from "../__new/services/action" import { handleAccountMessage } from "../accountMessaging" import { handleActionMessage } from "../actionMessaging" -import { hasTab, sendMessageToActiveTabs } from "../activeTabs" +import { addTab, hasTab, sendMessageToActiveTabs } from "../activeTabs" import { BackgroundService, HandleMessage, @@ -55,7 +55,7 @@ export const handleMessage = async ( actionService: backgroundActionService, } - const extensionUrl = browser.extension.getURL("") + const extensionUrl = browser.runtime.getURL("") const safeOrigin = extensionUrl.replace(/\/$/, "") const origin = getOriginFromSender(sender) const isSafeOrigin = Boolean(origin === safeOrigin) @@ -82,6 +82,14 @@ export const handleMessage = async ( } } + if (sender.tab?.id && port) { + await addTab({ + id: sender.tab?.id, + host: origin, + port, + }) + } + for (const handleMessage of handlers) { try { await handleMessage({ diff --git a/packages/extension/src/background/migrations/index.ts b/packages/extension/src/background/migrations/index.ts index 175aafcad..50641e61b 100644 --- a/packages/extension/src/background/migrations/index.ts +++ b/packages/extension/src/background/migrations/index.ts @@ -4,6 +4,7 @@ import { runRemoveTestnet2Migration } from "./network/removeTestnet2" import { KeyValueStorage } from "../../shared/storage" import { runRemoveTestnet2Accounts, runV581Migration } from "./wallet" import { runV59TokenMigration } from "./token/v5.9" +import { runV510TokenMigration } from "./token/v5.10" enum WalletMigrations { v581 = "wallet:v581", @@ -16,6 +17,7 @@ enum NetworkMigrations { enum TokenMigrations { v59 = "token:v59", + v510 = "token:v510", } const migrationsStore = new KeyValueStorage( @@ -24,6 +26,7 @@ const migrationsStore = new KeyValueStorage( [WalletMigrations.removeTestnet2Accounts]: false, [NetworkMigrations.removeTestnet2]: false, [TokenMigrations.v59]: false, + [TokenMigrations.v510]: false, }, "core:migrations", ) @@ -41,6 +44,7 @@ export const migrationListener = walletSessionServiceEmitter.on( WalletMigrations.removeTestnet2Accounts, ) const v59Migration = await migrationsStore.get(TokenMigrations.v59) + const v510Migration = await migrationsStore.get(TokenMigrations.v510) if (!v581Migration) { await runV581Migration() await migrationsStore.set(WalletMigrations.v581, true) @@ -58,6 +62,11 @@ export const migrationListener = walletSessionServiceEmitter.on( await runV59TokenMigration() await migrationsStore.set(TokenMigrations.v59, true) } + + if (!v510Migration) { + await runV510TokenMigration() + await migrationsStore.set(TokenMigrations.v510, true) + } } }, ) diff --git a/packages/extension/src/background/migrations/token/v5.10.ts b/packages/extension/src/background/migrations/token/v5.10.ts new file mode 100644 index 000000000..155b48cb8 --- /dev/null +++ b/packages/extension/src/background/migrations/token/v5.10.ts @@ -0,0 +1,7 @@ +import { tokenService } from "../../../shared/token/__new/service" +import { parsedDefaultTokens } from "../../../shared/token/__new/utils" + +export async function runV510TokenMigration() { + // This will add the updated default tokens (with id) to the token service + await tokenService.addToken(parsedDefaultTokens) +} diff --git a/packages/extension/src/shared/multisig/worker/implementation.ts b/packages/extension/src/background/multisig/worker/implementation.ts similarity index 83% rename from packages/extension/src/shared/multisig/worker/implementation.ts rename to packages/extension/src/background/multisig/worker/implementation.ts index 5fd800ea2..a157e9ef0 100644 --- a/packages/extension/src/shared/multisig/worker/implementation.ts +++ b/packages/extension/src/background/multisig/worker/implementation.ts @@ -1,35 +1,42 @@ import { flatMap, isEmpty, partition } from "lodash-es" import { hash, transaction } from "starknet" -import { INetworkService } from "../../network/service/interface" -import { getChainIdFromNetworkId } from "../../network/utils" import { - sendMultisigAccountReadyNotification, - sendMultisigTransactionNotification, -} from "../../notification" -import { IScheduleService } from "../../schedule/interface" + IBackgroundUIService, + Opened, +} from "../../../background/__new/services/ui/interface" +import { IScheduleService } from "../../../shared/schedule/interface" +import { IMultisigBackendService } from "../../../shared/multisig/service/backend/interface" +import { INetworkService } from "../../../shared/network/service/interface" +import { RefreshInterval } from "../../../shared/config" +import { + getAllPendingMultisigs, + pendingMultisigToMultisig, +} from "../../../shared/multisig/utils/pendingMultisig" +import { BasePendingMultisig } from "../../../shared/multisig/types" import { BaseMultisigWalletAccount, - baseMultisigWalletAccountSchema, BaseWalletAccount, MultisigWalletAccount, -} from "../../wallet.model" + baseMultisigWalletAccountSchema, +} from "../../../shared/wallet.model" +import { + sendMultisigAccountReadyNotification, + sendMultisigTransactionNotification, +} from "../../../shared/notification" +import { getMultisigAccounts } from "../../../shared/multisig/utils/baseMultisig" +import { multisigBaseWalletRepo } from "../../../shared/multisig/repository" import { MultisigPendingTransaction, addToMultisigPendingTransactions, getMultisigPendingTransactions, multisigPendingTransactionToTransaction, -} from "../pendingTransactionsStore" -import { multisigBaseWalletRepo } from "../repository" -import { BasePendingMultisig } from "../types" -import { getMultisigAccounts } from "../utils/baseMultisig" -import { - getAllPendingMultisigs, - pendingMultisigToMultisig, -} from "../utils/pendingMultisig" -import { getMultisigTransactionType } from "../utils/getMultisigTransactionType" -import { IMultisigBackendService } from "../service/backend/interface" -import { RefreshInterval } from "../../config" +} from "../../../shared/multisig/pendingTransactionsStore" +import { getMultisigTransactionType } from "../../../shared/multisig/utils/getMultisigTransactionType" +import { getChainIdFromNetworkId } from "../../../shared/network/utils" +import { everyWhenOpen } from "../../__new/services/worker/schedule/decorators" +import { IDebounceService } from "../../../shared/debounce" +import { pipe } from "../../__new/services/worker/schedule/pipe" const id = "multisigUpdate" @@ -39,17 +46,19 @@ export class MultisigWorker { constructor( private readonly scheduleService: IScheduleService, private readonly multisigBackendService: IMultisigBackendService, + private readonly backgroundUiService: IBackgroundUIService, + private readonly debounceService: IDebounceService, private networkService: Pick, - ) { - void this.scheduleService.registerImplementation({ - id, - callback: this.updateAll.bind(this), - }) - - void this.scheduleService.every(RefreshInterval.FAST, { id }) - } - - async updateAll(): Promise { + ) {} + + updateAll = pipe( + everyWhenOpen( + this.backgroundUiService, + this.scheduleService, + this.debounceService, + RefreshInterval.FAST, + ), + )(async (): Promise => { console.log("Updating multisig data") await Promise.all([ this.updateDataForPendingMultisig(), @@ -58,7 +67,7 @@ export class MultisigWorker { ]) console.log("Updated multisig data. Sleeping for 20 seconds") - } + }) async updateDataForPendingMultisig() { // get all base mutlisig accounts diff --git a/packages/extension/src/background/multisig/worker/index.ts b/packages/extension/src/background/multisig/worker/index.ts new file mode 100644 index 000000000..b9d209798 --- /dev/null +++ b/packages/extension/src/background/multisig/worker/index.ts @@ -0,0 +1,14 @@ +import { backgroundUIService } from "../../../background/__new/services/ui" +import { debounceService } from "../../../shared/debounce" +import { argentMultisigBackendService } from "../../../shared/multisig/service/backend" +import { networkService } from "../../../shared/network/service" +import { chromeScheduleService } from "../../../shared/schedule" +import { MultisigWorker } from "./implementation" + +export const multisigWorker = new MultisigWorker( + chromeScheduleService, + argentMultisigBackendService, + backgroundUIService, + debounceService, + networkService, +) diff --git a/packages/extension/src/background/preAuthorizationMessaging.ts b/packages/extension/src/background/preAuthorizationMessaging.ts index c671ed548..9d8afeee6 100644 --- a/packages/extension/src/background/preAuthorizationMessaging.ts +++ b/packages/extension/src/background/preAuthorizationMessaging.ts @@ -5,7 +5,7 @@ import { uiService } from "../shared/__new/services/ui" import { PreAuthorisationMessage } from "../shared/messages/PreAuthorisationMessage" import { isPreAuthorized, preAuthorizeStore } from "../shared/preAuthorizations" import { Opened, backgroundUIService } from "./__new/services/ui" -import { addTab, sendMessageToHost } from "./activeTabs" +import { sendMessageToHost } from "./activeTabs" import { UnhandledMessage } from "./background" import { HandleMessage } from "./background" @@ -32,24 +32,7 @@ preAuthorizeStore.subscribe(async (_, changeSet) => { export const handlePreAuthorizationMessage: HandleMessage< PreAuthorisationMessage -> = async ({ - msg, - sender, - origin, - port, - background: { wallet, actionService }, - respond, -}) => { - async function addSenderTab() { - if (sender.tab?.id && port) { - await addTab({ - id: sender.tab?.id, - host: origin, - port, - }) - } - } - +> = async ({ msg, origin, background: { wallet, actionService }, respond }) => { switch (msg.type) { case "CONNECT_DAPP": { let selectedAccount = await wallet.getSelectedAccount() @@ -67,7 +50,6 @@ export const handlePreAuthorizationMessage: HandleMessage< } const isAuthorized = await isPreAuthorized(selectedAccount, origin) - await addSenderTab() if (!isAuthorized) { /** Prompt user to connect to dapp */ @@ -116,7 +98,6 @@ export const handlePreAuthorizationMessage: HandleMessage< case "IS_PREAUTHORIZED": { const selectedAccount = await wallet.getSelectedAccount() - await addSenderTab() if (!selectedAccount) { return respond({ type: "IS_PREAUTHORIZED_RES", data: false }) diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts index b8ce5771e..192172ddd 100644 --- a/packages/extension/src/background/transactions/transactionExecution.ts +++ b/packages/extension/src/background/transactions/transactionExecution.ts @@ -1,9 +1,6 @@ import { - Call, - EstimateFee, constants, num, - stark, TransactionFinalityStatus, TransactionExecutionStatus, } from "starknet" @@ -11,8 +8,6 @@ import { ExtQueueItem, TransactionActionPayload, } from "../../shared/actionQueue/types" -import { getL1GasPrice } from "../../shared/ethersUtils" -import { AllowArray } from "../../shared/storage/types" import { ExtendedTransactionStatus, TransactionRequest, @@ -200,37 +195,3 @@ export const executeTransactionAction = async ( return transaction } - -export const calculateEstimateFeeFromL1Gas = async ( - account: WalletAccount, - transactions: AllowArray, -): Promise => { - const fallbackPrice = num.toBigInt(10e14) - try { - if (account.networkId === "localhost") { - console.log("Using fallback gas price for localhost") - return { - overall_fee: fallbackPrice, - suggestedMaxFee: stark.estimatedFeeToMaxFee(fallbackPrice), - } - } - - const l1GasPrice = await getL1GasPrice(account.networkId) - - const callsLen = Array.isArray(transactions) ? transactions.length : 1 - const multiplier = BigInt(3744) - - const price = l1GasPrice.mul(callsLen).mul(multiplier).toString() - - return { - overall_fee: num.toBigInt(price), - suggestedMaxFee: stark.estimatedFeeToMaxFee(price), - } - } catch { - console.warn("Could not get L1 gas price") - return { - overall_fee: fallbackPrice, - suggestedMaxFee: stark.estimatedFeeToMaxFee(fallbackPrice), - } - } -} diff --git a/packages/extension/src/background/transactions/transactionMessaging.ts b/packages/extension/src/background/transactions/transactionMessaging.ts index 920494f68..9c0dbf9c6 100644 --- a/packages/extension/src/background/transactions/transactionMessaging.ts +++ b/packages/extension/src/background/transactions/transactionMessaging.ts @@ -154,7 +154,7 @@ export const handleTransactionMessage: HandleMessage< await wallet.getAccountDeploymentFee(account) const maxADFee = num.toHex( - stark.estimatedFeeToMaxFee(suggestedMaxFee, 1), // This adds the 3x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 3x + stark.estimatedFeeToMaxFee(suggestedMaxFee, 1), // This adds the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x ) return respond({ @@ -257,7 +257,7 @@ export const handleTransactionMessage: HandleMessage< } } - const suggestedMaxFee = argentMaxFee(maxTxFee) // This add the 3x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 3x + const suggestedMaxFee = argentMaxFee(maxTxFee) // This add the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x return respond({ type: "ESTIMATE_DECLARE_CONTRACT_FEE_RES", @@ -349,7 +349,7 @@ export const handleTransactionMessage: HandleMessage< } } - const suggestedMaxFee = argentMaxFee(maxTxFee) // This adds the 3x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 3x + const suggestedMaxFee = argentMaxFee(maxTxFee) // This adds the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x return respond({ type: "ESTIMATE_DEPLOY_CONTRACT_FEE_RES", diff --git a/packages/extension/src/background/utils/argentMaxFee.ts b/packages/extension/src/background/utils/argentMaxFee.ts index 291379aec..f6dc15686 100644 --- a/packages/extension/src/background/utils/argentMaxFee.ts +++ b/packages/extension/src/background/utils/argentMaxFee.ts @@ -1,12 +1,12 @@ -import { num, stark } from "starknet" +import { num } from "starknet" /** * - * This method calculate the max fee for argent. Argent adds a 3x overhead to the fee. + * This method calculate the max fee for argent. Argent keeps the 1.5x overhead to the fee. * * @param suggestedMaxFee: fee calculated in starknetjs with the formula: overall_fee * 1.5 - * @returns argentMaxFee: fee calculated by argent x overhead. argentMaxFee = suggestedMaxFee * 2 = overall_fee * 3 + * @returns argentMaxFee: currently equal to suggestedMaxFee * * */ export const argentMaxFee = (suggestedMaxFee: num.BigNumberish) => - num.toHex(stark.estimatedFeeToMaxFee(suggestedMaxFee, 1)) + num.toHex(suggestedMaxFee) diff --git a/packages/extension/src/background/wallet/account/starknet.service.ts b/packages/extension/src/background/wallet/account/starknet.service.ts index 97f164188..4cfac309a 100644 --- a/packages/extension/src/background/wallet/account/starknet.service.ts +++ b/packages/extension/src/background/wallet/account/starknet.service.ts @@ -102,20 +102,22 @@ export class WalletAccountStarknetService { return this.getStarknetAccountOfType(starknetAccount, account.type) } + /// TODO: Get rid of this deprecated code const providerV4 = getProviderv4__deprecated(account.network) - const oldAccount = new AccountV4__deprecated( - providerV4, - account.address, - isKeyPair(signer) - ? ec__deprecated.getKeyPair(signer.getPrivate()) - : signer, - ) + const oldAccount = providerV4 + ? new AccountV4__deprecated( + providerV4, + account.address, + isKeyPair(signer) + ? ec__deprecated.getKeyPair(signer.getPrivate()) + : signer, + ) + : null - const isOldAccount = await isNonceManagedOnAccountContract( - oldAccount, - account, - ) + const isOldAccount = oldAccount + ? await isNonceManagedOnAccountContract(oldAccount, account) + : false // Keep the fallback here as we don't want to block the users // if the worker has not updated the account yet @@ -140,7 +142,7 @@ export class WalletAccountStarknetService { accountCairoVersion, ) - return isOldAccount + return isOldAccount && oldAccount ? oldAccount : this.getStarknetAccountOfType(starknetAccount, account.type) } diff --git a/packages/extension/src/background/wallet/crypto/shared.service.ts b/packages/extension/src/background/wallet/crypto/shared.service.ts index 1ee046ece..e569c6cdc 100644 --- a/packages/extension/src/background/wallet/crypto/shared.service.ts +++ b/packages/extension/src/background/wallet/crypto/shared.service.ts @@ -1,6 +1,11 @@ import { WalletBackupService } from "../backup/backup.service" -import { ethers } from "ethers" +import { + decryptKeystoreJson, + encryptKeystoreJson, + HDNodeWallet, + Mnemonic, +} from "ethers" import { defaultNetwork } from "../../../shared/network" import { WalletRecoverySharedService } from "../recovery/shared.service" import { WalletSessionService } from "../session/session.service" @@ -8,6 +13,7 @@ import type { WalletSession } from "../session/walletSession.model" import { IWalletDeploymentService } from "../deployment/interface" import { IObjectStore } from "../../../shared/storage/__new/interface" import { WalletError } from "../../../shared/errors/wallet" +import { walletToKeystore } from "../utils" export class WalletCryptoSharedService { constructor( @@ -24,10 +30,14 @@ export class WalletCryptoSharedService { if ((await this.backupService.isInitialized()) || session) { throw new WalletError({ code: "ALREADY_INITIALIZED" }) } - const ethersWallet = ethers.Wallet.fromMnemonic(seedPhrase) - const encryptedBackup = await ethersWallet.encrypt(newPassword, { - scrypt: { N: this.SCRYPT_N }, - }) + const ethersWallet = HDNodeWallet.fromPhrase(seedPhrase) + const encryptedBackup = await encryptKeystoreJson( + walletToKeystore(ethersWallet), + newPassword, + { + scrypt: { N: this.SCRYPT_N }, + }, + ) await this.backupService.importBackup(encryptedBackup) await this.sessionService.setSession(ethersWallet.privateKey, newPassword) @@ -50,11 +60,14 @@ export class WalletCryptoSharedService { throw new Error("Session is not open") } - const wallet = await ethers.Wallet.fromEncryptedJson( - backup, - session.password, - ) + const keystore = await decryptKeystoreJson(backup, session.password) + + if (!keystore.mnemonic?.entropy) { + throw new Error("No entropy found in keystore") + } + + const mnemonic = Mnemonic.fromEntropy(keystore.mnemonic.entropy) - return wallet.mnemonic.phrase + return mnemonic.phrase } } diff --git a/packages/extension/src/background/wallet/index.ts b/packages/extension/src/background/wallet/index.ts index ea669705b..5812c0eb8 100644 --- a/packages/extension/src/background/wallet/index.ts +++ b/packages/extension/src/background/wallet/index.ts @@ -19,7 +19,7 @@ import { WalletDeploymentStarknetService } from "./deployment/starknet.service" import { WalletRecoverySharedService } from "./recovery/shared.service" import { WalletSessionService } from "./session/session.service" import { WalletRecoveryStarknetService } from "./recovery/starknet.service" -import { ProgressCallback } from "ethers/lib/utils" +import { ProgressCallback } from "ethers" export class Wallet { constructor( diff --git a/packages/extension/src/background/wallet/recovery/shared.service.test.ts b/packages/extension/src/background/wallet/recovery/shared.service.test.ts index c00849aad..74fc36ac5 100644 --- a/packages/extension/src/background/wallet/recovery/shared.service.test.ts +++ b/packages/extension/src/background/wallet/recovery/shared.service.test.ts @@ -23,15 +23,6 @@ import { WalletRecoverySharedService } from "./shared.service" import { WalletRecoveryStarknetService } from "./starknet.service" import { WalletError } from "../../../shared/errors/wallet" -vi.mock("ethers", async () => { - const actual = await vi.importActual("ethers") - - return { - ...(actual as object), - Wallet: vi.fn().mockReturnValue({ privateKey: "abc" }), - } -}) - describe("WalletRecoverySharedService", () => { let service: WalletRecoverySharedService let storeMock: IObjectStore diff --git a/packages/extension/src/background/wallet/recovery/shared.service.ts b/packages/extension/src/background/wallet/recovery/shared.service.ts index de91d3f9f..198f4e16b 100644 --- a/packages/extension/src/background/wallet/recovery/shared.service.ts +++ b/packages/extension/src/background/wallet/recovery/shared.service.ts @@ -1,5 +1,3 @@ -import { ethers } from "ethers" - import { defaultNetworks } from "../../../shared/network" import { INetworkService } from "../../../shared/network/service/interface" import { @@ -28,7 +26,6 @@ export class WalletRecoverySharedService { if (!session?.secret) { throw new WalletError({ code: "NOT_INITIALIZED" }) } - const wallet = new ethers.Wallet(session?.secret) const networks = defaultNetworks.map((network) => network.id) @@ -38,7 +35,7 @@ export class WalletRecoverySharedService { const network = await this.networkService.getById(networkId) const accountResults = await this.chainRecoveryService.restoreAccountsFromWallet( - wallet.privateKey, + session.secret, network, ) accounts.push(...accountResults) diff --git a/packages/extension/src/background/wallet/session/session.service.ts b/packages/extension/src/background/wallet/session/session.service.ts index 90771e237..944e3cfc3 100644 --- a/packages/extension/src/background/wallet/session/session.service.ts +++ b/packages/extension/src/background/wallet/session/session.service.ts @@ -1,6 +1,10 @@ import Emittery from "emittery" -import { ethers } from "ethers" -import { ProgressCallback } from "ethers/lib/utils" +import { + ProgressCallback, + Wallet, + decryptKeystoreJson, + encryptKeystoreJson, +} from "ethers" import { noop, throttle } from "lodash-es" import { SessionError } from "../../../shared/errors/session" @@ -14,6 +18,7 @@ import { } from "../backup/backup.service" import { WalletRecoverySharedService } from "../recovery/shared.service" import { Events, Locked } from "./interface" +import { walletToKeystore } from "../utils" type TaskId = "sessionTimeout" @@ -34,6 +39,7 @@ export interface WalletSession { export class WalletSessionService { private _locked = true + private isInitialising = true constructor( readonly emitter: Emittery, @@ -84,7 +90,7 @@ export class WalletSessionService { } try { - const wallet = await ethers.Wallet.fromEncryptedJson( + const wallet = await decryptKeystoreJson( backup, password, throttledProgressCallback, @@ -113,12 +119,12 @@ export class WalletSessionService { return } - const ethersWallet = ethers.Wallet.createRandom() - const encryptedBackup = await ethersWallet.encrypt( - password, - { scrypt: { N: this.SCRYPT_N } }, + const ethersWallet = Wallet.createRandom() + const keystore = walletToKeystore(ethersWallet) + const encryptedBackup = await encryptKeystoreJson(keystore, password, { + scrypt: { N: this.SCRYPT_N }, progressCallback, - ) + }) await this.store.set({ discoveredOnce: true }) await this.store.set({ backup: encryptedBackup }) @@ -155,6 +161,12 @@ export class WalletSessionService { } private set locked(locked: boolean) { + if (this.isInitialising) { + /** don't emit on initial value change */ + this.isInitialising = false + this._locked = locked + return + } if (this._locked === locked) { return } diff --git a/packages/extension/src/background/wallet/test.utils.ts b/packages/extension/src/background/wallet/test.utils.ts index 43843db97..416c06c14 100644 --- a/packages/extension/src/background/wallet/test.utils.ts +++ b/packages/extension/src/background/wallet/test.utils.ts @@ -23,6 +23,7 @@ import { WalletRecoveryStarknetService } from "./recovery/starknet.service" import { WalletSessionService } from "./session/session.service" import { Wallet } from "." import { MultisigBackendService } from "../../shared/multisig/service/backend/implementation" +import { IScheduleService } from "../../shared/schedule/interface" const isDev = true const isTest = true @@ -101,11 +102,13 @@ export const backupServiceMock = new WalletBackupService( networkServiceMock, ) -const schedulingServiceMock = { +const schedulingServiceMock: IScheduleService = { in: vi.fn(), every: vi.fn(), delete: vi.fn(), registerImplementation: vi.fn(), + onInstallAndUpgrade: vi.fn(), + onStartup: vi.fn(), } export const emitterMock = { diff --git a/packages/extension/src/background/wallet/utils.ts b/packages/extension/src/background/wallet/utils.ts new file mode 100644 index 000000000..768a6342f --- /dev/null +++ b/packages/extension/src/background/wallet/utils.ts @@ -0,0 +1,18 @@ +import { HDNodeWallet, KeystoreAccount } from "ethers" + +export function walletToKeystore(wallet: HDNodeWallet): KeystoreAccount { + const account: KeystoreAccount = { + address: wallet.address, + privateKey: wallet.privateKey, + } + const m = wallet.mnemonic + if (wallet.path && m?.wordlist.locale === "en" && m?.password === "") { + account.mnemonic = { + path: wallet.path, + locale: "en", + entropy: m.entropy, + } + } + + return account +} diff --git a/packages/extension/src/background/workers.ts b/packages/extension/src/background/workers.ts index 9ea1bf56a..16eda233e 100644 --- a/packages/extension/src/background/workers.ts +++ b/packages/extension/src/background/workers.ts @@ -1,5 +1,5 @@ -import { multisigWorker } from "../shared/multisig/worker" -import { nftsWorker } from "../shared/nft/worker" +import { multisigWorker } from "./multisig/worker" +import { nftsWorker } from "./__new/services/nft/worker" import { analyticsWorker } from "./__new/services/analytics" import { networkWorker } from "./__new/services/network" import { onboardingWorker } from "./__new/services/onboarding" diff --git a/packages/extension/src/content.ts b/packages/extension/src/content.ts index 2fa077495..9438eee8b 100644 --- a/packages/extension/src/content.ts +++ b/packages/extension/src/content.ts @@ -1,5 +1,6 @@ import { Relayer, WindowMessenger } from "@argent/x-window" import { relay } from "trpc-browser/relay" +import { autoConnect } from "trpc-browser/shared/chrome" import browser from "webextension-polyfill" import { ExtensionMessenger } from "./shared/extensionMessenger" @@ -17,15 +18,26 @@ container.insertBefore(script, container.children[0]) const windowMessenger = new WindowMessenger(window, { post: window.location.origin, }) -const port = browser.runtime.connect() -const portMessenger = new ExtensionMessenger(port) -const bridge = new Relayer(windowMessenger, portMessenger) +void autoConnect( + () => browser.runtime.connect(), + (port) => { + const portMessenger = new ExtensionMessenger(port) -// Please keep this log statement, it is used to detect if the bridge is loaded -console.log("Legacy Bridge ID:", bridge.id) + const bridge = new Relayer(windowMessenger, portMessenger) -// NOTE: not used yet, as trpc is only used for UI <-> Background comms atm -const unsub = relay(window, port) -// unsub on content script unload -window.addEventListener("unload", unsub) + // Please keep this log statement, it is used to detect if the bridge is loaded + console.log("Legacy Bridge ID:", bridge.id) + + // NOTE: not used yet, as trpc is only used for UI <-> Background comms atm + const unsub = relay(window, port) + // unsub on content script unload + window.addEventListener("unload", unsub) + + return () => { + unsub() + bridge.destroy() + window.removeEventListener("unload", unsub) + } + }, +) diff --git a/packages/extension/src/inpage/ArgentXAccount.ts b/packages/extension/src/inpage/ArgentXAccount.ts index 7d564b5d3..7a2a9265f 100644 --- a/packages/extension/src/inpage/ArgentXAccount.ts +++ b/packages/extension/src/inpage/ArgentXAccount.ts @@ -14,6 +14,7 @@ import { import { sendMessage, waitForMessage } from "./messageActions" import { StarknetMethodArgumentsSchemas } from "@argent/x-window" +import { SignMessageOptions } from "../shared/messages/ActionMessage" /** * This is the latest Account Object that is imported from starknet.js. @@ -129,8 +130,11 @@ export class ArgentXAccount extends Account { } } - public async signMessage(data: typedData.TypedData): Promise { - sendMessage({ type: "SIGN_MESSAGE", data }) + public async signMessage( + typedData: typedData.TypedData, + options: SignMessageOptions = { skipDeploy: false }, + ): Promise { + sendMessage({ type: "SIGN_MESSAGE", data: { typedData, options } }) const { actionHash } = await waitForMessage("SIGN_MESSAGE_RES", 1000) sendMessage({ type: "OPEN_UI" }) diff --git a/packages/extension/src/inpage/ArgentXAccount4.ts b/packages/extension/src/inpage/ArgentXAccount4.ts index a88d5ab67..dd9c49549 100644 --- a/packages/extension/src/inpage/ArgentXAccount4.ts +++ b/packages/extension/src/inpage/ArgentXAccount4.ts @@ -14,6 +14,7 @@ import { import { sendMessage, waitForMessage } from "./messageActions" import { stark } from "starknet" import { StarknetMethodArgumentsSchemas } from "@argent/x-window" +import { SignMessageOptions } from "../shared/messages/ActionMessage" /** * This is Account Object is imported from starknet v4. @@ -82,8 +83,11 @@ export class ArgentXAccount4 extends Account implements AccountInterface { } } - public async signMessage(data: typedData.TypedData): Promise { - sendMessage({ type: "SIGN_MESSAGE", data }) + public async signMessage( + typedData: typedData.TypedData, + options: SignMessageOptions = { skipDeploy: false }, + ): Promise { + sendMessage({ type: "SIGN_MESSAGE", data: { typedData, options } }) const { actionHash } = await waitForMessage("SIGN_MESSAGE_RES", 1000) sendMessage({ type: "OPEN_UI" }) diff --git a/packages/extension/src/inpage/ArgentXProvider.ts b/packages/extension/src/inpage/ArgentXProvider.ts index aa20d539c..b26d46c96 100644 --- a/packages/extension/src/inpage/ArgentXProvider.ts +++ b/packages/extension/src/inpage/ArgentXProvider.ts @@ -1,17 +1,23 @@ -import { BlockIdentifier, Call, Provider } from "starknet" +import { BlockIdentifier, Call, Provider, ProviderInterface } from "starknet" import { Network, getProvider } from "../shared/network" -import { isArgentNetwork } from "../shared/network/utils" +import { + getRandomPublicRPCNode, + isArgentNetwork, +} from "../shared/network/utils" -export class ArgentXProvider extends Provider { +export class ArgentXProvider extends Provider implements ProviderInterface { constructor(network: Network) { // Only expose sequencer provider for argent networks if (isArgentNetwork(network)) { - if (!network.sequencerUrl) { - throw new Error( - `No Sequencer URL found for argent network: ${network.id}`, - ) + const publicRpcNode = getRandomPublicRPCNode(network) + if (network.id === "mainnet-alpha") { + if (!network.sequencerUrl) { + throw new Error("Missing sequencer url for mainnet") + } + super({ sequencer: { baseUrl: network.sequencerUrl } }) + } else { + super({ rpc: { nodeUrl: publicRpcNode.testnet } }) } - super({ sequencer: { baseUrl: network.sequencerUrl } }) } else { // Otherwise, it's a custom network, so we expose the custom provider super(getProvider(network)) diff --git a/packages/extension/src/inpage/ArgentXProvider4.ts b/packages/extension/src/inpage/ArgentXProvider4.ts index bddd8834b..4a10d7d7a 100644 --- a/packages/extension/src/inpage/ArgentXProvider4.ts +++ b/packages/extension/src/inpage/ArgentXProvider4.ts @@ -1,18 +1,24 @@ -import { Call, Provider } from "starknet4" +import { Call, Provider, ProviderInterface } from "starknet4" import { Network } from "../shared/network" -import { isArgentNetwork } from "../shared/network/utils" +import { + getRandomPublicRPCNode, + isArgentNetwork, +} from "../shared/network/utils" import { getProviderv4 } from "../shared/network/provider" -export class ArgentXProviderV4 extends Provider { +export class ArgentXProviderV4 extends Provider implements ProviderInterface { constructor(network: Network) { // Only expose sequencer provider for argent networks if (isArgentNetwork(network)) { - if (!network.sequencerUrl) { - throw new Error( - `No Sequencer URL found for argent network: ${network.id}`, - ) + const publicRpcNode = getRandomPublicRPCNode(network) + if (network.id === "mainnet-alpha") { + if (!network.sequencerUrl) { + throw new Error("Missing sequencer url for mainnet") + } + super({ sequencer: { baseUrl: network.sequencerUrl } }) + } else { + super({ rpc: { nodeUrl: publicRpcNode.testnet } }) } - super({ sequencer: { baseUrl: network.sequencerUrl } }) } else { // Otherwise, it's a custom network, so we expose the custom provider super(getProviderv4(network)) @@ -20,9 +26,6 @@ export class ArgentXProviderV4 extends Provider { } public async callContract(request: Call, blockIdentifier: any) { - // TODO: remove console.log - // TODO: add metrics to track usage - console.log("ArgentXProvider.callContract", request) return super.callContract(request, blockIdentifier) } } diff --git a/packages/extension/src/shared/__new/services/ui/implementation.test.ts b/packages/extension/src/shared/__new/services/ui/implementation.test.ts index 02a2c9ca9..acfe10e0c 100644 --- a/packages/extension/src/shared/__new/services/ui/implementation.test.ts +++ b/packages/extension/src/shared/__new/services/ui/implementation.test.ts @@ -5,14 +5,16 @@ import UIService from "./implementation" describe("UIService", () => { const makeService = () => { const browser = { - browserAction: { + action: { setPopup: vi.fn(), - }, + } as any, + browserAction: {} as any, extension: { getViews: vi.fn(), }, runtime: { getURL: vi.fn(), + getManifest: vi.fn(() => ({ manifest_version: 3 } as any)), }, tabs: { create: vi.fn(), @@ -34,14 +36,14 @@ describe("UIService", () => { test("setDefaultPopup", async () => { const { uiService, browser } = makeService() await uiService.setDefaultPopup() - expect(browser.browserAction.setPopup).toHaveBeenCalledWith({ + expect(browser.action.setPopup).toHaveBeenCalledWith({ popup: "index.html", }) }) test("unsetDefaultPopup", async () => { const { uiService, browser } = makeService() await uiService.unsetDefaultPopup() - expect(browser.browserAction.setPopup).toHaveBeenCalledWith({ + expect(browser.action.setPopup).toHaveBeenCalledWith({ popup: "", }) }) diff --git a/packages/extension/src/shared/__new/services/ui/implementation.ts b/packages/extension/src/shared/__new/services/ui/implementation.ts index ac921dbe3..54a0d02d3 100644 --- a/packages/extension/src/shared/__new/services/ui/implementation.ts +++ b/packages/extension/src/shared/__new/services/ui/implementation.ts @@ -1,10 +1,10 @@ +import { MinimalActionBrowser, getBrowserAction } from "../../../browser" import { DeepPick } from "../../../types/deepPick" import { UI_SERVICE_CONNECT_ID } from "./constants" import { IUIService } from "./interface" type MinimalBrowser = DeepPick< typeof chrome, - | "browserAction.setPopup" | "extension.getViews" | "runtime.getURL" | "tabs.create" @@ -13,7 +13,8 @@ type MinimalBrowser = DeepPick< | "windows.update" | "windows.remove" | "windows.getAll" -> +> & + MinimalActionBrowser export default class UIService implements IUIService { constructor( @@ -22,7 +23,7 @@ export default class UIService implements IUIService { ) {} setDefaultPopup(popup = "index.html") { - return this.browser.browserAction.setPopup({ popup }) + return getBrowserAction(this.browser).setPopup({ popup }) } unsetDefaultPopup() { @@ -35,6 +36,9 @@ export default class UIService implements IUIService { } hasPopup() { + if (typeof window === "undefined") { + return false + } const popup = this.getPopup() return Boolean(popup) } diff --git a/packages/extension/src/shared/account/details/escape.model.ts b/packages/extension/src/shared/account/details/escape.model.ts index 3b2d29117..89a0b19df 100644 --- a/packages/extension/src/shared/account/details/escape.model.ts +++ b/packages/extension/src/shared/account/details/escape.model.ts @@ -1,6 +1,12 @@ import { z } from "zod" -/** https://github.com/argentlabs/argent-contracts-starknet/blob/main/contracts/account/library.cairo#L249-L250 */ +/** + * Cairo 0 + * https://github.com/argentlabs/argent-contracts-starknet/blob/main/contracts/account/library.cairo#L249-L250 + * + * Cairo 1 + * https://github.com/argentlabs/argent-contracts-starknet-private/blob/develop/src/account/argent_account.cairo#L40-L45 + */ export const ESCAPE_TYPE_GUARDIAN = 1 export const ESCAPE_TYPE_SIGNER = 2 diff --git a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts index bc312428f..3445c8848 100644 --- a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts +++ b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts @@ -1,5 +1,5 @@ import { getAccountClassHashFromChain } from "./getAccountClassHashFromChain" -import { tryGetClassHashFromMulticall } from "./tryGetClassHashFromMulticall" +import { tryGetClassHash } from "./tryGetClassHashFromMulticall" import { tryGetClassHashFromProvider } from "./tryGetClassHashFromProvider" import { networkService } from "../../network/service" import { getProvider } from "../../network" @@ -25,10 +25,9 @@ const mockNetworkService = networkService as jest.Mocked const mockGetMulticallForNetwork = getMulticallForNetwork as jest.MockedFunction const mockGetProvider = getProvider as jest.MockedFunction -const mockTryGetClassHashFromMulticall = - tryGetClassHashFromMulticall as jest.MockedFunction< - typeof tryGetClassHashFromMulticall - > +const mockTryGetClassHashFromMulticall = tryGetClassHash as jest.MockedFunction< + typeof tryGetClassHash +> const mockTryGetClassHashFromProvider = tryGetClassHashFromProvider as jest.MockedFunction< typeof tryGetClassHashFromProvider @@ -67,7 +66,9 @@ describe("getAccountClassHashFromChain", () => { } mockGetMulticallForNetwork.mockReturnValueOnce({ - call: vi.fn().mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]), + callContract: vi + .fn() + .mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]), } as any) mockGetProvider.mockReturnValueOnce({ @@ -85,8 +86,10 @@ describe("getAccountClassHashFromChain", () => { expect(mockTryGetClassHashFromMulticall).toHaveBeenCalledWith( call, - mockGetMulticallForNetwork.mock.results[0].value, - mockGetProvider.mock.results[0].value, + expect.objectContaining({ + callContract: expect.any(Function), + getClassHashAt: expect.any(Function), + }), STANDARD_ACCOUNT_CLASS_HASH, ) @@ -140,7 +143,9 @@ describe("getAccountClassHashFromChain", () => { } mockGetMulticallForNetwork.mockReturnValueOnce({ - call: vi.fn().mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]), + callContract: vi + .fn() + .mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]), } as any) mockGetProvider.mockReturnValueOnce({ @@ -164,16 +169,20 @@ describe("getAccountClassHashFromChain", () => { expect(mockTryGetClassHashFromMulticall).toHaveBeenNthCalledWith( 1, first_call, - mockGetMulticallForNetwork.mock.results[0].value, - mockGetProvider.mock.results[0].value, + expect.objectContaining({ + callContract: expect.any(Function), + getClassHashAt: expect.any(Function), + }), STANDARD_ACCOUNT_CLASS_HASH, ) expect(mockTryGetClassHashFromMulticall).toHaveBeenNthCalledWith( 2, second_call, - mockGetMulticallForNetwork.mock.results[0].value, - mockGetProvider.mock.results[0].value, + expect.objectContaining({ + callContract: expect.any(Function), + getClassHashAt: expect.any(Function), + }), MULTISIG_ACCOUNT_CLASS_HASH, ) diff --git a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts index 08ab513aa..6a8040a8a 100644 --- a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts +++ b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts @@ -8,7 +8,7 @@ import { mapImplementationToArgentAccountType } from "../../network/utils" import { ArgentAccountType, WalletAccount } from "../../wallet.model" import { accountsEqual } from "../../utils/accountsEqual" import { addressSchema } from "@argent/shared" -import { tryGetClassHashFromMulticall } from "./tryGetClassHashFromMulticall" +import { tryGetClassHash } from "./tryGetClassHashFromMulticall" import { tryGetClassHashFromProvider } from "./tryGetClassHashFromProvider" import { MULTISIG_ACCOUNT_CLASS_HASH, @@ -73,10 +73,12 @@ export async function getAccountClassHashFromChain( const responses = await Promise.all( classHashWithCalls.map(({ classHash, call }) => { - return tryGetClassHashFromMulticall( + return tryGetClassHash( call, - multicall, - provider, + { + callContract: multicall.callContract.bind(multicall), + getClassHashAt: provider.getClassHashAt.bind(provider), + }, classHash, ) }), diff --git a/packages/extension/src/shared/account/details/getEscape.ts b/packages/extension/src/shared/account/details/getEscape.ts index 85486d1d4..79d44bb7d 100644 --- a/packages/extension/src/shared/account/details/getEscape.ts +++ b/packages/extension/src/shared/account/details/getEscape.ts @@ -23,21 +23,23 @@ export const getEscapeForAccount = async (account: BaseWalletAccount) => { entrypoint: "get_escape", } const multicall = getMulticallForNetwork(network) - let response: string[] = [] + let response: { result: string[] } = { result: [] } try { - response = await multicall.call(call) + response = await multicall.callContract(call) } catch { call.entrypoint = "getEscape" - response = await multicall.call(call) + response = await multicall.callContract(call) } - return shapeResponse(response) + return shapeResponse(response.result) } /* Example responses: +Cairo 0 + inactive [ "0x0", <- activeAt @@ -49,13 +51,30 @@ active "0x63e3bb79", <- activeAt "0x1" <- type ] + +Cairo 1 + +inactive +[ + "0x0", <- ready_at + "0x0" <- escape_type + "0x0" <- new_signer +] + +active +[ + "0x653a33a3", <- ready_at + "0x1", <- escape_type + "0x0" <- new_signer +] + */ const shapeResponse = (response: string[]) => { - if (response.length !== 2) { + if (response.length !== 2 && response.length !== 3) { return } - const [activeAtHex, typeHex] = response + const [activeAtHex, typeHex] = response /** ignore Cairo 1 new_signer */ const activeAt = Number(num.hexToDecimalString(activeAtHex)) const type = Number(num.hexToDecimalString(typeHex)) if ( diff --git a/packages/extension/src/shared/account/details/getGuardian.ts b/packages/extension/src/shared/account/details/getGuardian.ts index 0c4650124..80f786da0 100644 --- a/packages/extension/src/shared/account/details/getGuardian.ts +++ b/packages/extension/src/shared/account/details/getGuardian.ts @@ -18,16 +18,16 @@ export const getGuardianForAccount = async ( entrypoint: "get_guardian", } const multicall = getMulticallForNetwork(network) - let response: string[] = [] + let response: { result: string[] } = { result: [] } try { - response = await multicall.call(call) + response = await multicall.callContract(call) } catch { call.entrypoint = "getGuardian" - response = await multicall.call(call) + response = await multicall.callContract(call) } // if guardian is 0, return undefined - return num.toHex(response[0]) === num.toHex(constants.ZERO) + return num.toHex(response.result[0]) === num.toHex(constants.ZERO) ? undefined - : response[0] + : response.result[0] } diff --git a/packages/extension/src/shared/account/details/getImplementation.ts b/packages/extension/src/shared/account/details/getImplementation.ts index 3edef083a..1eac4bc75 100644 --- a/packages/extension/src/shared/account/details/getImplementation.ts +++ b/packages/extension/src/shared/account/details/getImplementation.ts @@ -24,8 +24,8 @@ export const getImplementationForAccount = async ( entrypoint: "get_implementation", } const multicall = getMulticallForNetwork(network) - const response = await multicall.call(call) - return response[0] + const response = await multicall.callContract(call) + return response.result[0] } catch { try { // If it fails, get implementation Class Hash for Cairo 1 accounts diff --git a/packages/extension/src/shared/account/details/tryGetClassHashFromMulticall.ts b/packages/extension/src/shared/account/details/tryGetClassHashFromMulticall.ts index f4de463e0..7e356ff18 100644 --- a/packages/extension/src/shared/account/details/tryGetClassHashFromMulticall.ts +++ b/packages/extension/src/shared/account/details/tryGetClassHashFromMulticall.ts @@ -1,21 +1,24 @@ -import { Multicall } from "@argent/x-multicall" import { Call, ProviderInterface } from "starknet" import { STANDARD_ACCOUNT_CLASS_HASH } from "../../network/constants" +import { addressSchema } from "@argent/shared" -export async function tryGetClassHashFromMulticall( +export async function tryGetClassHash( call: Call, - multicall: Multicall, - provider: ProviderInterface, + provider: Pick, fallbackClassHash?: string, ): Promise { - return multicall - .call(call) - .then(([classHash]) => classHash) - .catch(() => provider.getClassHashAt(call.contractAddress)) - .catch(() => { + try { + const expected = await provider.callContract(call) + return expected.result[0] + } catch (e) { + try { + const firstFallback = await provider.getClassHashAt(call.contractAddress) + return addressSchema.parse(firstFallback) + } catch (e) { if (fallbackClassHash) { return fallbackClassHash } return STANDARD_ACCOUNT_CLASS_HASH - }) + } + } } diff --git a/packages/extension/src/shared/account/service/implementation.ts b/packages/extension/src/shared/account/service/implementation.ts index c642cc2c0..4bb4deca3 100644 --- a/packages/extension/src/shared/account/service/implementation.ts +++ b/packages/extension/src/shared/account/service/implementation.ts @@ -97,12 +97,18 @@ export class AccountService implements IAccountService { baseWalletAccount: BaseWalletAccount, targetImplementationType?: ArgentAccountType | undefined, ): Promise { - const account = baseWalletAccountSchema.parse(baseWalletAccount) + const baseAccount = baseWalletAccountSchema.parse(baseWalletAccount) + const [account] = await this.get((a) => accountsEqual(a, baseAccount)) + const [upgradeNeeded, correctAcc] = + await this.trpcClient.account.upgrade.mutate({ + account, + targetImplementationType, + }) - return this.trpcClient.account.upgrade.mutate({ - account, - targetImplementationType, - }) + if (!upgradeNeeded) { + // This means we have incorrect state locally, and we should update it with onchain state + await this.upsert(correctAcc) + } } async get( diff --git a/packages/extension/src/shared/account/update.ts b/packages/extension/src/shared/account/update.ts index e48dff74d..62345ac36 100644 --- a/packages/extension/src/shared/account/update.ts +++ b/packages/extension/src/shared/account/update.ts @@ -11,11 +11,11 @@ import { } from "./details/getAndMergeAccountDetails" import { accountService } from "./service" import { multisigBaseWalletRepo } from "../multisig/repository" -import { Multicall } from "@argent/x-multicall" import { getProvider } from "../network" import { networkService } from "../network/service" import { MultisigEntryPointType } from "../multisig/types" import { getAccountCairoVersionFromChain } from "./details/getAccountCairoVersionFromChain" +import { getBatchProvider } from "@argent/x-multicall" type UpdateScope = "all" | "implementation" | "deploy" | "guardian" @@ -87,11 +87,16 @@ export async function updateMultisigAccountDetails( const { address, networkId } = multisigAccount const network = await networkService.getById(networkId) const provider = getProvider(network) - const multicall = new Multicall(provider, network.multicallAddress) - return multicall.call({ + const multicall = getBatchProvider( + provider, + undefined, // batch options + network.multicallAddress, + ) + const { result } = await multicall.callContract({ contractAddress: address, entrypoint, }) + return result }), ) return { diff --git a/packages/extension/src/shared/account/worker/implementation.test.ts b/packages/extension/src/shared/account/worker/implementation.test.ts index bfda44ceb..a34943fe1 100644 --- a/packages/extension/src/shared/account/worker/implementation.test.ts +++ b/packages/extension/src/shared/account/worker/implementation.test.ts @@ -1,4 +1,4 @@ -import { AccountWorker } from "./implementation" +import { AccountUpdaterTaskId, AccountWorker } from "./implementation" import { IAccountService } from "../../account/service/interface" import { IScheduleService } from "../../schedule/interface" import { getMockWalletAccount } from "../../../../test/walletAccount.mock" @@ -10,7 +10,7 @@ vi.mock("../details/getAccountCairoVersionFromChain") describe("AccountWorker", () => { let accountServiceMock: IAccountService - let scheduleService: IScheduleService<"accountUpdate"> + let scheduleService: IScheduleService beforeEach(() => { vi.resetAllMocks() @@ -32,6 +32,8 @@ describe("AccountWorker", () => { in: vi.fn(), every: vi.fn(), delete: vi.fn(), + onInstallAndUpgrade: vi.fn(), + onStartup: vi.fn(), } }) @@ -45,7 +47,7 @@ describe("AccountWorker", () => { "updateAccountCairoVersion", ) - await worker.updateAll() + await worker.runUpdaterForAllTasks() expect(spyUpdateDeployed).toHaveBeenCalled() expect(spyUpdateAccountClassHash).toHaveBeenCalled() diff --git a/packages/extension/src/shared/account/worker/implementation.ts b/packages/extension/src/shared/account/worker/implementation.ts index 82dc4f45b..cfe5d4f1a 100644 --- a/packages/extension/src/shared/account/worker/implementation.ts +++ b/packages/extension/src/shared/account/worker/implementation.ts @@ -5,44 +5,59 @@ import { getAccountClassHashFromChain } from "../details/getAccountClassHashFrom import { getAccountCairoVersionFromChain } from "../details/getAccountCairoVersionFromChain" import { IAccountService } from "../service/interface" import { isUndefined, keyBy } from "lodash-es" -import { RefreshInterval } from "../../config" +import { + onInstallAndUpgrade, + onStartup, +} from "../../../background/__new/services/worker/schedule/decorators" +// This is a worker, and workers should be in the background, not shared +// TODO: move this file +import { AllowArray } from "../../storage/__new/interface" +import { pipe } from "../../../background/__new/services/worker/schedule/pipe" + +export enum AccountUpdaterTaskId { + UPDATE_DEPLOYED = "accountUpdateDeployed", + ACCOUNT_UPDATE_ON_STARTUP = "accountUpdateOnStartup", + ACCOUNT_UPDATE_ON_INSTALL_AND_UPGRADE = "accountUpdateOnInstallAndUpgrade", +} -type TaskId = "accountUpdate" +enum AccountUpdaterTask { + UPDATE_DEPLOYED, + UPDATE_ACCOUNT_CLASS_HASH, + UPDATE_ACCOUNT_CAIRO_VERSION, +} export class AccountWorker { constructor( private readonly accountService: IAccountService, - private readonly scheduleService: IScheduleService, - ) { - void this.scheduleService.registerImplementation({ - id: "accountUpdate", - callback: this.updateAll.bind(this), - }) - - void this.scheduleService.every( - RefreshInterval.SLOW, // 5 minutes - { - id: "accountUpdate", - }, - ) - - // required for the initial update and to prevent tests from failing - setTimeout(() => { - void this.updateImmediately() - }, 100) - } - - async updateAll(): Promise { - // Keeping the promises sequential here as both the functions use upsert - // Using upsert in parallel can cause unexpected results - await this.updateDeployed() - await this.updateAccountClassHash() - await this.updateAccountCairoVersion() - } - - async updateImmediately(): Promise { - await this.updateAccountClassHashImmediately() - await this.updateAccountCairoVersionImmediately() + private readonly scheduleService: IScheduleService, + ) {} + + runUpdaterForAllTasks = pipe( + onStartup(this.scheduleService), // This will run the function on startup + onInstallAndUpgrade(this.scheduleService), // This will run the function on install and upgrade + )(async (): Promise => { + await this.runUpdaterTask([ + AccountUpdaterTask.UPDATE_DEPLOYED, + AccountUpdaterTask.UPDATE_ACCOUNT_CLASS_HASH, + AccountUpdaterTask.UPDATE_ACCOUNT_CAIRO_VERSION, + ]) + }) + + async runUpdaterTask(tasks: AllowArray): Promise { + const updaterTasks = Array.isArray(tasks) ? tasks : [tasks] + for (const task of updaterTasks) { + switch (task) { + case AccountUpdaterTask.UPDATE_DEPLOYED: + await this.updateDeployed() + break + case AccountUpdaterTask.UPDATE_ACCOUNT_CLASS_HASH: + await this.updateAccountClassHash() + break + case AccountUpdaterTask.UPDATE_ACCOUNT_CAIRO_VERSION: + await this.updateAccountCairoVersion() + break + } + } } async updateDeployed(): Promise { diff --git a/packages/extension/src/shared/actionQueue/types.ts b/packages/extension/src/shared/actionQueue/types.ts index 23f542047..95fb4f924 100644 --- a/packages/extension/src/shared/actionQueue/types.ts +++ b/packages/extension/src/shared/actionQueue/types.ts @@ -11,6 +11,7 @@ import { Network } from "../network" import { TransactionMeta } from "../transactions" import { BaseWalletAccount } from "../wallet.model" import { ActionQueueItem } from "./schema" +import { SignMessageOptions } from "../messages/ActionMessage" export type ExtQueueItem = ActionQueueItem & T @@ -42,7 +43,7 @@ export type ActionItem = } | { type: "SIGN" - payload: typedData.TypedData + payload: { typedData: typedData.TypedData; options: SignMessageOptions } } | { type: "REQUEST_TOKEN" diff --git a/packages/extension/src/shared/analytics.ts b/packages/extension/src/shared/analytics.ts index 5c7d4d2e8..4462e1101 100644 --- a/packages/extension/src/shared/analytics.ts +++ b/packages/extension/src/shared/analytics.ts @@ -1,9 +1,8 @@ -import { base64 } from "ethers/lib/utils" +import { base64 } from "@scure/base" import { encode } from "starknet" -import { create } from "zustand" -import { persist } from "zustand/middleware" import { CreateAccountType } from "./wallet.model" +import { KeyValueStorage } from "./storage" const SEGMENT_TRACK_URL = "https://api.segment.io/v1/track" @@ -305,23 +304,32 @@ interface ActiveStoreValues { lastClosed: number } -interface ActiveStore extends ActiveStoreValues { - update: (key: keyof ActiveStoreValues) => void +interface ActiveStoreMethods { + update: (key: keyof ActiveStoreValues) => Promise } -export type IActiveStore = typeof activeStore +class ActiveStore + extends KeyValueStorage + implements ActiveStoreMethods +{ + constructor() { + super( + { + lastOpened: 0, // defaults to tracking once when no value set yet + lastUnlocked: 0, // defaults to tracking once when no value set yet + lastSession: 0, // defaults to tracking once when no value set yet + lastClosed: 0, // defaults to tracking once when no value set yet + }, + { + namespace: "analytics:active", + }, + ) + } -export const activeStore = create()( - persist( - (set) => ({ - lastOpened: 0, // defaults to tracking once when no value set yet - lastUnlocked: 0, // defaults to tracking once when no value set yet - lastSession: 0, // defaults to tracking once when no value set yet - lastClosed: 0, // defaults to tracking once when no value set yet - update: (key) => set((state) => ({ ...state, [key]: Date.now() })), - }), - { - name: "lastSeen", - }, - ), -) + update(key: keyof ActiveStoreValues) { + return this.set(key, Date.now()) + } +} + +export const activeStore = new ActiveStore() +export type IActiveStore = typeof activeStore diff --git a/packages/extension/src/shared/browser.ts b/packages/extension/src/shared/browser.ts new file mode 100644 index 000000000..cd248110e --- /dev/null +++ b/packages/extension/src/shared/browser.ts @@ -0,0 +1,12 @@ +import { DeepPick } from "../shared/types/deepPick" + +export type MinimalActionBrowser = DeepPick< + typeof chrome, + "action" | "browserAction" | "runtime.getManifest" +> + +export function getBrowserAction(browser: MinimalActionBrowser) { + return browser.runtime.getManifest().manifest_version === 2 + ? browser.browserAction + : browser.action +} diff --git a/packages/extension/src/shared/browser/badgeText.ts b/packages/extension/src/shared/browser/badgeText.ts index 4b3b76536..5d854b064 100644 --- a/packages/extension/src/shared/browser/badgeText.ts +++ b/packages/extension/src/shared/browser/badgeText.ts @@ -1,14 +1,11 @@ import browser from "webextension-polyfill" - -/** browserAction is v2 API, action is v3 */ - -const action = browser.browserAction || browser.action +import { getBrowserAction } from "../browser" export const showNotificationBadge = (text: string | number) => { - action.setBadgeText({ + void getBrowserAction(browser).setBadgeText({ text: String(text), }) - action.setBadgeBackgroundColor({ color: "#29C5FF" }) + void getBrowserAction(browser).setBadgeBackgroundColor({ color: "#29C5FF" }) } export const hideNotificationBadge = () => { diff --git a/packages/extension/src/shared/debounce/chrome.ts b/packages/extension/src/shared/debounce/chrome.ts new file mode 100644 index 000000000..eeb36350c --- /dev/null +++ b/packages/extension/src/shared/debounce/chrome.ts @@ -0,0 +1,47 @@ +import { BaseScheduledTask } from "../schedule/interface" +import { KeyValueStorage } from "../storage" +import { + DebouncedImplementedScheduledTask, + IDebounceService, +} from "./interface" + +function shouldRun(lastRun: number, debounce: number): boolean { + return Date.now() - lastRun > debounce +} + +export class DebounceService + implements IDebounceService +{ + // it's okay to keep this inmemory, as worker should ignore if shut down during task + private readonly taskIsRunning = new Map() + + constructor( + private readonly kv: KeyValueStorage<{ + [key: string]: number + }>, + ) {} + + async debounce(task: DebouncedImplementedScheduledTask): Promise { + const lastRun = await this.lastRun(task) + if (this.isRunning(task) || !shouldRun(lastRun ?? 0, task.debounce)) { + // if task is running or last run is within debounce, + return + } + + this.taskIsRunning.set(task.id, true) + await this.kv.set(task.id, Date.now()) + this.taskIsRunning.set(task.id, false) + } + + isRunning(task: BaseScheduledTask): boolean { + return this.taskIsRunning.get(task.id) ?? false + } + + async lastRun(task: BaseScheduledTask): Promise { + const taskRun = await this.kv.get(task.id) + if (!taskRun) { + return + } + return taskRun + } +} diff --git a/packages/extension/src/shared/debounce/index.ts b/packages/extension/src/shared/debounce/index.ts new file mode 100644 index 000000000..1fc824ec4 --- /dev/null +++ b/packages/extension/src/shared/debounce/index.ts @@ -0,0 +1,14 @@ +import { KeyValueStorage } from "../storage" +import { DebounceService } from "./chrome" + +const debounceStorage = new KeyValueStorage( + {}, + { + namespace: "core:debounce", + areaName: "local", + }, +) + +export const debounceService = new DebounceService(debounceStorage) + +export { IDebounceService } from "./interface" diff --git a/packages/extension/src/shared/debounce/interface.ts b/packages/extension/src/shared/debounce/interface.ts new file mode 100644 index 000000000..ef61f5307 --- /dev/null +++ b/packages/extension/src/shared/debounce/interface.ts @@ -0,0 +1,15 @@ +import { + ImplementedScheduledTask, + BaseScheduledTask, +} from "../schedule/interface" + +export interface DebouncedImplementedScheduledTask + extends ImplementedScheduledTask { + debounce: number +} + +export interface IDebounceService { + debounce(task: DebouncedImplementedScheduledTask): Promise + isRunning(task: BaseScheduledTask): boolean + lastRun(task: BaseScheduledTask): Promise +} diff --git a/packages/extension/src/shared/debounce/mock.ts b/packages/extension/src/shared/debounce/mock.ts new file mode 100644 index 000000000..b7b325543 --- /dev/null +++ b/packages/extension/src/shared/debounce/mock.ts @@ -0,0 +1,9 @@ +import { IDebounceService } from "." + +export const getMockDebounceService = (): IDebounceService => { + return { + debounce: vi.fn(() => Promise.resolve()), + isRunning: vi.fn(), + lastRun: vi.fn(), + } +} diff --git a/packages/extension/src/shared/errors/token.ts b/packages/extension/src/shared/errors/token.ts index 248b4dbd5..758225a7e 100644 --- a/packages/extension/src/shared/errors/token.ts +++ b/packages/extension/src/shared/errors/token.ts @@ -7,9 +7,10 @@ export enum TOKEN_ERROR_MESSAGES { TOKEN_PRICE_PARSING_ERROR = "Unable to parse token price response", TOKEN_PRICE_NOT_FOUND = "Token price not found", TOKEN_NOT_FOUND = "Token not found", + TOKEN_DETAILS_NOT_FOUND = "Token details not found", FEE_TOKEN_NOT_FOUND = "Fee token not found", UNABLE_TO_CALCULATE_CURRENCY_VALUE = "Unable to calculate currency value", - NOT_SAFE = "Not a safe integer", + UNSAFE_DECIMALS = "Unsafe decimals in token", } export type TokenValidationErrorMessage = keyof typeof TOKEN_ERROR_MESSAGES diff --git a/packages/extension/src/shared/ethersUtils.ts b/packages/extension/src/shared/ethersUtils.ts deleted file mode 100644 index 25a278814..000000000 --- a/packages/extension/src/shared/ethersUtils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ethers } from "ethers" - -export const getDefaultEthersProvider = (networkId: string) => { - return ethers.getDefaultProvider( - networkId === "mainnet-alpha" ? "mainnet" : "goerli", - ) -} - -export const getL1GasPrice = async (networkId: string) => { - const ethersProvider = getDefaultEthersProvider(networkId) - - const gasInWei = await ethersProvider.getGasPrice() - - return gasInWei -} diff --git a/packages/extension/src/shared/json.ts b/packages/extension/src/shared/json.ts index f17d4c907..a74164d19 100644 --- a/packages/extension/src/shared/json.ts +++ b/packages/extension/src/shared/json.ts @@ -1,4 +1,4 @@ -import { BigNumber } from "ethers" +import { BigNumber } from "@ethersproject/bignumber" export const reviveJsonBigNumber = (_: string, value: any) => { if (value?.type === "BigNumber" && "hex" in value) { diff --git a/packages/extension/src/shared/knownDapps/implementation.ts b/packages/extension/src/shared/knownDapps/implementation.ts index 004707235..6f95e991f 100644 --- a/packages/extension/src/shared/knownDapps/implementation.ts +++ b/packages/extension/src/shared/knownDapps/implementation.ts @@ -30,7 +30,7 @@ export class KnownDappService implements IKnownDappService { const knownDapps = await this.knownDappsRepository.get() const dapp = knownDapps?.find((knownDapp) => - knownDapp.contracts.some( + knownDapp.contracts?.some( (contract) => contract.address === contractAddress && contract.chain === "starknet", ), diff --git a/packages/extension/src/shared/knownDapps/worker/implementation.ts b/packages/extension/src/shared/knownDapps/worker/implementation.ts index a0529a34c..3166db4b3 100644 --- a/packages/extension/src/shared/knownDapps/worker/implementation.ts +++ b/packages/extension/src/shared/knownDapps/worker/implementation.ts @@ -1,6 +1,13 @@ import { IScheduleService } from "../../schedule/interface" import { KnownDappService } from "../implementation" import { RefreshInterval } from "../../config" +import { + every, + onStartup, +} from "../../../background/__new/services/worker/schedule/decorators" +import { pipe } from "../../../background/__new/services/worker/schedule/pipe" +// Worker should be in background, not shared, as they are only used in the background +// TODO: move this file const id = "knownDappsUpdate" @@ -10,23 +17,15 @@ export class KnownDappsWorker { constructor( private readonly scheduleService: IScheduleService, private readonly knownDappsService: KnownDappService, - ) { - void this.scheduleService.registerImplementation({ - id, - callback: this.update.bind(this), - }) + ) {} - // Run once on startup - void this.update() - - // And then every 24 hours - void this.scheduleService.every(RefreshInterval.VERY_SLOW, { id }) - } - - async update(): Promise { + update = pipe( + onStartup(this.scheduleService), // This will run the function on startup + every(this.scheduleService, RefreshInterval.VERY_SLOW), // This will run the function every 24 hours + )(async (): Promise => { console.log("Updating known dapps data") const dapps = await this.knownDappsService.getDapps() await this.knownDappsService.upsert(dapps) - } + }) } diff --git a/packages/extension/src/shared/messages/ActionMessage.ts b/packages/extension/src/shared/messages/ActionMessage.ts index 01b64976d..a228158c6 100644 --- a/packages/extension/src/shared/messages/ActionMessage.ts +++ b/packages/extension/src/shared/messages/ActionMessage.ts @@ -1,7 +1,14 @@ import type { ArraySignatureType, typedData } from "starknet" +export interface SignMessageOptions { + skipDeploy: boolean +} + export type ActionMessage = - | { type: "SIGN_MESSAGE"; data: typedData.TypedData } + | { + type: "SIGN_MESSAGE" + data: { typedData: typedData.TypedData; options: SignMessageOptions } + } | { type: "SIGN_MESSAGE_RES"; data: { actionHash: string } } | { type: "SIGNATURE_FAILURE"; data: { actionHash: string } } | { diff --git a/packages/extension/src/shared/messages/index.ts b/packages/extension/src/shared/messages/index.ts index b77bc5103..eff9c556c 100644 --- a/packages/extension/src/shared/messages/index.ts +++ b/packages/extension/src/shared/messages/index.ts @@ -62,8 +62,9 @@ export async function waitForMessage< export type WaitForMessage = typeof waitForMessage -if ((window).PLAYWRIGHT || IS_DEV) { - ;(window).messageStream = messageStream - ;(window).sendMessage = sendMessage - ;(window).waitForMessage = waitForMessage -} +// // window is not accessible in MV3 +// if ((window).PLAYWRIGHT || IS_DEV) { +// ;(window).messageStream = messageStream +// ;(window).sendMessage = sendMessage +// ;(window).waitForMessage = waitForMessage +// } diff --git a/packages/extension/src/shared/multicall/getMulticall.ts b/packages/extension/src/shared/multicall/getMulticall.ts index 9feb5d2f7..e33bd5d4e 100644 --- a/packages/extension/src/shared/multicall/getMulticall.ts +++ b/packages/extension/src/shared/multicall/getMulticall.ts @@ -1,7 +1,7 @@ -import { Multicall } from "@argent/x-multicall" import { memoize } from "lodash-es" import { Network, getProvider } from "../network" +import { getBatchProvider } from "@argent/x-multicall" const MAX_BATCH_SIZE = process.env.MULTICALL_MAX_BATCH_SIZE @@ -22,12 +22,12 @@ const getMulticallAddress = (network: Network) => { const maxBatchSize = MAX_BATCH_SIZE ? parseInt(MAX_BATCH_SIZE) : 10 const getMemoizeKey = (network: Network) => { - // using chainId here because we want to memoize based on the network. RPC network can have same chainId as sequencer network + // using chainId here because we want to memoize based on the network. RPC network can have same chainId as sequencer network, so also change the key if the prefered method changes const elements = [ network.chainId, getMulticallAddress(network), maxBatchSize, - localStorage.getItem("betaFeatureRpcProvider") === "true", + network.prefer, ] const key = elements.filter(Boolean).join("-") return key @@ -35,13 +35,13 @@ const getMemoizeKey = (network: Network) => { export const getMulticallForNetwork = memoize( (network: Network) => { - const multicall = new Multicall( + const multicall = getBatchProvider( getProvider(network), - getMulticallAddress(network), { batchInterval: 500, maxBatchSize, }, + getMulticallAddress(network), ) return multicall }, diff --git a/packages/extension/src/shared/multisig/service/backend/implementation.test.ts b/packages/extension/src/shared/multisig/service/backend/implementation.test.ts new file mode 100644 index 000000000..fe5c763dd --- /dev/null +++ b/packages/extension/src/shared/multisig/service/backend/implementation.test.ts @@ -0,0 +1,675 @@ +import { constants } from "starknet" +import { getMockNetwork } from "../../../../../test/network.mock" +import { + ApiMultisigAccountData, + ApiMultisigDataForSigner, + ApiMultisigGetRequests, + ApiMultisigTxnResponse, +} from "../../multisig.model" +import { MultisigBackendService } from "./implementation" +import { getMultisigAccountFromBaseWallet } from "../../utils/baseMultisig" +import { getMockAccount } from "../../../../../test/account.mock" +import { chainIdToStarknetNetwork } from "../../../utils/starknetNetwork" + +vi.mock("../../utils/baseMultisig") +vi.mock("../../pendingTransactionsStore") +const addToMultisigPendingTransactionsSpy = vi.fn() +const cancelMultisigPendingTransactionsSpy = vi.fn() +const convertToTransactionSpy = vi.fn() + +const mockGetMultisigAccountFromBaseWallet = + getMultisigAccountFromBaseWallet as jest.MockedFunction< + typeof getMultisigAccountFromBaseWallet + > + +describe("MultisigBackendService", () => { + const mockFetcher = vi.fn() + let mockCurrentTime + + afterEach(() => { + vi.clearAllMocks() + }) + beforeEach(() => { + mockCurrentTime = 1618491623961 // Example timestamp + vi.spyOn(Date, "now").mockReturnValue(mockCurrentTime) + }) + describe("constructor", () => { + it("should throw an error if no baseUrl is provided", () => { + expect(() => new MultisigBackendService()).toThrowError( + "No multisig base url provided", + ) + }) + + it("should instantiate with provided baseUrl", () => { + const service = new MultisigBackendService("http://example.com") + expect(service.baseUrl).toBe("http://example.com") + }) + }) + + describe("fetchMultisigDataForSigner", () => { + it("should correctly construct the URL and fetch the multisig data for the right signer", async () => { + mockFetcher.mockResolvedValueOnce({ + totalPages: 1, + totalElements: 1, + size: 1, + content: [ + { + address: "0x123", + creator: "0x456", + signers: ["0x789"], + threshold: 1, + }, + ], + } as ApiMultisigDataForSigner) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const signerName = "someSigner" + + const response = await service.fetchMultisigDataForSigner({ + signer: signerName, + network: getMockNetwork(), + }) + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/testnet?signer=${signerName}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "GET", + body: undefined, + }, + ) + expect(response).toEqual({ + totalPages: 1, + totalElements: 1, + size: 1, + content: [ + { + address: "0x123", + creator: "0x456", + signers: ["0x789"], + threshold: 1, + }, + ], + }) + + expect(mockFetcher).toHaveBeenCalledTimes(1) + }) + it("should throw on error", async () => { + mockFetcher.mockRejectedValueOnce(new Error("some error")) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const signerName = "someSigner" + + await expect( + service.fetchMultisigDataForSigner({ + signer: signerName, + network: getMockNetwork(), + }), + ).rejects.toThrowError("An error occured Error: some error") + }) + }) + + describe("fetchMultisigAccountData", () => { + it("should call the correct endpoint and return the multisig account data", async () => { + mockFetcher.mockResolvedValueOnce({ + content: { + address: "0x123", + creator: "0x456", + signers: ["0x789"], + threshold: 1, + }, + } as ApiMultisigAccountData) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const address = "0x1" + + const response = await service.fetchMultisigAccountData({ + address: address, + networkId: getMockNetwork().id, + }) + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/testnet/${address}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "GET", + body: undefined, + }, + ) + expect(response).toEqual({ + content: { + address: "0x123", + creator: "0x456", + signers: ["0x789"], + threshold: 1, + }, + }) + + expect(mockFetcher).toHaveBeenCalledTimes(1) + }) + it("should throw on error", async () => { + mockFetcher.mockRejectedValueOnce(new Error("some error")) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const address = "0x1" + + await expect( + service.fetchMultisigAccountData({ + address: address, + networkId: getMockNetwork().id, + }), + ).rejects.toThrowError("An error occured Error: some error") + }) + }) + + describe("fetchMultisigRequests", () => { + it("should call the correct endpoint and return the multisig requests", async () => { + const payload = { + totalElements: 1, + totalPages: 1, + size: 1, + content: [ + { + id: "0x123", + multisigAddress: "0x456", + creator: "0x789", + state: "AWAITING_SIGNATURES", + transaction: { + maxFee: "0", + nonce: "0", + version: "0", + calls: [], + }, + nonce: 0, + nonApprovedSigners: [], + approvedSigners: [], + }, + ], + } as ApiMultisigGetRequests + mockFetcher.mockResolvedValueOnce(payload) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const address = "0x1" + + const response = await service.fetchMultisigRequests({ + address: address, + networkId: getMockNetwork().id, + }) + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/testnet/${address}/request`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "GET", + body: undefined, + }, + ) + + expect(response).toEqual(payload) + + expect(mockFetcher).toHaveBeenCalledTimes(1) + }) + it("should throw on error", async () => { + mockFetcher.mockRejectedValueOnce(new Error("some error")) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + const address = "0x1" + + await expect( + service.fetchMultisigRequests({ + address: address, + networkId: getMockNetwork().id, + }), + ).rejects.toThrowError("An error occured Error: some error") + }) + }) + + describe("addNewTransaction", () => { + it("should call the correct endpoint with the correct payload and return the correct hash", async () => { + const address = "0x1" + + mockGetMultisigAccountFromBaseWallet.mockResolvedValueOnce({ + ...getMockAccount({ + address, + }), + threshold: 1, + signers: ["0x123"], + publicKey: "0x123", + updatedAt: 123, + }) + const payload = { + creator: "0x2b7d60e", + transaction: { + maxFee: "0x1", + nonce: "0x1", + version: "0x1", + calls: [ + { + contractAddress: "randomContractAddress", + entrypoint: "randomEntryPoint", + calldata: ["randomCalldata"], + }, + ], + }, + starknetSignature: { r: "0xbc845e", s: "0x2b7d60e" }, + } + const expectedRes = { + content: { + id: "0x123", + multisigAddress: address, + nonce: 1, + approvedSigners: ["0x123"], + nonApprovedSigners: [], + state: "COMPLETE", + creator: "0x123", + transaction: { + maxFee: "0", + nonce: "0", + version: "0", + calls: [], + }, + starknetSignature: { + r: "0", + s: "0", + }, + }, + } as ApiMultisigTxnResponse + + mockFetcher.mockResolvedValueOnce(expectedRes) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + ) + + const returnValue = await service.addNewTransaction({ + address: address, + signature: [ + BigInt(45602318).toString(), + BigInt(12354654).toString(), + BigInt(45602318).toString(), + BigInt(45602318).toString(), + ], + calls: [ + { + entrypoint: "randomEntryPoint", + calldata: ["randomCalldata"], + contractAddress: "randomContractAddress", + }, + ], + transactionDetails: { + walletAddress: "0x420", + chainId: constants.StarknetChainId.SN_GOERLI, + nonce: 1, + cairoVersion: "1", + maxFee: 1, + version: 1, + }, + }) + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/testnet/${address}/request`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify(payload), + }, + ) + + expect(returnValue).toEqual({ + transaction_hash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + }) + + expect(mockFetcher).toHaveBeenCalledTimes(1) + }) + it("should not execute the transaction directly is multisig threshold is more than 1", async () => { + const address = "0x1" + mockGetMultisigAccountFromBaseWallet.mockResolvedValueOnce({ + ...getMockAccount({ + address, + }), + threshold: 2, + signers: ["0x123"], + publicKey: "0x123", + updatedAt: 123, + }) + const payload = { + creator: "0x2b7d60e", + transaction: { + maxFee: "0x1", + nonce: "0x1", + version: "0x1", + calls: [ + { + contractAddress: "randomContractAddress", + entrypoint: "randomEntryPoint", + calldata: ["randomCalldata"], + }, + ], + }, + starknetSignature: { r: "0xbc845e", s: "0x2b7d60e" }, + } + const expectedRes = { + content: { + id: "0x123", + multisigAddress: address, + nonce: 1, + approvedSigners: ["0x123"], + nonApprovedSigners: [], + state: "COMPLETE", + creator: "0x123", + transaction: { + maxFee: "0", + nonce: "0", + version: "0", + calls: [], + }, + starknetSignature: { + r: "0", + s: "0", + }, + }, + } as ApiMultisigTxnResponse + + mockFetcher.mockResolvedValueOnce(expectedRes) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + addToMultisigPendingTransactionsSpy, + ) + + const returnValue = await service.addNewTransaction({ + address: address, + signature: [ + BigInt(45602318).toString(), + BigInt(12354654).toString(), + BigInt(45602318).toString(), + BigInt(45602318).toString(), + ], + calls: [ + { + entrypoint: "randomEntryPoint", + calldata: ["randomCalldata"], + contractAddress: "randomContractAddress", + }, + ], + transactionDetails: { + walletAddress: "0x420", + chainId: constants.StarknetChainId.SN_GOERLI, + nonce: 1, + cairoVersion: "1", + maxFee: 1, + version: 1, + }, + }) + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/testnet/${address}/request`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify(payload), + }, + ) + + expect(returnValue).toEqual({ + transaction_hash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + }) + + expect(mockFetcher).toHaveBeenCalledTimes(1) + + expect(addToMultisigPendingTransactionsSpy).toHaveBeenCalledTimes(1) + expect(addToMultisigPendingTransactionsSpy).toHaveBeenCalledWith({ + account: { + address: "0x1", + networkId: "goerli-alpha", + }, + approvedSigners: ["0x123"], + creator: "0x123", + id: "0x123", + multisigAddress: "0x1", + nonApprovedSigners: [], + nonce: 1, + notify: false, + requestId: "0x123", + state: "COMPLETE", + timestamp: Date.now(), + transaction: { + calls: [], + maxFee: "0", + nonce: "0", + version: "0", + }, + transactionHash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + type: "INVOKE", + }) + }) + }) + + describe("addRequestSignature", () => { + it("should call the correct endpoint with the correct payload and return the correct hash", async () => { + const address = "0x1" + + mockGetMultisigAccountFromBaseWallet.mockResolvedValueOnce({ + ...getMockAccount({ + address, + }), + threshold: 2, + signers: ["0x123"], + publicKey: "0x123", + updatedAt: 123, + }) + + const expectedRes = { + content: { + id: "0x123", + multisigAddress: address, + nonce: 1, + approvedSigners: ["0x123"], + nonApprovedSigners: [], + state: "COMPLETE", + creator: "0x123", + transaction: { + maxFee: "0", + nonce: "0", + version: "0", + calls: [], + }, + starknetSignature: { + r: "0", + s: "0", + }, + }, + } as ApiMultisigTxnResponse + + mockFetcher.mockResolvedValueOnce(expectedRes) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + addToMultisigPendingTransactionsSpy, + ) + const requestId = "0x6969" + await service.addRequestSignature({ + address: address, + signature: [ + BigInt(45602318).toString(), + BigInt(12354654).toString(), + BigInt(45602318).toString(), + BigInt(45602318).toString(), + ], + transactionToSign: { + account: { + address: "0x1", + networkId: "goerli-alpha", + }, + approvedSigners: ["0x123"], + creator: "0x123", + nonApprovedSigners: [], + nonce: 1, + notify: false, + requestId, + state: "COMPLETE", + timestamp: Date.now(), + transaction: { + calls: [], + maxFee: "0", + nonce: "0", + version: "0", + }, + transactionHash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + type: "INVOKE", + }, + chainId: constants.StarknetChainId.SN_GOERLI, + }) + + expect(mockFetcher).toHaveBeenCalledWith( + `http://example.com/${chainIdToStarknetNetwork( + constants.StarknetChainId.SN_GOERLI, + )}/${address}/request/${requestId}/signature`, + { + body: '{"signer":"0x2b7d60e","starknetSignature":{"r":"0xbc845e","s":"0x2b7d60e"}}', + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + }, + ) + expect(addToMultisigPendingTransactionsSpy).toHaveBeenCalledTimes(1) + expect(addToMultisigPendingTransactionsSpy).toHaveBeenCalledWith({ + account: { + address: "0x1", + networkId: "goerli-alpha", + }, + approvedSigners: ["0x123"], + creator: "0x123", + nonApprovedSigners: [], + nonce: 1, + notify: false, + requestId, + state: "COMPLETE", + timestamp: Date.now(), + transaction: { + calls: [], + maxFee: "0", + nonce: "0", + version: "0", + }, + transactionHash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + type: "INVOKE", + }) + }) + it("should cancel the pending transaction an transform into a transaction if the threshold is met", async () => { + const address = "0x1" + + mockGetMultisigAccountFromBaseWallet.mockResolvedValueOnce({ + ...getMockAccount({ + address, + }), + threshold: 1, + signers: ["0x123"], + publicKey: "0x123", + updatedAt: 123, + }) + + const expectedRes = { + content: { + id: "0x123", + multisigAddress: address, + nonce: 1, + approvedSigners: ["0x123"], + nonApprovedSigners: [], + state: "COMPLETE", + creator: "0x123", + transaction: { + maxFee: "0", + nonce: "0", + version: "0", + calls: [], + }, + starknetSignature: { + r: "0", + s: "0", + }, + }, + } as ApiMultisigTxnResponse + + mockFetcher.mockResolvedValueOnce(expectedRes) + const service = new MultisigBackendService( + "http://example.com", + mockFetcher, + undefined, + cancelMultisigPendingTransactionsSpy, + convertToTransactionSpy, + ) + const requestId = "0x6969" + await service.addRequestSignature({ + address: address, + signature: [ + BigInt(45602318).toString(), + BigInt(12354654).toString(), + BigInt(45602318).toString(), + BigInt(45602318).toString(), + ], + transactionToSign: { + account: { + address: "0x1", + networkId: "goerli-alpha", + }, + approvedSigners: ["0x123"], + creator: "0x123", + nonApprovedSigners: [], + nonce: 1, + notify: false, + requestId, + state: "COMPLETE", + timestamp: Date.now(), + transaction: { + calls: [], + maxFee: "0", + nonce: "0", + version: "0", + }, + transactionHash: + "0x2f66ff9611ad753dd0c69ff11f3d48dbf5d147aa0e42f6079012cd76bc794bc", + type: "INVOKE", + }, + chainId: constants.StarknetChainId.SN_GOERLI, + }) + + expect(cancelMultisigPendingTransactionsSpy).toHaveBeenCalledTimes(1) + expect(cancelMultisigPendingTransactionsSpy).toHaveBeenCalledWith({ + address: "0x1", + networkId: "goerli-alpha", + }) + expect(convertToTransactionSpy).toHaveBeenCalledTimes(1) + expect(convertToTransactionSpy).toHaveBeenCalledWith("0x123", "COMPLETE") + }) + }) +}) diff --git a/packages/extension/src/shared/multisig/service/backend/implementation.ts b/packages/extension/src/shared/multisig/service/backend/implementation.ts index 3216de20b..6a1fd57c9 100644 --- a/packages/extension/src/shared/multisig/service/backend/implementation.ts +++ b/packages/extension/src/shared/multisig/service/backend/implementation.ts @@ -7,7 +7,7 @@ import { starknetNetworkToNetworkId, } from "../../../utils/starknetNetwork" import { urlWithQuery } from "../../../utils/url" -import { BaseWalletAccount } from "../../../wallet.model" +import { BaseWalletAccount, MultisigWalletAccount } from "../../../wallet.model" import { ApiMultisigAccountData, ApiMultisigAccountDataSchema, @@ -16,7 +16,9 @@ import { ApiMultisigDataForSignerSchema, ApiMultisigGetRequests, ApiMultisigGetRequestsSchema, + ApiMultisigPostRequestTxn, ApiMultisigPostRequestTxnSchema, + ApiMultisigState, ApiMultisigTxnResponseSchema, } from "../../multisig.model" import { IMultisigBackendService } from "./interface" @@ -24,10 +26,24 @@ import { IAddNewTransaction, IAddRequestSignature, IFetchMultisigDataForSigner, + IMapTransactionDetails, + IPrepareTransaction, + IProcessNewTransactionResponse, + IProcessRequestSignatureResponse, + MappedTransactionDetails, } from "./types" import { getMultisigAccountFromBaseWallet } from "../../utils/baseMultisig" -import { InvokeFunctionResponse, hash, num, stark, transaction } from "starknet" import { + AllowArray, + InvokeFunctionResponse, + Signature, + hash, + num, + stark, + transaction, +} from "starknet" +import { + MultisigPendingTransaction, addToMultisigPendingTransactions, cancelPendingMultisigTransactions, multisigPendingTransactionToTransaction, @@ -36,36 +52,73 @@ import { MultisigError } from "../../../errors/multisig" export class MultisigBackendService implements IMultisigBackendService { public readonly baseUrl: string - - constructor(baseUrl?: string, private readonly fetcherImpl = fetcher) { + public addToTransactionsStore: ( + payload: AllowArray, + ) => Promise + public cancelPendingTransactions: ( + account: BaseWalletAccount, + ) => Promise + public convertToTransaction: ( + requestId: string, + state: ApiMultisigState, + ) => Promise + + constructor( + baseUrl?: string, + private readonly fetcherImpl = fetcher, + addToTransactionsStore: ( + payload: AllowArray, + ) => Promise = addToMultisigPendingTransactions, + cancelPendingTransactions: ( + account: BaseWalletAccount, + ) => Promise = cancelPendingMultisigTransactions, + convertToTransaction: ( + requestId: string, + state: ApiMultisigState, + ) => Promise = multisigPendingTransactionToTransaction, + ) { if (!baseUrl) { throw new MultisigError({ code: "NO_MULTISIG_BASE_URL", message: "No multisig base url provided", }) } - + this.addToTransactionsStore = addToTransactionsStore + this.cancelPendingTransactions = cancelPendingTransactions + this.convertToTransaction = convertToTransaction this.baseUrl = baseUrl } + private constructUrlForSigner( + starknetNetwork: string, + signer: string, + ): string { + return urlWithQuery([this.baseUrl, starknetNetwork], { signer }) + } + + private async makeApiCall( + url: string, + method: "GET" | "POST" = "GET", + body?: BodyInit | null | undefined, + ): Promise { + return await this.fetcherImpl(url, { + method, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body, + }) + } + async fetchMultisigDataForSigner({ signer, network, }: IFetchMultisigDataForSigner): Promise { try { const starknetNetwork = networkToStarknetNetwork(network) - - const url = urlWithQuery([this.baseUrl, starknetNetwork], { - signer, - }) - - const data = await this.fetcherImpl(url, { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }) + const url = this.constructUrlForSigner(starknetNetwork, signer) + const data = await this.makeApiCall(url) return ApiMultisigDataForSignerSchema.parse(data) } catch (e) { @@ -79,16 +132,8 @@ export class MultisigBackendService implements IMultisigBackendService { }: BaseWalletAccount): Promise { try { const starknetNetwork = networkIdToStarknetNetwork(networkId) - const url = urlJoin(this.baseUrl, starknetNetwork, address) - - const data = await this.fetcherImpl(url, { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }) + const data = await this.makeApiCall(url) return ApiMultisigAccountDataSchema.parse(data) } catch (e) { @@ -102,16 +147,8 @@ export class MultisigBackendService implements IMultisigBackendService { }: BaseWalletAccount): Promise { try { const starknetNetwork = networkIdToStarknetNetwork(networkId) - const url = urlJoin(this.baseUrl, starknetNetwork, address, "request") - - const data = await fetcher(url, { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }) + const data = await this.makeApiCall(url) return ApiMultisigGetRequestsSchema.parse(data) } catch (e) { @@ -119,47 +156,64 @@ export class MultisigBackendService implements IMultisigBackendService { } } - async addNewTransaction({ - address, - signature, - calls, + private mapTransactionDetails({ transactionDetails, - }: IAddNewTransaction): Promise { - const { nonce, version, maxFee, cairoVersion, chainId } = transactionDetails + address, + }: IMapTransactionDetails): MappedTransactionDetails { + const { nonce, version, maxFee, chainId, cairoVersion } = transactionDetails const starknetNetwork = chainIdToStarknetNetwork(chainId) const networkId = starknetNetworkToNetworkId(starknetNetwork) const account: BaseWalletAccount = { address, networkId } - const multisig = await getMultisigAccountFromBaseWallet(account) + return { + nonce, + version, + maxFee, + starknetNetwork, + account, + chainId, + cairoVersion, + } + } + private async fetchMultisigAccount( + account: BaseWalletAccount, + ): Promise { + const multisig = await getMultisigAccountFromBaseWallet(account) if (!multisig) { - throw Error(`Multisig wallet with address ${address} not found`) + throw Error(`Multisig wallet with address ${account.address} not found`) } + return multisig + } + private async prepareTransaction({ + signature, + mappedDetails, + calls, + }: IPrepareTransaction): Promise { const [creator, r, s] = stark.signatureToHexArray(signature) - - const request = ApiMultisigPostRequestTxnSchema.parse({ + return ApiMultisigPostRequestTxnSchema.parse({ creator, transaction: { - nonce: num.toHex(nonce), - version: num.toHex(version), - maxFee: num.toHex(maxFee), + nonce: num.toHex(mappedDetails.nonce), + version: num.toHex(mappedDetails.version), + maxFee: num.toHex(mappedDetails.maxFee), calls, }, starknetSignature: { r, s }, }) + } - const url = urlJoin(this.baseUrl, starknetNetwork, address, "request") - - const response = await fetcher(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - }) - + private async processNewTransactionApiResponse({ + response, + calls, + cairoVersion, + chainId, + nonce, + maxFee, + address, + version, + }: IProcessNewTransactionResponse) { const data = ApiMultisigTxnResponseSchema.parse(response) const calldata = transaction.getExecuteCalldata( @@ -178,12 +232,60 @@ export class MultisigBackendService implements IMultisigBackendService { const transactionHash = data.content.transactionHash ?? computedTransactionHash + return { transactionHash, content: data.content } + } + + async addNewTransaction({ + address, + signature, + calls, + transactionDetails, + }: IAddNewTransaction): Promise { + const { + nonce, + version, + maxFee, + cairoVersion, + chainId, + account, + starknetNetwork, + } = this.mapTransactionDetails({ transactionDetails, address }) + + const multisig = await this.fetchMultisigAccount(account) + const request = await this.prepareTransaction({ + signature, + mappedDetails: { + nonce, + version, + maxFee, + }, + calls, + }) + const url = urlJoin(this.baseUrl, starknetNetwork, address, "request") + + const response = await this.makeApiCall( + url, + "POST", + JSON.stringify(request), + ) + + const { transactionHash, content } = + await this.processNewTransactionApiResponse({ + response, + calls, + cairoVersion, + chainId, + nonce, + maxFee, + address, + version, + }) // If the multisig threshold is 1, we can execute the transaction directly if (multisig.threshold !== 1) { - await addToMultisigPendingTransactions({ - ...data.content, - requestId: data.content.id, + await this.addToTransactionsStore({ + ...content, + requestId: content.id, timestamp: Date.now(), type: "INVOKE", account, @@ -197,6 +299,42 @@ export class MultisigBackendService implements IMultisigBackendService { } } + private async prepareRequestSignature(signature: Signature) { + const [signer, r, s] = stark.signatureToHexArray(signature) + const request = ApiMultisigAddRequestSignatureSchema.parse({ + signer, + starknetSignature: { r, s }, + }) + return request + } + + private async processRequestSignatureApiResponse({ + response, + multisig, + address, + networkId, + transactionToSign, + }: IProcessRequestSignatureResponse) { + const data = ApiMultisigTxnResponseSchema.parse(response) + + if (data.content.approvedSigners.length === multisig.threshold) { + await this.convertToTransaction(data.content.id, data.content.state) + + await this.cancelPendingTransactions({ + address, + networkId, + }) + } else { + await this.addToTransactionsStore({ + ...transactionToSign, + approvedSigners: data.content.approvedSigners, + nonApprovedSigners: data.content.nonApprovedSigners, + state: data.content.state, + notify: false, + }) + } + } + async addRequestSignature({ address, transactionToSign, @@ -205,17 +343,11 @@ export class MultisigBackendService implements IMultisigBackendService { }: IAddRequestSignature): Promise { const starknetNetwork = chainIdToStarknetNetwork(chainId) const networkId = starknetNetworkToNetworkId(starknetNetwork) - const multisig = await getMultisigAccountFromBaseWallet({ + const multisig = await this.fetchMultisigAccount({ address, networkId, }) - if (!multisig) { - throw Error(`Multisig wallet with address ${address} not found`) - } - - const [signer, r, s] = stark.signatureToHexArray(signature) - const url = urlJoin( this.baseUrl, starknetNetwork, @@ -224,43 +356,22 @@ export class MultisigBackendService implements IMultisigBackendService { transactionToSign.requestId, "signature", ) + const request = await this.prepareRequestSignature(signature) - const request = ApiMultisigAddRequestSignatureSchema.parse({ - signer, - starknetSignature: { r, s }, - }) + const response = await this.makeApiCall( + url, + "POST", + JSON.stringify(request), + ) - const response = await fetcher(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(request), + void this.processRequestSignatureApiResponse({ + response, + multisig, + address, + networkId, + transactionToSign, }) - const data = ApiMultisigTxnResponseSchema.parse(response) - - if (data.content.approvedSigners.length === multisig.threshold) { - await multisigPendingTransactionToTransaction( - data.content.id, - data.content.state, - ) - - await cancelPendingMultisigTransactions({ - address, - networkId, - }) - } else { - await addToMultisigPendingTransactions({ - ...transactionToSign, - approvedSigners: data.content.approvedSigners, - nonApprovedSigners: data.content.nonApprovedSigners, - state: data.content.state, - notify: false, - }) - } - return { transaction_hash: transactionToSign.transactionHash, } diff --git a/packages/extension/src/shared/multisig/service/backend/types.ts b/packages/extension/src/shared/multisig/service/backend/types.ts index 9d7dd7015..bc86ef621 100644 --- a/packages/extension/src/shared/multisig/service/backend/types.ts +++ b/packages/extension/src/shared/multisig/service/backend/types.ts @@ -1,6 +1,14 @@ -import { Call, InvocationsSignerDetails, Signature, constants } from "starknet" +import { + BigNumberish, + CairoVersion, + Call, + InvocationsSignerDetails, + Signature, + constants, +} from "starknet" import { MultisigPendingTransaction } from "../../pendingTransactionsStore" import { Network } from "../../../network" +import { BaseWalletAccount, MultisigWalletAccount } from "../../../wallet.model" export interface IFetchMultisigDataForSigner { signer: string @@ -14,9 +22,49 @@ export interface IAddNewTransaction { signature: Signature } +export interface IMapTransactionDetails { + address: string + transactionDetails: InvocationsSignerDetails +} + export interface IAddRequestSignature { address: string transactionToSign: MultisigPendingTransaction chainId: constants.StarknetChainId signature: Signature } + +export interface MappedTransactionDetails { + nonce: BigNumberish + version: BigNumberish + maxFee: BigNumberish + starknetNetwork: string + account: BaseWalletAccount + chainId: constants.StarknetChainId + cairoVersion: CairoVersion +} + +export interface IPrepareTransaction { + signature: Signature + mappedDetails: Pick + calls: Call[] +} + +export interface IProcessNewTransactionResponse { + response: unknown + calls: Call[] + address: string + cairoVersion: CairoVersion + maxFee: BigNumberish + chainId: constants.StarknetChainId + nonce: BigNumberish + version: BigNumberish +} + +export interface IProcessRequestSignatureResponse { + response: unknown + multisig: MultisigWalletAccount + address: string + networkId: string + transactionToSign: MultisigPendingTransaction +} diff --git a/packages/extension/src/shared/multisig/worker/index.ts b/packages/extension/src/shared/multisig/worker/index.ts deleted file mode 100644 index 4378fadb2..000000000 --- a/packages/extension/src/shared/multisig/worker/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { networkService } from "../../network/service" -import { chromeScheduleService } from "../../schedule" -import { argentMultisigBackendService } from "../service/backend" -import { MultisigWorker } from "./implementation" - -export const multisigWorker = new MultisigWorker( - chromeScheduleService, - argentMultisigBackendService, - networkService, -) diff --git a/packages/extension/src/shared/network/constants.ts b/packages/extension/src/shared/network/constants.ts index 404b3265b..0bca5c4a4 100644 --- a/packages/extension/src/shared/network/constants.ts +++ b/packages/extension/src/shared/network/constants.ts @@ -1,3 +1,5 @@ +import { PublicRpcNode } from "./type" + export const FEE_TOKEN_ADDRESS_ETH = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" @@ -18,7 +20,21 @@ export const BETTER_MULTICAL_ACCOUNT_CLASS_HASH = "0x057c2f22f0209a819e6c60f78ad7d3690f82ade9c0c68caea492151698934ede" export const ARGENT_5_MINUTE_ESCAPE_TESTING_ACCOUNT_CLASS_HASH = - "0x058a42e2553e65e301b7f22fb89e4a2576e9867c1e20bb1d32746c74ff823639" + "0x0545d680a2b4909f886371b2ac820745491f55ac7f0e81b3c4668a81e2a656f0" /** Cairo 1 */ +// "0x058a42e2553e65e301b7f22fb89e4a2576e9867c1e20bb1d32746c74ff823639" /** Cairo 0 - please keep for testing */ export const MULTICALL_CONTRACT_ADDRESS = "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4" + +// Public RPC nodes +export const BLAST_RPC_NODE: PublicRpcNode = { + mainnet: "https://starknet-mainnet.public.blastapi.io", + testnet: "https://starknet-testnet.public.blastapi.io", +} + +export const LAVA_RPC_NODE: PublicRpcNode = { + mainnet: "https://rpc.starknet.lava.build", + testnet: "https://rpc.starknet-testnet.lava.build", +} + +export const PUBLIC_RPC_NODES = [BLAST_RPC_NODE, LAVA_RPC_NODE] diff --git a/packages/extension/src/shared/network/defaults.ts b/packages/extension/src/shared/network/defaults.ts index 732b2b2a1..c231ec86c 100644 --- a/packages/extension/src/shared/network/defaults.ts +++ b/packages/extension/src/shared/network/defaults.ts @@ -56,6 +56,7 @@ export const defaultNetworks: Network[] = [ multicallAddress: MULTICALL_CONTRACT_ADDRESS, feeTokenAddress: FEE_TOKEN_ADDRESS_ETH, readonly: true, + prefer: "sequencer", }, { id: "goerli-alpha", @@ -77,6 +78,7 @@ export const defaultNetworks: Network[] = [ multicallAddress: MULTICALL_CONTRACT_ADDRESS, feeTokenAddress: FEE_TOKEN_ADDRESS_ETH, readonly: true, + prefer: "rpc", }, ...(process.env.NODE_ENV === "development" ? DEV_ONLY_NETWORKS : []), { diff --git a/packages/extension/src/shared/network/provider.ts b/packages/extension/src/shared/network/provider.ts index 864212013..412e5fec8 100644 --- a/packages/extension/src/shared/network/provider.ts +++ b/packages/extension/src/shared/network/provider.ts @@ -15,13 +15,20 @@ const getProviderForBaseUrl = memoize((baseUrl: string): SequencerProvider => { return new SequencerProvider({ baseUrl }) }) +export const shouldUseRpcProvider = (network: Network) => { + const hasRpcUrl = !!network.rpcUrl?.length + const hasSequencerUrl = !!network.sequencerUrl?.length + const preferRpc = network.prefer === "rpc" + return hasRpcUrl && (!hasSequencerUrl || preferRpc) +} + /** * Returns a RPC provider for the given RPC URL. * */ -export function getProviderForRpcUrl(rpcUrl: string): RpcProvider { +export const getProviderForRpcUrl = memoize((rpcUrl: string): RpcProvider => { return new RpcProvider({ nodeUrl: rpcUrl }) -} +}) /** * Returns a provider for the given network @@ -29,7 +36,7 @@ export function getProviderForRpcUrl(rpcUrl: string): RpcProvider { * @returns */ export function getProvider(network: Network): ProviderInterface { - if (network.rpcUrl && allowRpcProvider(network)) { + if (network.rpcUrl && shouldUseRpcProvider(network)) { return getProviderForRpcUrl(network.rpcUrl) } else if (network.sequencerUrl) { return getProviderForBaseUrl(network.sequencerUrl) @@ -44,9 +51,6 @@ export function getProvider(network: Network): ProviderInterface { } } -const allowRpcProvider = (network: Network) => - localStorage.getItem("betaFeatureRpcProvider") === "true" || !network.readonly - /** ======================================================================== */ const getProviderV4ForBaseUrl = memoize((baseUrl: string) => { @@ -76,7 +80,8 @@ export function getProviderv4__deprecated(network: Network) { if (network.sequencerUrl) { return getProviderV4ForBaseUrl__deprecated(network.sequencerUrl) } else { - throw new Error("RPC is not supported for v4 deprecated provider") + console.error("RPC is not supported for v4 deprecated provider") + return undefined } } diff --git a/packages/extension/src/shared/network/schema.ts b/packages/extension/src/shared/network/schema.ts index 22a1ecd0c..cabbfe7b8 100644 --- a/packages/extension/src/shared/network/schema.ts +++ b/packages/extension/src/shared/network/schema.ts @@ -1,4 +1,4 @@ -import { addressSchema } from "@argent/shared" +import { addressOrEmptyUndefinedSchema } from "@argent/shared" import { z } from "zod" const REGEX_HEXSTRING = /^0x[a-f0-9]+$/i @@ -24,15 +24,13 @@ export const networkSchema = baseNetworkSchema message: "chain id must be hexadecimal string, uppercase alphanumeric or underscore, like 'SN_GOERLI'", }), + prefer: z.enum(["sequencer", "rpc"]).default("sequencer").optional(), sequencerUrl: z .string() .url("Sequencer url must be a valid URL") .optional(), rpcUrl: z.string().url("RPC url must be a valid URL").optional(), - feeTokenAddress: addressSchema - .or(z.literal("")) - .transform((v) => (v === "" ? undefined : v)) - .optional(), + feeTokenAddress: addressOrEmptyUndefinedSchema, accountImplementation: z.optional( z.string().regex(REGEX_HEXSTRING, { @@ -88,11 +86,7 @@ export const networkSchema = baseNetworkSchema blockExplorerUrl: z.optional( z.string().url("block explorer url must be a valid URL"), ), - multicallAddress: z.optional( - z - .string() - .regex(REGEX_HEXSTRING, "multicall address must be a valid URL"), - ), + multicallAddress: addressOrEmptyUndefinedSchema, readonly: z.optional(z.boolean()), }) .refine( diff --git a/packages/extension/src/shared/network/store.ts b/packages/extension/src/shared/network/store.ts index 68c34a5d6..62650982d 100644 --- a/packages/extension/src/shared/network/store.ts +++ b/packages/extension/src/shared/network/store.ts @@ -15,20 +15,18 @@ export const networksEqual = (a: BaseNetwork, b: BaseNetwork) => a.id === b.id export const allNetworksStore = new ArrayStorage(defaultNetworks, { namespace: "core:allNetworks", compare: networksEqual, - serialize(value): Network[] { - // filter out the readonly networks - return value.filter( - (n) => !defaultReadonlyNetworks.some((rn) => rn.id === n.id), - ) - }, - deserialize(value): Network[] { - // add the readonly networks - return mergeArrayStableWith( - value, - defaultReadonlyNetworks, - networksEqual, - "unshift", - ) + deserialize(value: Network[]): Network[] { + // overwrite the stored values for the default networks with the default values + const mergedArray = mergeArrayStableWith(value, defaultReadonlyNetworks, { + compareFn: networksEqual, + insertMode: "unshift", + }) + + // except for the prefer property, which should be kept + return mergedArray.map((n) => ({ + ...n, + prefer: value.find((v) => v.id === n.id)?.prefer ?? n.prefer, + })) }, }) diff --git a/packages/extension/src/shared/network/type.ts b/packages/extension/src/shared/network/type.ts index d149c81a3..aed62724a 100644 --- a/packages/extension/src/shared/network/type.ts +++ b/packages/extension/src/shared/network/type.ts @@ -20,3 +20,8 @@ export type DefaultNetworkId = | "goerli-alpha" | "localhost" | "integration" + +export type PublicRpcNode = { + mainnet: string + testnet: string +} diff --git a/packages/extension/src/shared/network/utils.ts b/packages/extension/src/shared/network/utils.ts index a7f254acb..460ea97dd 100644 --- a/packages/extension/src/shared/network/utils.ts +++ b/packages/extension/src/shared/network/utils.ts @@ -3,6 +3,7 @@ import { constants } from "starknet" import { isEqualAddress } from "../../ui/services/addresses" import { ArgentAccountType } from "../wallet.model" import { DefaultNetworkId, Network } from "./type" +import { PUBLIC_RPC_NODES } from "./constants" // LEGACY ⬇️ export function mapImplementationToArgentAccountType( @@ -109,3 +110,15 @@ export const getNetworkUrl = (network: Network) => { export function isArgentNetwork(network: Network) { return network.id === "mainnet-alpha" || network.id === "goerli-alpha" } + +export function getRandomPublicRPCNode(network: Network) { + const randomIndex = Math.floor(Math.random() * PUBLIC_RPC_NODES.length) + + const randomNode = PUBLIC_RPC_NODES[randomIndex] + + if (!randomNode) { + throw new Error(`No random node found for network: ${network.id}`) + } + + return randomNode +} diff --git a/packages/extension/src/ui/services/nfts/interface.ts b/packages/extension/src/shared/nft/interface.ts similarity index 79% rename from packages/extension/src/ui/services/nfts/interface.ts rename to packages/extension/src/shared/nft/interface.ts index 81683d43d..467179e30 100644 --- a/packages/extension/src/ui/services/nfts/interface.ts +++ b/packages/extension/src/shared/nft/interface.ts @@ -1,9 +1,10 @@ import { Address, Collection, NftItem } from "@argent/shared" - -import { ContractAddress } from "../../../shared/storage/__new/repositories/nft" -import { AllowArray } from "../../../shared/storage/types" +import { ContractAddress } from "../storage/__new/repositories/nft" +import { AllowArray } from "../storage/types" +import { Network } from "../network" export interface INFTService { + isSupported: (network: Network) => boolean getAsset: ( chain: string, networkId: string, @@ -36,5 +37,6 @@ export interface INFTService { tokenId: string, recipient: string, schema: string, + network: Network, ) => Promise } diff --git a/packages/extension/src/shared/nft/worker/index.ts b/packages/extension/src/shared/nft/worker/index.ts deleted file mode 100644 index 971f5f2d0..000000000 --- a/packages/extension/src/shared/nft/worker/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backgroundUIService } from "../../../background/__new/services/ui" -import { transactionsStore } from "../../../background/transactions/store" -import { - sessionService, - walletSingleton, -} from "../../../background/walletSingleton" -import { old_walletStore } from "../../../shared/wallet/walletStore" -import { nftService } from "../../../ui/services/nfts" -import { chromeScheduleService } from "../../schedule" -import { NftsWorker } from "./implementation" - -export const nftsWorker = new NftsWorker( - nftService, - chromeScheduleService, - walletSingleton, - old_walletStore, - transactionsStore, - sessionService, - backgroundUIService, -) diff --git a/packages/extension/src/shared/nft/worker/interface.ts b/packages/extension/src/shared/nft/worker/interface.ts new file mode 100644 index 000000000..8d40be334 --- /dev/null +++ b/packages/extension/src/shared/nft/worker/interface.ts @@ -0,0 +1,7 @@ +export type INFTWorkerStore = Record< + string, + { + isUpdating: boolean + lastUpdatedTimestamp: number + } +> diff --git a/packages/extension/src/shared/nft/worker/store.ts b/packages/extension/src/shared/nft/worker/store.ts new file mode 100644 index 000000000..6c62a228b --- /dev/null +++ b/packages/extension/src/shared/nft/worker/store.ts @@ -0,0 +1,9 @@ +import { KeyValueStorage } from "../../storage" +import { INFTWorkerStore } from "./interface" + +export const nftWorkerStore = new KeyValueStorage( + {}, + { + namespace: "core:nft:worker", + }, +) diff --git a/packages/extension/src/ui/services/recovery/interface.ts b/packages/extension/src/shared/recovery/service/interface.ts similarity index 100% rename from packages/extension/src/ui/services/recovery/interface.ts rename to packages/extension/src/shared/recovery/service/interface.ts diff --git a/packages/extension/src/shared/recovery/storage.ts b/packages/extension/src/shared/recovery/storage.ts new file mode 100644 index 000000000..f2ef60f56 --- /dev/null +++ b/packages/extension/src/shared/recovery/storage.ts @@ -0,0 +1,14 @@ +import { KeyValueStorage } from "../storage" +import { adaptKeyValue } from "../storage/__new/keyvalue" +import { IRecoveryStorage } from "./types" + +const keyValueStorage = new KeyValueStorage( + { + isRecovering: false, + }, + { + namespace: "service:recovery", + }, +) + +export const recoveryStore = adaptKeyValue(keyValueStorage) diff --git a/packages/extension/src/shared/recovery/types.ts b/packages/extension/src/shared/recovery/types.ts new file mode 100644 index 000000000..852d09e59 --- /dev/null +++ b/packages/extension/src/shared/recovery/types.ts @@ -0,0 +1,3 @@ +export interface IRecoveryStorage { + isRecovering: boolean +} diff --git a/packages/extension/src/shared/schedule/chromeService.test.ts b/packages/extension/src/shared/schedule/chromeService.test.ts index 276eebc7e..0bdd8a4e3 100644 --- a/packages/extension/src/shared/schedule/chromeService.test.ts +++ b/packages/extension/src/shared/schedule/chromeService.test.ts @@ -3,10 +3,10 @@ import { beforeEach, describe, test, vi } from "vitest" import { ChromeScheduleService } from "./chromeService" import { BaseScheduledTask, ImplementedScheduledTask } from "./interface" -describe("ChromeScheduleService", () => { - let waitFn = vi.fn() - let service: ChromeScheduleService - let mockBrowser = { +function getMockBrowser() { + const onStartUpListeners: Array<(...args: unknown[]) => void> = [] + const onInstalledListeners: Array<(...args: unknown[]) => void> = [] + return { alarms: { create: vi.fn(), getAll: vi.fn(), @@ -15,20 +15,33 @@ describe("ChromeScheduleService", () => { addListener: vi.fn(), }, }, + runtime: { + onStartup: { + addListener: vi.fn((callback: () => void) => + onStartUpListeners.push(callback), + ), + }, + onInstalled: { + addListener: vi.fn((callback: (details: any) => void) => + onInstalledListeners.push(callback), + ), + }, + }, + _methods: { + fireOnStartup: () => onStartUpListeners.forEach((cb) => cb()), + fireOnInstalled: () => onInstalledListeners.forEach((cb) => cb()), + }, } +} + +describe("ChromeScheduleService", () => { + let waitFn = vi.fn() + let service: ChromeScheduleService + let mockBrowser = getMockBrowser() beforeEach(() => { waitFn = vi.fn() - mockBrowser = { - alarms: { - create: vi.fn(), - getAll: vi.fn(), - clear: vi.fn(), - onAlarm: { - addListener: vi.fn(), - }, - }, - } + mockBrowser = getMockBrowser() service = new ChromeScheduleService(mockBrowser, waitFn) }) @@ -117,4 +130,27 @@ describe("ChromeScheduleService", () => { expect(waitFn).toBeCalledTimes(4) expect(task.callback).toBeCalledTimes(5) }) + + // add tests for service.onStartup and service.onInstallAndUpgrade + test("onStartup runs callback", async () => { + const task: ImplementedScheduledTask = { + id: "test", + callback: vi.fn(), + } + await service.onStartup(task) + expect(mockBrowser.runtime.onStartup.addListener).toBeCalled() + mockBrowser._methods.fireOnStartup() + expect(task.callback).toBeCalled() + }) + + test("onInstallAndUpgrade runs callback", async () => { + const task: ImplementedScheduledTask = { + id: "test", + callback: vi.fn(), + } + await service.onInstallAndUpgrade(task) + expect(mockBrowser.runtime.onInstalled.addListener).toBeCalled() + mockBrowser._methods.fireOnInstalled() + expect(task.callback).toBeCalled() + }) }) diff --git a/packages/extension/src/shared/schedule/chromeService.ts b/packages/extension/src/shared/schedule/chromeService.ts index a5a8a1ce9..df541667f 100644 --- a/packages/extension/src/shared/schedule/chromeService.ts +++ b/packages/extension/src/shared/schedule/chromeService.ts @@ -11,6 +11,8 @@ export type MinimalBrowser = DeepPick< | "alarms.getAll" | "alarms.clear" | "alarms.onAlarm.addListener" + | "runtime.onStartup.addListener" + | "runtime.onInstalled.addListener" > type WaitFn = (ms: number) => Promise @@ -57,14 +59,14 @@ export class ChromeScheduleService implements IScheduleService { ? Math.max(Math.floor(60 / seconds - 1), 1) : 1 const periodInMinutes = Math.max(Math.round(seconds / 60), 1) - this.browser.alarms.create(`${task.id}::run${runXTimesPerMinute}`, { + await this.browser.alarms.create(`${task.id}::run${runXTimesPerMinute}`, { periodInMinutes, }) } async in(seconds: number, task: BaseScheduledTask): Promise { const delayInMinutes = Math.max(Math.round(seconds / 60), 1) - this.browser.alarms.create(`${task.id}::run1`, { + await this.browser.alarms.create(`${task.id}::run1`, { delayInMinutes, }) } @@ -88,4 +90,16 @@ export class ChromeScheduleService implements IScheduleService { } }) } + + async onStartup(task: ImplementedScheduledTask): Promise { + this.browser.runtime.onStartup.addListener(() => { + void task.callback() + }) + } + + async onInstallAndUpgrade(task: ImplementedScheduledTask): Promise { + this.browser.runtime.onInstalled.addListener(() => { + void task.callback() + }) + } } diff --git a/packages/extension/src/shared/schedule/interface.ts b/packages/extension/src/shared/schedule/interface.ts index de84aaf09..d63edace4 100644 --- a/packages/extension/src/shared/schedule/interface.ts +++ b/packages/extension/src/shared/schedule/interface.ts @@ -16,4 +16,7 @@ export interface IScheduleService { delete(task: BaseScheduledTask): Promise registerImplementation(task: ImplementedScheduledTask): Promise + + onStartup(task: ImplementedScheduledTask): Promise + onInstallAndUpgrade(task: ImplementedScheduledTask): Promise } diff --git a/packages/extension/src/shared/schedule/mock.ts b/packages/extension/src/shared/schedule/mock.ts new file mode 100644 index 000000000..c103b7029 --- /dev/null +++ b/packages/extension/src/shared/schedule/mock.ts @@ -0,0 +1,13 @@ +import { IScheduleService } from "./interface" + +export const createScheduleServiceMock = (): IScheduleService => { + const service: IScheduleService = { + delete: vi.fn(() => Promise.resolve()), + in: vi.fn(() => Promise.resolve()), + every: vi.fn(() => Promise.resolve()), + onInstallAndUpgrade: vi.fn(() => Promise.resolve()), + onStartup: vi.fn(() => Promise.resolve()), + registerImplementation: vi.fn(() => Promise.resolve()), + } + return service +} diff --git a/packages/extension/src/shared/settings/store.ts b/packages/extension/src/shared/settings/store.ts index daf342212..d4bae119b 100644 --- a/packages/extension/src/shared/settings/store.ts +++ b/packages/extension/src/shared/settings/store.ts @@ -10,8 +10,6 @@ export const settingsStore = new KeyValueStorage( privacyAutomaticErrorReporting: false, experimentalAllowChooseAccount: false, blockExplorerKey: defaultBlockExplorerKey, - betaFeatureMultisig: false, - betaFeatureRpcProvider: false, }, "core:settings", ) diff --git a/packages/extension/src/shared/settings/types.ts b/packages/extension/src/shared/settings/types.ts index 452046b1c..a53b6df1f 100644 --- a/packages/extension/src/shared/settings/types.ts +++ b/packages/extension/src/shared/settings/types.ts @@ -7,8 +7,6 @@ export interface ISettingsStorage { privacyAutomaticErrorReporting: boolean experimentalAllowChooseAccount: boolean blockExplorerKey: BlockExplorerKey - betaFeatureMultisig: boolean - betaFeatureRpcProvider: boolean } export type SettingsStorageKey = keyof ISettingsStorage diff --git a/packages/extension/src/shared/shield/GuardianSignerArgentX.ts b/packages/extension/src/shared/shield/GuardianSignerArgentX.ts index 4e231ae72..9eccc8427 100644 --- a/packages/extension/src/shared/shield/GuardianSignerArgentX.ts +++ b/packages/extension/src/shared/shield/GuardianSignerArgentX.ts @@ -1,8 +1,8 @@ import { CosignerOffchainMessage, GuardianSigner } from "@argent/guardian" import type { CosignerMessage } from "@argent/guardian" import { Signature, hash, num } from "starknet" +import { isEqualAddress } from "@argent/shared" -import { isEqualAddress } from "../../ui/services/addresses" import { isTokenExpired } from "./backend/account" /** @@ -11,6 +11,8 @@ import { isTokenExpired } from "./backend/account" export const guardianSignerNotRequired = [ "escapeGuardian", "triggerEscapeGuardian", + "escape_guardian", + "trigger_escape_guardian", ] export const guardianSignerNotRequiredSelectors = guardianSignerNotRequired.map( diff --git a/packages/extension/src/shared/storage/__test__/mergeArrayStableWith.test.ts b/packages/extension/src/shared/storage/__new/__test__/mergeStableArrayWith.test.ts similarity index 62% rename from packages/extension/src/shared/storage/__test__/mergeArrayStableWith.test.ts rename to packages/extension/src/shared/storage/__new/__test__/mergeStableArrayWith.test.ts index 9ceadb31e..287ab3d47 100644 --- a/packages/extension/src/shared/storage/__test__/mergeArrayStableWith.test.ts +++ b/packages/extension/src/shared/storage/__new/__test__/mergeStableArrayWith.test.ts @@ -1,38 +1,38 @@ -import { mergeArrayStableWith } from "../array" +import { mergeArrayStableWith } from "../base" describe("mergeArrayStableWith", () => { describe("primitive values", () => { - test("should merge 2 arrays", async () => { + test("should merge 2 arrays", () => { const arr1 = [1, 2, 3] const arr2 = [4, 5, 6] const result = mergeArrayStableWith(arr1, arr2) expect(result).toEqual([1, 2, 3, 4, 5, 6]) }) - test("should merge 2 arrays with duplicates in each", async () => { + test("should merge 2 arrays with duplicates in each", () => { const arr1 = [1, 2, 3, 3] const arr2 = [4, 5, 6, 6] const result = mergeArrayStableWith(arr1, arr2) expect(result).toEqual([1, 2, 3, 4, 5, 6]) }) - test("should merge 2 arrays with duplicates", async () => { + test("should merge 2 arrays with duplicates", () => { const arr1 = [1, 2, 3] const arr2 = [3, 4, 5, 6, 6] const result = mergeArrayStableWith(arr1, arr2) expect(result).toEqual([1, 2, 3, 4, 5, 6]) }) - test("should merge array with empty array", async () => { + test("should merge array with empty array", () => { const arr1 = [1, 2, 3] const arr2: typeof arr1 = [] const result = mergeArrayStableWith(arr1, arr2) expect(result).toEqual([1, 2, 3]) }) - test("should merge empty array with array", async () => { + test("should merge empty array with array", () => { const arr1 = [1, 2, 3] const arr2: typeof arr1 = [] const result = mergeArrayStableWith(arr2, arr1) expect(result).toEqual([1, 2, 3]) }) - test("should merge empty array with empty array", async () => { + test("should merge empty array with empty array", () => { const arr1: unknown[] = [] const arr2: typeof arr1 = [] const result = mergeArrayStableWith(arr1, arr2) @@ -40,7 +40,7 @@ describe("mergeArrayStableWith", () => { }) }) describe("objects", () => { - test("should merge 2 arrays", async () => { + test("should merge 2 arrays", () => { const arr1: object[] = [{ a: 1 }, { b: 2 }, { c: 3 }] const arr2: typeof arr1 = [{ d: 4 }, { e: 5 }, { f: 6 }] const result = mergeArrayStableWith(arr1, arr2) @@ -53,7 +53,7 @@ describe("mergeArrayStableWith", () => { { f: 6 }, ]) }) - test("should merge 2 arrays with duplicates in each", async () => { + test("should merge 2 arrays with duplicates in each", () => { const arr1: object[] = [{ a: 1 }, { b: 2 }, { c: 3 }, { c: 3 }] const arr2: typeof arr1 = [{ d: 4 }, { e: 5 }, { f: 6 }, { f: 6 }] const result = mergeArrayStableWith(arr1, arr2) @@ -66,7 +66,7 @@ describe("mergeArrayStableWith", () => { { f: 6 }, ]) }) - test("should merge 2 arrays with duplicates", async () => { + test("should merge 2 arrays with duplicates", () => { const arr1: object[] = [{ a: 1 }, { b: 2 }, { c: 3 }] const arr2: typeof arr1 = [ { c: 3 }, @@ -85,13 +85,13 @@ describe("mergeArrayStableWith", () => { { f: 6 }, ]) }) - test("should merge array with empty array", async () => { + test("should merge array with empty array", () => { const arr1: object[] = [{ a: 1 }, { b: 2 }, { c: 3 }] const arr2: typeof arr1 = [] const result = mergeArrayStableWith(arr1, arr2) expect(result).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]) }) - test("should merge empty array with array", async () => { + test("should merge empty array with array", () => { const arr1: object[] = [{ a: 1 }, { b: 2 }, { c: 3 }] const arr2: typeof arr1 = [] const result = mergeArrayStableWith(arr2, arr1) @@ -107,7 +107,7 @@ describe("mergeArrayStableWith", () => { return a.id === b.id } - test("should merge 2 arrays", async () => { + test("should merge 2 arrays", () => { const arr1: AdvancedObject[] = [ { id: 1, value: "a" }, { id: 2, value: "b" }, @@ -118,7 +118,7 @@ describe("mergeArrayStableWith", () => { { id: 5, value: "e" }, { id: 6, value: "f" }, ] - const result = mergeArrayStableWith(arr1, arr2, compareFn) + const result = mergeArrayStableWith(arr1, arr2, { compareFn }) expect(result).toEqual([ { id: 1, value: "a" }, { id: 2, value: "b" }, @@ -128,7 +128,7 @@ describe("mergeArrayStableWith", () => { { id: 6, value: "f" }, ]) }) - test("should merge 2 arrays with duplicates in each", async () => { + test("should merge 2 arrays with duplicates in each", () => { const arr1: AdvancedObject[] = [ { id: 1, value: "a" }, { id: 2, value: "b" }, @@ -141,7 +141,7 @@ describe("mergeArrayStableWith", () => { { id: 6, value: "f" }, { id: 6, value: "f1" }, ] - const result = mergeArrayStableWith(arr1, arr2, compareFn) + const result = mergeArrayStableWith(arr1, arr2, { compareFn }) expect(result).toEqual([ // last occurence of each element is kept { id: 1, value: "a" }, @@ -152,7 +152,7 @@ describe("mergeArrayStableWith", () => { { id: 6, value: "f1" }, ]) }) - test("should merge 2 arrays with duplicates", async () => { + test("should merge 2 arrays with duplicates", () => { const arr1: AdvancedObject[] = [ { id: 1, value: "a" }, { id: 2, value: "b" }, @@ -165,7 +165,7 @@ describe("mergeArrayStableWith", () => { { id: 6, value: "f" }, { id: 6, value: "f1" }, ] - const result = mergeArrayStableWith(arr1, arr2, compareFn) + const result = mergeArrayStableWith(arr1, arr2, { compareFn }) expect(result).toEqual([ // last occurence of each element is kept { id: 1, value: "a1" }, @@ -177,4 +177,62 @@ describe("mergeArrayStableWith", () => { ]) }) }) + describe("objects with custom compare function and merge function", () => { + interface AdvancedObject { + id: number + value: string + } + function compareFn(a: AdvancedObject, b: AdvancedObject): boolean { + return a.id === b.id + } + function mergeFn(a: AdvancedObject, b: AdvancedObject): AdvancedObject { + return { id: a.id, value: `${a.value}${b.value}` } + } + + test("should merge 2 arrays", () => { + const arr1: AdvancedObject[] = [ + { id: 1, value: "a" }, + { id: 2, value: "b" }, + { id: 3, value: "c" }, + ] + const arr2: AdvancedObject[] = [ + { id: 4, value: "d" }, + { id: 5, value: "e" }, + { id: 6, value: "f" }, + ] + const result = mergeArrayStableWith(arr1, arr2, { compareFn, mergeFn }) + expect(result).toEqual([ + { id: 1, value: "a" }, + { id: 2, value: "b" }, + { id: 3, value: "c" }, + { id: 4, value: "d" }, + { id: 5, value: "e" }, + { id: 6, value: "f" }, + ]) + }) + test("should merge 2 arrays with duplicates in each", () => { + const arr1: AdvancedObject[] = [ + { id: 1, value: "a" }, + { id: 2, value: "b" }, + { id: 3, value: "c" }, + { id: 3, value: "c1" }, + ] + const arr2: AdvancedObject[] = [ + { id: 4, value: "d" }, + { id: 5, value: "e" }, + { id: 6, value: "f" }, + { id: 6, value: "f1" }, + ] + const result = mergeArrayStableWith(arr1, arr2, { compareFn, mergeFn }) + expect(result).toEqual([ + // last occurence of each element is kept + { id: 1, value: "a" }, + { id: 2, value: "b" }, + { id: 3, value: "cc1" }, + { id: 4, value: "d" }, + { id: 5, value: "e" }, + { id: 6, value: "ff1" }, + ]) + }) + }) }) diff --git a/packages/extension/src/shared/storage/__new/base.ts b/packages/extension/src/shared/storage/__new/base.ts index b12d9575f..daa47b744 100644 --- a/packages/extension/src/shared/storage/__new/base.ts +++ b/packages/extension/src/shared/storage/__new/base.ts @@ -1,30 +1,53 @@ +import { isEqual } from "lodash-es" import type { IRepositoryOptions } from "./interface" -export function uniqWithRight( - array: T[], - compareFn: (a: T, b: T) => boolean, -): T[] { - return array.reduceRight((result: T[], element: T) => { +interface OptionFunctions { + compareFn?: (a: T, b: T) => boolean + mergeFn?: (a: T, b: T) => T +} + +function getDefaultOptionFunctions( + options: OptionFunctions, +): Required> { + return { + compareFn: isEqual, + mergeFn: (_: T, b: T) => b, + ...options, + } +} + +function uniqWithMerge(array: T[], options: OptionFunctions = {}): T[] { + const { compareFn, mergeFn } = getDefaultOptionFunctions(options) + + return array.reduce((result: T[], element: T) => { if (!result.some((e) => compareFn(e, element))) { - return [element, ...result] + return [...result, element] } - return result + return result.map((e) => (compareFn(e, element) ? mergeFn(e, element) : e)) }, []) } export function mergeArrayStableWith( source: T[], other: T[], - compareFn: (a: T, b: T) => boolean, - insertMode: "unshift" | "push" = "push", + options: OptionFunctions & { + insertMode?: "unshift" | "push" + } = {}, ): T[] { - const result = uniqWithRight(source, compareFn) - return other.reduceRight((acc: T[], element: T) => { + const { compareFn, mergeFn } = getDefaultOptionFunctions(options) + const insertMode = options.insertMode ?? "push" + + const result = uniqWithMerge(source, options) + return other.reduce((acc: T[], element: T) => { const index = acc.findIndex((e) => compareFn(e, element)) if (index === -1) { - return insertMode === "push" ? [element, ...acc] : [...acc, element] + return insertMode === "unshift" ? [element, ...acc] : [...acc, element] } else { - return [...acc.slice(0, index), element, ...acc.slice(index + 1)] + return [ + ...acc.slice(0, index), + mergeFn(acc[index], element), + ...acc.slice(index + 1), + ] } }, result) } @@ -34,11 +57,14 @@ export function optionsWithDefaults>( ): Required> & O { const passThrough = (value: T) => value const compare = (a: T, b: T) => a === b + const merge = (_: T, b: T) => b + return { defaults: [], serialize: passThrough, deserialize: passThrough, compare, + merge, ...options, } } diff --git a/packages/extension/src/shared/storage/__new/chrome.ts b/packages/extension/src/shared/storage/__new/chrome.ts index 3b65159fe..93b4f234d 100644 --- a/packages/extension/src/shared/storage/__new/chrome.ts +++ b/packages/extension/src/shared/storage/__new/chrome.ts @@ -94,11 +94,10 @@ export class ChromeRepository implements IRepository { newValues = [value] } - const mergedValues = mergeArrayStableWith( - items, - newValues, - this.options.compare, - ) + const mergedValues = mergeArrayStableWith(items, newValues, { + compareFn: this.options.compare.bind(this), + mergeFn: this.options.merge.bind(this), + }) await this.set(mergedValues) diff --git a/packages/extension/src/shared/storage/__new/interface.ts b/packages/extension/src/shared/storage/__new/interface.ts index f9f02805c..906dbbc15 100644 --- a/packages/extension/src/shared/storage/__new/interface.ts +++ b/packages/extension/src/shared/storage/__new/interface.ts @@ -42,6 +42,8 @@ export interface IRepositoryOptions { deserialize?: (value: any) => AllowPromise /** Optional. A function that compares two values of type T and returns a boolean. */ compare?: (a: T, b: T) => boolean + /** Optional. A function that merges two values of type T. */ + merge?: (oldValue: T, newValue: T) => T } export type UpsertResult = { created: number; updated: number } diff --git a/packages/extension/src/shared/storage/array.ts b/packages/extension/src/shared/storage/array.ts index d25bbaa1c..e874b0061 100644 --- a/packages/extension/src/shared/storage/array.ts +++ b/packages/extension/src/shared/storage/array.ts @@ -1,10 +1,4 @@ -import { - differenceWith, - isEqual, - isFunction, - reverse, - uniqWith, -} from "lodash-es" +import { differenceWith, isEqual, isFunction } from "lodash-es" import { ObjectStorage, ObjectStorageOptions } from "./object" import { StorageOptionsOrNameSpace, getOptionsWithDefaults } from "./options" @@ -17,24 +11,7 @@ import { SetterFn, StorageChange, } from "./types" - -export function mergeArrayStableWith( - source: T[], - other: T[], - compareFn: (a: T, b: T) => boolean = isEqual, - insertMode: "unshift" | "push" = "push", -): T[] { - const result = reverse(uniqWith(reverse(source), compareFn)) // 2x reverse to keep the order while keeping the last occurence of duplicates - for (const element of other) { - const index = result.findIndex((e) => compareFn(e, element)) - if (index === -1) { - result[insertMode](element) - } else { - result[index] = element - } - } - return result -} +import { mergeArrayStableWith } from "./__new/base" interface ArrayStorageOptions extends ObjectStorageOptions { compare?: (a: T, b: T) => boolean @@ -95,14 +72,20 @@ export class ArrayStorage implements IArrayStorage { public async push(value: AllowArray | SetterFn): Promise { const all = await this.get() const newValue = this.setterOrArrayToValue(value, all) - const newAll = mergeArrayStableWith(all, newValue, this.compare) + const newAll = mergeArrayStableWith(all, newValue, { + compareFn: this.compare.bind(this), + insertMode: "push", + }) await this.storageImplementation.set(newAll) } public async unshift(value: AllowArray | SetterFn): Promise { const all = await this.get() const newValue = this.setterOrArrayToValue(value, all) - const newAll = mergeArrayStableWith(all, newValue, this.compare, "unshift") + const newAll = mergeArrayStableWith(all, newValue, { + compareFn: this.compare.bind(this), + insertMode: "unshift", + }) await this.storageImplementation.set(newAll) } diff --git a/packages/extension/src/shared/storage/hooks.ts b/packages/extension/src/shared/storage/hooks.ts index a60b5f792..5ccf56878 100644 --- a/packages/extension/src/shared/storage/hooks.ts +++ b/packages/extension/src/shared/storage/hooks.ts @@ -95,26 +95,3 @@ export function useArrayStorage( return filteredValue } - -export function useLocalStorageState( - key: string, - initialValue: T, -): [T, (value: T) => void] { - const [state, setState] = useState(() => { - try { - const item = localStorage.getItem(key) - if (!item) { - return initialValue - } - return JSON.parse(item) - } catch { - return initialValue - } - }) - - const setValue = (value: T) => { - setState(value) - localStorage.setItem(key, JSON.stringify(value)) - } - return [state, setValue] -} diff --git a/packages/extension/src/shared/token/__new/repository/token.ts b/packages/extension/src/shared/token/__new/repository/token.ts index 7529918ba..7a5bc49ca 100644 --- a/packages/extension/src/shared/token/__new/repository/token.ts +++ b/packages/extension/src/shared/token/__new/repository/token.ts @@ -12,6 +12,10 @@ export const tokenRepo: ITokenRepository = new ChromeRepository( areaName: "local", namespace: "core:tokens", compare: (a: BaseToken, b: BaseToken) => equalToken(a, b), + merge: (oldValue: Token, newValue: Token) => ({ + ...oldValue, + ...newValue, + }), defaults: parsedDefaultTokens, }, ) diff --git a/packages/extension/src/shared/token/__new/service/implementation.test.ts b/packages/extension/src/shared/token/__new/service/implementation.test.ts new file mode 100644 index 000000000..3db2e6d97 --- /dev/null +++ b/packages/extension/src/shared/token/__new/service/implementation.test.ts @@ -0,0 +1,725 @@ +import { Mocked } from "vitest" +import { NetworkService } from "../../../network/service/implementation" +import { INetworkService } from "../../../network/service/interface" +import { MockFnRepository } from "../../../storage/__new/__test__/mockFunctionImplementation" +import { ITokenRepository } from "../repository/token" +import { ITokenBalanceRepository } from "../repository/tokenBalance" +import { ITokenPriceRepository } from "../repository/tokenPrice" +import { TokenService } from "./implementation" +import { + getMockApiTokenDetails, + getMockBaseToken, + getMockToken, + getMockTokenPriceDetails, + getMockTokenWithBalance, +} from "../../../../../test/token.mock" +import { defaultNetwork } from "../../../network" +import { + getMockNetwork, + getMockNetworkWithoutMulticall, +} from "../../../../../test/network.mock" +import { INetworkRepo } from "../../../network/store" +import { rest } from "msw" +import { setupServer } from "msw/node" +import { GatewayError, shortString, stark } from "starknet" +import { addressSchema } from "@argent/shared" +import { TokenError } from "../../../errors/token" + +const BASE_INFO_ENDPOINT = "https://token.info.argent47.net/v1" +const BASE_INFO_ENDPOINT_INVALID = "https://token.info.argent47.net/v2" +const BASE_PRICES_ENDPOINT = "https://token.prices.argent47.net/v1" + +// const BASE_URL_WITH_WILDCARD = BASE_URL_ENDPOINT + "*" + +const randomAddress1 = addressSchema.parse(stark.randomAddress()) +const randomAddress2 = addressSchema.parse(stark.randomAddress()) + +/** + * @vitest-environment jsdom + */ +const server = setupServer( + rest.get(BASE_INFO_ENDPOINT, (req, res, ctx) => { + return res( + ctx.json({ + tokens: [ + getMockApiTokenDetails({ id: 1, address: randomAddress1 }), + getMockApiTokenDetails({ + id: 2, + address: randomAddress2, + pricingId: 2, + }), + ], + }), + ) + }), + rest.get(BASE_INFO_ENDPOINT_INVALID, (req, res, ctx) => { + return res( + ctx.json([ + getMockApiTokenDetails({ address: randomAddress1 }), + getMockApiTokenDetails({ address: randomAddress2, pricingId: 2 }), + ]), + ) + }), + rest.get(BASE_PRICES_ENDPOINT, (req, res, ctx) => { + return res( + ctx.json({ + prices: [ + getMockTokenPriceDetails({ pricingId: 1, ethValue: "0.32" }), + getMockTokenPriceDetails({ pricingId: 2, ethValue: "0.64" }), + ], + }), + ) + }), +) + +describe("TokenService", () => { + let tokenService: TokenService + let mockNetworkService: Mocked + let mockNetworkRepo: MockFnRepository + let mockTokenRepo: MockFnRepository + let mockTokenBalanceRepo: MockFnRepository + let mockTokenPriceRepo: MockFnRepository + beforeAll(() => { + server.listen() + }) + beforeEach(() => { + mockTokenRepo = new MockFnRepository() + mockTokenBalanceRepo = new MockFnRepository() + mockTokenPriceRepo = new MockFnRepository() + mockNetworkRepo = new MockFnRepository() + + mockNetworkService = vi.mocked( + new NetworkService(mockNetworkRepo), + ) + + tokenService = new TokenService( + mockNetworkService, + mockTokenRepo, + mockTokenBalanceRepo, + mockTokenPriceRepo, + BASE_INFO_ENDPOINT, + BASE_PRICES_ENDPOINT, + ) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it("should add a token", async () => { + const mockToken = getMockToken() + await tokenService.addToken(mockToken) + expect(mockTokenRepo.upsert).toHaveBeenCalledWith(mockToken) + }) + + it("should remove a token", async () => { + const mockBaseToken = getMockBaseToken() + const mockToken = getMockToken() + mockTokenRepo.get.mockResolvedValueOnce([mockToken]) + await tokenService.removeToken(mockBaseToken) + expect(mockTokenRepo.remove).toHaveBeenCalledWith(mockToken) + }) + + it("should update tokens", async () => { + const mockTokens = [ + getMockToken(), + getMockToken({ address: "0x456" }), + getMockToken({ networkId: "goerli-alpha" }), + ] + await tokenService.updateTokens(mockTokens) + expect(mockTokenRepo.upsert).toHaveBeenCalledWith(mockTokens) + }) + + it("should update token balances", async () => { + const mockTokensWithBalance = [ + getMockTokenWithBalance(), + getMockTokenWithBalance({ address: "0x456", balance: "200" }), + ] + await tokenService.updateTokenBalances(mockTokensWithBalance) + expect(mockTokenBalanceRepo.upsert).toHaveBeenCalledWith( + mockTokensWithBalance, + ) + }) + + it("should update token prices", async () => { + const mockTokenPrices = [ + getMockTokenPriceDetails(), + getMockTokenPriceDetails({ pricingId: 2, ethValue: "0.36" }), + ] + await tokenService.updateTokenPrices(mockTokenPrices) + expect(mockTokenPriceRepo.upsert).toHaveBeenCalledWith(mockTokenPrices) + }) + + describe("fetch tokens from backend", () => { + it("should fetch tokens from backend", async () => { + const mockNetworkId = defaultNetwork.id + const defaultMockApiTokeDetails = getMockApiTokenDetails() + const mockToken1 = getMockToken({ + id: 1, + address: randomAddress1, + networkId: mockNetworkId, + iconUrl: defaultMockApiTokeDetails.iconUrl, + showAlways: undefined, + custom: undefined, + popular: defaultMockApiTokeDetails.popular, + pricingId: defaultMockApiTokeDetails.pricingId, + }) + const mockToken2 = getMockToken({ + id: 2, + address: randomAddress2, + networkId: mockNetworkId, + showAlways: undefined, + iconUrl: defaultMockApiTokeDetails.iconUrl, + custom: undefined, + popular: defaultMockApiTokeDetails.popular, + pricingId: 2, + }) + const mockTokens = [mockToken1, mockToken2] + mockTokenRepo.get.mockResolvedValueOnce(mockTokens) + const result = await tokenService.fetchTokensFromBackend(mockNetworkId) + expect(result).toEqual(mockTokens) + }) + + it("should return without fetching tokens if it is not a default network", async () => { + const mockNetworkId = "mockNetworkId" + const mockTokens = [getMockToken({ networkId: mockNetworkId })] + mockTokenRepo.get.mockResolvedValueOnce(mockTokens) + const result = await tokenService.fetchTokensFromBackend(mockNetworkId) + expect(result).toEqual(mockTokens) + }) + + it("should throw parsing error if token info response is not valid", async () => { + const invalidTokenService = new TokenService( + mockNetworkService, + mockTokenRepo, + mockTokenBalanceRepo, + mockTokenPriceRepo, + BASE_INFO_ENDPOINT_INVALID, + BASE_PRICES_ENDPOINT, + ) + + const mockNetworkId = defaultNetwork.id + const defaultMockApiTokeDetails = getMockApiTokenDetails() + const mockToken1 = getMockToken({ + address: randomAddress1, + networkId: mockNetworkId, + iconUrl: defaultMockApiTokeDetails.iconUrl, + showAlways: undefined, + custom: undefined, + popular: defaultMockApiTokeDetails.popular, + pricingId: defaultMockApiTokeDetails.pricingId, + }) + const mockToken2 = getMockToken({ + address: randomAddress2, + networkId: mockNetworkId, + showAlways: undefined, + iconUrl: defaultMockApiTokeDetails.iconUrl, + custom: undefined, + popular: defaultMockApiTokeDetails.popular, + pricingId: 2, + }) + const mockTokens = [mockToken1, mockToken2] + mockTokenRepo.get.mockResolvedValueOnce(mockTokens) + await expect( + invalidTokenService.fetchTokensFromBackend(mockNetworkId), + ).rejects.toThrowError(new TokenError({ code: "TOKEN_PARSING_ERROR" })) + }) + }) + + describe("fetch onchain token balances", () => { + it("should fetch token balances from on-chain for same network", async () => { + const mockNetwork = getMockNetwork() + const mockAccount = { address: randomAddress1, networkId: mockNetwork.id } + + const mockBaseTokens = [ + getMockBaseToken({ networkId: mockNetwork.id }), + getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }), + ] + + const mockTokens = [ + getMockToken({ ...mockBaseTokens[0] }), + getMockToken({ ...mockBaseTokens[1] }), + ] + const mockTokenBalances = mockBaseTokens.map((token) => ({ + ...token, + balance: "100", + account: mockAccount, + })) + mockTokenRepo.get.mockResolvedValueOnce(mockTokens) + mockNetworkRepo.get.mockResolvedValueOnce([mockNetwork]) + tokenService.fetchTokenBalancesWithMulticall = vi + .fn() + .mockResolvedValueOnce([ + { + balance: "100", + account: mockAccount, + address: "0x123", + networkId: mockNetwork.id, + }, + { + balance: "100", + account: mockAccount, + address: "0x456", + networkId: mockNetwork.id, + }, + ]) + + const result = await tokenService.fetchTokenBalancesFromOnChain( + mockAccount, + ) + expect(result).toEqual(mockTokenBalances) + }) + + it("should fetch token balances from on-chain for different network", async () => { + const mockNetwork = getMockNetwork() + const defaultMockNetwork = getMockNetwork({ id: defaultNetwork.id }) + const mockAccounts = [ + { address: randomAddress1, networkId: mockNetwork.id }, + { address: randomAddress2, networkId: defaultNetwork.id }, + ] + + const mockBaseTokens = [ + getMockBaseToken({ networkId: mockNetwork.id }), + getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }), + getMockBaseToken({ networkId: defaultNetwork.id }), + ] + + const mockTokens = [ + getMockToken({ ...mockBaseTokens[0] }), + getMockToken({ ...mockBaseTokens[1] }), + getMockToken({ ...mockBaseTokens[2] }), + ] + const mockTokenBalances = [ + { ...mockBaseTokens[0], balance: "100", account: mockAccounts[0] }, + { ...mockBaseTokens[1], balance: "150", account: mockAccounts[0] }, + { ...mockBaseTokens[2], balance: "1000", account: mockAccounts[1] }, + ] + mockTokenRepo.get.mockResolvedValueOnce(mockTokens) + mockNetworkRepo.get.mockResolvedValueOnce([ + mockNetwork, + defaultMockNetwork, + ]) + mockNetworkService.getById = vi.fn().mockImplementation((networkId) => { + if (networkId === mockNetwork.id) { + return mockNetwork + } else { + return defaultMockNetwork + } + }) + tokenService.fetchTokenBalancesWithMulticall = vi + .fn() + .mockResolvedValueOnce([ + { + balance: "100", + account: mockAccounts[0], + address: "0x123", + networkId: mockNetwork.id, + }, + { + balance: "150", + account: mockAccounts[0], + address: "0x456", + networkId: mockNetwork.id, + }, + ]) + .mockResolvedValueOnce([ + { + balance: "1000", + account: mockAccounts[1], + address: "0x123", + networkId: defaultNetwork.id, + }, + ]) + + const result = await tokenService.fetchTokenBalancesFromOnChain( + mockAccounts, + ) + expect(result).toEqual(mockTokenBalances) + }) + }) + describe("fetch token prices from backend", () => { + it("should fetch token prices from backend on default network", async () => { + const mockNetworkId = defaultNetwork.id + const mockTokens = [ + getMockToken({ networkId: mockNetworkId, pricingId: 1 }), + getMockToken({ + address: "0x456", + networkId: mockNetworkId, + pricingId: 2, + }), + ] + const mockTokenPrices = mockTokens.map((token, i) => + getMockTokenPriceDetails({ + address: token.address, + networkId: token.networkId, + ethValue: ((i + 1) * 0.32).toString(), + pricingId: i + 1, + }), + ) + mockTokenPriceRepo.get.mockResolvedValueOnce([]) + const result = await tokenService.fetchTokenPricesFromBackend( + mockTokens, + defaultNetwork.id, + ) + expect(result).toEqual(mockTokenPrices) + }) + + it("should return token prices without fetching on a different network", async () => { + const mockNetworkId = "goerli-alpha" + const mockTokens = [ + getMockToken({ networkId: mockNetworkId, pricingId: 1 }), + getMockToken({ + address: "0x456", + networkId: mockNetworkId, + pricingId: 2, + }), + ] + const mockTokenPrices = mockTokens.map((token, i) => + getMockTokenPriceDetails({ + address: token.address, + networkId: token.networkId, + ethValue: ((i + 1) * 0.32).toString(), + pricingId: i + 1, + }), + ) + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + const result = await tokenService.fetchTokenPricesFromBackend( + mockTokens, + mockNetworkId, + ) + expect(result).toEqual(mockTokenPrices) + }) + }) + + describe("fetch token details", () => { + it("should return the cached version", async () => { + const mockBaseToken = getMockBaseToken() + const mockToken = getMockToken() + mockTokenRepo.get.mockResolvedValueOnce([mockToken]) + const result = await tokenService.fetchTokenDetails(mockBaseToken) + expect(result).toEqual(mockToken) + }) + it("should fetch token details from onchain with multicall if cached version is not found", async () => { + const mockBaseToken = getMockBaseToken() + const mockToken = getMockToken() + mockTokenRepo.get.mockResolvedValueOnce([]) + mockNetworkService.getById = vi + .fn() + .mockResolvedValueOnce(getMockNetwork()) + tokenService.fetchTokenDetailsWithMulticall = vi + .fn() + .mockResolvedValueOnce([ + shortString.encodeShortString(mockToken.name), + shortString.encodeShortString(mockToken.symbol), + mockToken.decimals, + ]) + + const result = await tokenService.fetchTokenDetails(mockBaseToken) + + expect(result).toEqual({ + ...mockToken, + custom: true, + }) + }) + it("should fetch token details from onchain without multicall if cached version is not found ", async () => { + const mockBaseToken = getMockBaseToken() + const mockToken = getMockToken() + mockTokenRepo.get.mockResolvedValueOnce([]) + mockNetworkService.getById = vi + .fn() + .mockResolvedValueOnce(getMockNetworkWithoutMulticall()) + tokenService.fetchTokenDetailsWithoutMulticall = vi + .fn() + .mockResolvedValueOnce([ + shortString.encodeShortString(mockToken.name), + shortString.encodeShortString(mockToken.symbol), + mockToken.decimals, + ]) + + const result = await tokenService.fetchTokenDetails(mockBaseToken) + + expect(result).toEqual({ + ...mockToken, + custom: true, + }) + }) + + it("should throw error if decimals is greater than max javascript safe integer", async () => { + const mockBaseToken = getMockBaseToken() + const mockToken = getMockToken() + mockTokenRepo.get.mockResolvedValueOnce([]) + mockNetworkService.getById = vi + .fn() + .mockResolvedValueOnce(getMockNetworkWithoutMulticall()) + tokenService.fetchTokenDetailsWithoutMulticall = vi + .fn() + .mockResolvedValueOnce([ + shortString.encodeShortString(mockToken.name), + shortString.encodeShortString(mockToken.symbol), + Number.MAX_SAFE_INTEGER + 1, + ]) + await expect( + tokenService.fetchTokenDetails(mockBaseToken), + ).rejects.toThrowError( + new TokenError({ + code: "UNSAFE_DECIMALS", + options: { + context: { decimals: Number.MAX_SAFE_INTEGER + 1 }, + }, + }), + ) + }) + + it("should throw error if token details are not found with multicall", async () => { + const mockBaseToken = getMockBaseToken() + mockTokenRepo.get.mockResolvedValueOnce([]) + mockNetworkService.getById = vi + .fn() + .mockResolvedValueOnce(getMockNetwork()) + tokenService.fetchTokenDetailsWithMulticall = vi + .fn() + .mockRejectedValueOnce(new GatewayError("NOT_FOUND", "500")) + await expect( + tokenService.fetchTokenDetails(mockBaseToken), + ).rejects.toThrowError( + new TokenError({ + code: "TOKEN_DETAILS_NOT_FOUND", + message: `Token details not found for token ${mockBaseToken.address}`, + }), + ) + }) + + it("should throw error if token details are not found without multicall", async () => { + const mockBaseToken = getMockBaseToken() + mockTokenRepo.get.mockResolvedValueOnce([]) + mockNetworkService.getById = vi + .fn() + .mockResolvedValueOnce(getMockNetworkWithoutMulticall()) + tokenService.fetchTokenDetailsWithoutMulticall = vi + .fn() + .mockRejectedValueOnce(new GatewayError("NOT_FOUND", "500")) + await expect( + tokenService.fetchTokenDetails(mockBaseToken), + ).rejects.toThrowError( + new TokenError({ + code: "TOKEN_DETAILS_NOT_FOUND", + message: `Token details not found for token ${mockBaseToken.address}`, + }), + ) + }) + }) + + it("should get token balances for account", async () => { + const mockAccount = { address: "0x123", networkId: "mockNetworkId" } + const mockTokens = [getMockToken(), getMockToken({ address: "0x456" })] + const mockTokenBalances = mockTokens.map((token) => + getMockTokenWithBalance({ ...token, balance: "100" }), + ) + mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokenBalances) + const result = await tokenService.getTokenBalancesForAccount( + mockAccount, + mockTokens, + ) + expect(result).toEqual(mockTokenBalances) + }) + + describe("get currency value for token", () => { + afterEach(() => { + vi.resetAllMocks() + vi.resetModules() + }) + + it("should get currency value for tokens", async () => { + const mockTokensWithBalances = [ + getMockTokenWithBalance(), + getMockTokenWithBalance({ address: "0x456", balance: "200" }), + ] + const mockTokenPrices = [ + getMockTokenPriceDetails(), + getMockTokenPriceDetails({ + address: "0x456", + pricingId: 2, + ethValue: "0.36", + }), + ] + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + const result = await tokenService.getCurrencyValueForTokens( + mockTokensWithBalances, + ) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + usdValue: expect.any(String), + }), + ]), + ) + }) + + it("should throw error if token price is not found", async () => { + const mockTokensWithBalances = [ + getMockTokenWithBalance(), + getMockTokenWithBalance({ address: "0x456", balance: "200" }), + ] + const mockTokenPrices = [getMockTokenPriceDetails()] + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + await expect( + tokenService.getCurrencyValueForTokens(mockTokensWithBalances), + ).rejects.toThrowError( + new TokenError({ + code: "TOKEN_PRICE_NOT_FOUND", + message: `Token price for 0x456 not found`, + }), + ) + }) + + it("should throw error if token is not found", async () => { + const mockTokensWithBalances = [ + getMockTokenWithBalance(), + getMockTokenWithBalance({ address: "0x456", balance: "200" }), + ] + const mockTokenPrices = [ + getMockTokenPriceDetails(), + getMockTokenPriceDetails({ + address: "0x456", + pricingId: 2, + ethValue: "0.36", + }), + ] + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce([mockTokensWithBalances[0]]) + await expect( + tokenService.getCurrencyValueForTokens(mockTokensWithBalances), + ).rejects.toThrowError( + new TokenError({ + code: "TOKEN_NOT_FOUND", + message: `Token 0x456 not found`, + }), + ) + }) + + it("should throw error if unable to calculate currency value", async () => { + const mockTokensWithBalances = [ + getMockTokenWithBalance(), + getMockTokenWithBalance({ address: "0x456", balance: "xyz" }), // balance should be numeric + ] + const mockTokenPrices = [ + getMockTokenPriceDetails(), + getMockTokenPriceDetails({ + address: "0x456", + pricingId: 2, + ethValue: "0.36", + }), + ] + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + + await expect( + tokenService.getCurrencyValueForTokens(mockTokensWithBalances), + ).rejects.toThrowError( + new TokenError({ + code: "UNABLE_TO_CALCULATE_CURRENCY_VALUE", + message: `Unable to calculate currency value for token 0x456`, + }), + ) + }) + }) + + describe("get total currency balance for account", () => { + it("should get total currency balance for different accounts", async () => { + const mockAccounts = [ + { address: randomAddress1, networkId: defaultNetwork.id }, + { address: randomAddress2, networkId: defaultNetwork.id }, + ] + + const mockBaseTokens = [ + getMockBaseToken({ networkId: defaultNetwork.id }), + getMockBaseToken({ address: "0x456", networkId: defaultNetwork.id }), + ] + + const mockTokensWithBalances = [ + getMockTokenWithBalance({ + ...mockBaseTokens[0], + balance: BigInt(10e17).toString(), + account: mockAccounts[0], + }), + getMockTokenWithBalance({ + ...mockBaseTokens[1], + balance: BigInt(10e16).toString(), + account: mockAccounts[1], + }), + ] + const mockTokenPrices = [ + getMockTokenPriceDetails({ ...mockBaseTokens[0], pricingId: 1 }), + getMockTokenPriceDetails({ + ...mockBaseTokens[1], + pricingId: 2, + ethValue: "0.36", + }), + ] + mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + const result = await tokenService.getTotalCurrencyBalanceForAccounts( + mockAccounts, + ) + expect(result).toEqual({ + [`${randomAddress1}:${defaultNetwork.id}`]: "2000", + [`${randomAddress2}:${defaultNetwork.id}`]: "200", + }) + }) + }) + + it("should get total currency balance for account", async () => { + const mockAccount = { + address: randomAddress1, + networkId: defaultNetwork.id, + } + const mockBaseTokens = [ + getMockBaseToken({ networkId: defaultNetwork.id }), + getMockBaseToken({ address: "0x456", networkId: defaultNetwork.id }), + getMockBaseToken({ address: "0x789", networkId: defaultNetwork.id }), + ] + + const mockTokensWithBalances = [ + getMockTokenWithBalance({ + ...mockBaseTokens[0], + balance: BigInt(10e17).toString(), + account: mockAccount, + }), + getMockTokenWithBalance({ + ...mockBaseTokens[1], + balance: BigInt(10e16).toString(), + account: mockAccount, + }), + getMockTokenWithBalance({ + ...mockBaseTokens[2], + balance: BigInt(0).toString(), + account: mockAccount, + }), + ] + const mockTokenPrices = [ + getMockTokenPriceDetails({ ...mockBaseTokens[0], pricingId: 1 }), + getMockTokenPriceDetails({ + ...mockBaseTokens[1], + pricingId: 2, + ethValue: "0.36", + }), + getMockTokenPriceDetails({ + ...mockBaseTokens[2], + pricingId: 3, + ethValue: "10", + }), + ] + mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices) + mockTokenRepo.get.mockResolvedValueOnce(mockTokensWithBalances) + const result = await tokenService.getTotalCurrencyBalanceForAccounts([ + mockAccount, + ]) + expect(result).toEqual({ + [`${mockAccount.address}:${mockAccount.networkId}`]: "2200", + }) + }) +}) diff --git a/packages/extension/src/shared/token/__new/service/implementation.ts b/packages/extension/src/shared/token/__new/service/implementation.ts index 34f0c8c33..141ac8b09 100644 --- a/packages/extension/src/shared/token/__new/service/implementation.ts +++ b/packages/extension/src/shared/token/__new/service/implementation.ts @@ -26,10 +26,6 @@ import { getMulticallForNetwork } from "../../../multicall" import { getProvider } from "../../../network/provider" import { Network, defaultNetwork } from "../../../network" import { fetcherWithArgentApiHeadersForNetwork } from "../../../api/fetcher" -import { - ARGENT_API_TOKENS_INFO_URL, - ARGENT_API_TOKENS_PRICES_URL, -} from "../../../api/constants" import { getAccountIdentifier } from "../../../wallet.service" import { TokenError } from "../../../errors/token" @@ -38,18 +34,33 @@ import { TokenError } from "../../../errors/token" * It provides methods to interact with the token repository, token balance repository and token price repository. */ export class TokenService implements ITokenService { + private readonly TOKENS_INFO_URL: string + private readonly TOKENS_PRICES_URL: string /** * @param {INetworkService} networkService - The network service. * @param {ITokenRepository} tokenRepo - The token repository. * @param {ITokenBalanceRepository} tokenBalanceRepo - The token balance repository. * @param {ITokenPriceRepository} tokenPriceRepo - The token price repository. + * @param {string} TOKENS_INFO_URL - The tokens info url. + * @param {string} TOKENS_PRICES_URL - The tokens prices url. */ constructor( private readonly networkService: INetworkService, private readonly tokenRepo: ITokenRepository, private readonly tokenBalanceRepo: ITokenBalanceRepository, private readonly tokenPriceRepo: ITokenPriceRepository, - ) {} + TOKENS_INFO_URL: string | undefined, + TOKENS_PRICES_URL: string | undefined, + ) { + if (!TOKENS_INFO_URL) { + throw new TokenError({ code: "NO_TOKEN_API_URL" }) + } + if (!TOKENS_PRICES_URL) { + throw new TokenError({ code: "NO_TOKEN_PRICE_API_URL" }) + } + this.TOKENS_INFO_URL = TOKENS_INFO_URL + this.TOKENS_PRICES_URL = TOKENS_PRICES_URL + } /** * Add a token to the token repository. @@ -106,10 +117,10 @@ export class TokenService implements ITokenService { */ async fetchTokensFromBackend(networkId: string): Promise { const isDefaultNetwork = defaultNetwork.id === networkId + const tokensOnNetwork = await this.tokenRepo.get( (t) => t.networkId === networkId, ) - if (!isDefaultNetwork) { return tokensOnNetwork } @@ -119,11 +130,8 @@ export class TokenService implements ITokenService { tokensOnNetwork.map((t) => [getAccountIdentifier(t), t]), ) - if (!ARGENT_API_TOKENS_INFO_URL) { - throw new TokenError({ code: "NO_TOKEN_API_URL" }) - } const fetcher = fetcherWithArgentApiHeadersForNetwork(networkId) - const response = await fetcher(ARGENT_API_TOKENS_INFO_URL) + const response = await fetcher(this.TOKENS_INFO_URL) const parsedResponse = ApiTokenDataResponseSchema.safeParse(response) if (!parsedResponse.success) { @@ -141,6 +149,7 @@ export class TokenService implements ITokenService { getAccountIdentifier({ address: token.address, networkId }), ) return { + id: token.id, address: token.address, decimals: token.decimals || cached?.decimals || 18, name: token.name, @@ -186,7 +195,7 @@ export class TokenService implements ITokenService { ) tokenBalances.push(...balances) } else { - const balances = await this.fetchTokenBalancesWithSingleCall( + const balances = await this.fetchTokenBalancesWithoutMulticall( network, accountsArray, tokensOnCurrentNetwork, @@ -197,7 +206,7 @@ export class TokenService implements ITokenService { return tokenBalances } - private async fetchTokenBalancesWithMulticall( + public async fetchTokenBalancesWithMulticall( network: Network, accountsGroupedByNetwork: Record, tokensOnCurrentNetwork: Token[], @@ -207,7 +216,7 @@ export class TokenService implements ITokenService { const calls = tokensOnCurrentNetwork .map((token) => accounts.map((account) => - multicall.call({ + multicall.callContract({ contractAddress: token.address, entrypoint: "balanceOf", calldata: [account.address], @@ -223,7 +232,7 @@ export class TokenService implements ITokenService { const token = tokensOnCurrentNetwork[Math.floor(i / accounts.length)] const account = accounts[i % accounts.length] if (result.status === "fulfilled") { - const [low, high] = result.value + const [low, high] = result.value.result const balance = uint256.uint256ToBN({ low, high }).toString() tokenBalances.push({ account, @@ -237,7 +246,7 @@ export class TokenService implements ITokenService { return tokenBalances } - private async fetchTokenBalancesWithSingleCall( + async fetchTokenBalancesWithoutMulticall( network: Network, accountsArray: BaseWalletAccount[], tokensOnCurrentNetwork: Token[], @@ -284,11 +293,8 @@ export class TokenService implements ITokenService { return tokenPrices } - if (!ARGENT_API_TOKENS_PRICES_URL) { - throw new TokenError({ code: "NO_TOKEN_PRICE_API_URL" }) - } const fetcher = fetcherWithArgentApiHeadersForNetwork(defaultNetwork.id) - const response = await fetcher(ARGENT_API_TOKENS_PRICES_URL) + const response = await fetcher(this.TOKENS_PRICES_URL) const parsedResponse = ApiPriceDataResponseSchema.safeParse(response) if (!parsedResponse.success) { @@ -322,41 +328,37 @@ export class TokenService implements ITokenService { async fetchTokenDetails(baseToken: BaseToken): Promise { const [token] = await this.tokenRepo.get((t) => equalToken(t, baseToken)) - if (token) { + + // Only return cached token if it's not a custom token + // Otherwise fetch token details from blockchain + if (token && !token.custom) { return token } + const network = await this.networkService.getById(baseToken.networkId) - const tokenEntryPoints = ["name", "symbol", "decimals"] let name: string, symbol: string, decimals: string - if (network.multicallAddress) { - const multicall = getMulticallForNetwork(network) - const responses = await Promise.all( - tokenEntryPoints.map((entrypoint) => - multicall.call({ - contractAddress: baseToken.address, - entrypoint, - }), - ), - ) - ;[name, symbol, decimals] = responses.map((response) => response[0]) - } else { - const provider = getProvider(network) - const responses = await Promise.all( - tokenEntryPoints.map((entrypoint) => - provider.callContract({ - contractAddress: baseToken.address, - entrypoint, - }), - ), - ) - ;[name, symbol, decimals] = responses.map( - (response) => response.result[0], - ) + + try { + if (network.multicallAddress) { + ;[name, symbol, decimals] = await this.fetchTokenDetailsWithMulticall( + baseToken, + network, + ) + } else { + ;[name, symbol, decimals] = + await this.fetchTokenDetailsWithoutMulticall(baseToken, network) + } + } catch (error) { + console.error(error) + throw new TokenError({ + code: "TOKEN_DETAILS_NOT_FOUND", + message: `Token details not found for token ${baseToken.address}`, + }) } if (Number.parseInt(decimals) > Number.MAX_SAFE_INTEGER) { throw new TokenError({ - code: "NOT_SAFE", + code: "UNSAFE_DECIMALS", options: { context: { decimals } }, }) } @@ -371,6 +373,40 @@ export class TokenService implements ITokenService { } } + async fetchTokenDetailsWithMulticall( + baseToken: BaseToken, + network: Network, + tokenEntryPoints = ["name", "symbol", "decimals"], + ): Promise { + const multicall = getMulticallForNetwork(network) + const responses = await Promise.all( + tokenEntryPoints.map((entrypoint) => + multicall.callContract({ + contractAddress: baseToken.address, + entrypoint, + }), + ), + ) + return responses.map((response) => response.result[0]) + } + + async fetchTokenDetailsWithoutMulticall( + baseToken: BaseToken, + network: Network, + tokenEntryPoints = ["name", "symbol", "decimals"], + ): Promise { + const provider = getProvider(network) + const responses = await Promise.all( + tokenEntryPoints.map((entrypoint) => + provider.callContract({ + contractAddress: baseToken.address, + entrypoint, + }), + ), + ) + return responses.map((response) => response.result[0]) + } + async getToken(baseToken: BaseToken): Promise { const parsedToken = BaseTokenSchema.parse(baseToken) const [token] = await this.tokenRepo.get((t) => equalToken(t, parsedToken)) @@ -479,13 +515,12 @@ export class TokenService implements ITokenService { tokensWithBalanceAndPrice, ({ account }) => `${account.address}:${account.networkId}`, ) - const totalCurrencyBalanceForAccounts: { [key: string]: string } = {} for (const account in groupedBalances) { const totalBalance = groupedBalances[account].reduce( (total, token) => - total + bigDecimal.parseCurrency(token.usdValue || "0"), + total + bigDecimal.parseCurrency(token.usdValue || "0").value, 0n, ) totalCurrencyBalanceForAccounts[account] = diff --git a/packages/extension/src/shared/token/__new/service/index.ts b/packages/extension/src/shared/token/__new/service/index.ts index 25bd5d4a1..4765893c1 100644 --- a/packages/extension/src/shared/token/__new/service/index.ts +++ b/packages/extension/src/shared/token/__new/service/index.ts @@ -1,3 +1,7 @@ +import { + ARGENT_API_TOKENS_INFO_URL, + ARGENT_API_TOKENS_PRICES_URL, +} from "../../../api/constants" import { networkService } from "../../../network/service" import { tokenRepo } from "../repository/token" import { tokenBalanceRepo } from "../repository/tokenBalance" @@ -9,4 +13,6 @@ export const tokenService = new TokenService( tokenRepo, tokenBalanceRepo, tokenPriceRepo, + ARGENT_API_TOKENS_INFO_URL, + ARGENT_API_TOKENS_PRICES_URL, ) diff --git a/packages/extension/src/shared/token/__new/types/token.model.ts b/packages/extension/src/shared/token/__new/types/token.model.ts index 862c5763b..257948d62 100644 --- a/packages/extension/src/shared/token/__new/types/token.model.ts +++ b/packages/extension/src/shared/token/__new/types/token.model.ts @@ -22,6 +22,7 @@ export const RequestTokenSchema = z.object({ export type RequestToken = z.infer export const TokenSchema = RequestTokenSchema.required().extend({ + id: z.number().optional(), iconUrl: z.string().optional(), showAlways: z.boolean().optional(), popular: z.boolean().optional(), diff --git a/packages/extension/src/shared/token/__new/types/tokenPrice.model.ts b/packages/extension/src/shared/token/__new/types/tokenPrice.model.ts index ea026f6ef..3c9149978 100644 --- a/packages/extension/src/shared/token/__new/types/tokenPrice.model.ts +++ b/packages/extension/src/shared/token/__new/types/tokenPrice.model.ts @@ -1,6 +1,5 @@ import { z } from "zod" import { BaseTokenSchema, TokenSchema } from "./token.model" -import { addressSchema } from "@argent/shared" import { BaseTokenWithBalanceSchema } from "./tokenBalance.model" export const ApiPriceDetailsSchema = z.object({ diff --git a/packages/extension/src/shared/token/__new/utils/index.ts b/packages/extension/src/shared/token/__new/utils/index.ts index f13a1daec..5f45c8434 100644 --- a/packages/extension/src/shared/token/__new/utils/index.ts +++ b/packages/extension/src/shared/token/__new/utils/index.ts @@ -4,10 +4,9 @@ import { isEqualAddress, isNumeric, } from "@argent/shared" -import { BaseToken, BaseTokenSchema, Token } from "../types/token.model" +import { BaseToken, Token } from "../types/token.model" import { BigNumberish } from "starknet" import defaultTokens from "../../../../assets/default-tokens.json" -import { tokenRepo } from "../repository/token" export const equalToken = (a: BaseToken, b: BaseToken) => a.networkId === b.networkId && isEqualAddress(a.address, b.address) @@ -43,9 +42,14 @@ export const convertTokenAmountToCurrencyValue = ({ /** multiply to convert to currency */ const currencyValue = - BigInt(amount) * bigDecimal.parseUnits(unitCurrencyValue.toString(), 6) + BigInt(amount) * + bigDecimal.parseUnits(unitCurrencyValue.toString(), 6).value + /** keep as string to avoid loss of precision elsewhere */ - return bigDecimal.formatUnits(currencyValue, decimalsNumber + 6) + return bigDecimal.formatUnits({ + value: currencyValue, + decimals: decimalsNumber + 6, + }) } export const parsedDefaultTokens: Token[] = defaultTokens.map((token) => ({ diff --git a/packages/extension/src/shared/token/__new/worker/implementation.test.ts b/packages/extension/src/shared/token/__new/worker/implementation.test.ts new file mode 100644 index 000000000..6d4d7dfd0 --- /dev/null +++ b/packages/extension/src/shared/token/__new/worker/implementation.test.ts @@ -0,0 +1,280 @@ +import { IRepository } from "./../../../storage/__new/interface" +import { Mocked } from "vitest" +import { INetworkService } from "../../../network/service/interface" +import { ITokenService } from "../service/interface" +import { TokenWorker, TokenWorkerSchedule } from "./implementation" +import { MockFnRepository } from "../../../storage/__new/__test__/mockFunctionImplementation" +import { WalletStorageProps } from "../../../wallet/walletStore" +import { KeyValueStorage } from "../../../storage" +import { Transaction } from "../../../transactions" +import { Token } from "../types/token.model" +import { IAccountService } from "../../../account/service/interface" +import { IScheduleService } from "../../../schedule/interface" +import { + emitterMock, + recoverySharedServiceMock, + sessionServiceMock, +} from "../../../../background/wallet/test.utils" +import { IBackgroundUIService } from "../../../../background/__new/services/ui/interface" +import { getMockNetwork } from "../../../../../test/network.mock" +import { + getMockBaseToken, + getMockToken, + getMockTokenPriceDetails, +} from "../../../../../test/token.mock" +import { addressSchema } from "@argent/shared" +import { stark } from "starknet" +import { BaseWalletAccount } from "../../../wallet.model" +import { BaseTokenWithBalance } from "../types/tokenBalance.model" +import { TokenPriceDetails } from "../types/tokenPrice.model" +import { defaultNetwork } from "../../../network" + +const accountAddress1 = addressSchema.parse(stark.randomAddress()) +const accountAddress2 = addressSchema.parse(stark.randomAddress()) +const tokenAddress1 = addressSchema.parse(stark.randomAddress()) +const tokenAddress2 = addressSchema.parse(stark.randomAddress()) + +describe("TokenWorker", () => { + let tokenWorker: TokenWorker + let mockTokenService: Mocked + let mockNetworkService: Mocked + let mockWalletStore: KeyValueStorage + let mockTransactionsRepo: IRepository + let mockTokenRepo: IRepository + let mockAccountService: IAccountService + let mockScheduleService: IScheduleService + let mockBackgroundUIService: IBackgroundUIService + + beforeEach(() => { + // Initialize mocks + mockTokenService = { + fetchTokensFromBackend: vi.fn(), + updateTokens: vi.fn(), + addToken: vi.fn(), + removeToken: vi.fn(), + fetchTokenBalancesFromOnChain: vi.fn(), + fetchTokenDetails: vi.fn(), + fetchTokenPricesFromBackend: vi.fn(), + getCurrencyValueForTokens: vi.fn(), + getToken: vi.fn(), + getTokenBalancesForAccount: vi.fn(), + getTokens: vi.fn(), + getTotalCurrencyBalanceForAccounts: vi.fn(), + updateTokenBalances: vi.fn(), + updateTokenPrices: vi.fn(), + } + + mockNetworkService = { + get: vi.fn(), + getById: vi.fn(), + } as unknown as Mocked + + mockWalletStore = { + get: vi.fn(), + set: vi.fn(), + subscribe: vi.fn(), + } as unknown as KeyValueStorage + + mockAccountService = { + get: vi.fn(), + } as unknown as IAccountService + + mockTransactionsRepo = new MockFnRepository() + mockTokenRepo = new MockFnRepository() + + mockBackgroundUIService = { + emitter: emitterMock, + openUiAndUnlock: vi.fn(), + } as unknown as IBackgroundUIService + + mockScheduleService = { + registerImplementation: vi.fn(), + in: vi.fn(), + every: vi.fn(), + delete: vi.fn(), + onInstallAndUpgrade: vi.fn(), + onStartup: vi.fn(), + } + + tokenWorker = new TokenWorker( + mockWalletStore, + mockTransactionsRepo, + mockTokenRepo, + mockTokenService, + mockAccountService, + mockNetworkService, + sessionServiceMock, + recoverySharedServiceMock, + mockBackgroundUIService, + mockScheduleService, + ) + }) + + describe("updateTokens", () => { + it("should fetch tokens for all networks and update the token service", async () => { + // Arrange + const mockNetworks = [ + getMockNetwork({ id: "1" }), + getMockNetwork({ id: "2" }), + ] + const mockTokens = [ + [getMockToken({ address: tokenAddress1, networkId: "1" })], + [getMockToken({ address: tokenAddress2, networkId: "2" })], + ] + mockNetworkService.get.mockResolvedValue(mockNetworks) + mockTokenService.fetchTokensFromBackend + .mockResolvedValueOnce(mockTokens[0]) + .mockResolvedValueOnce(mockTokens[1]) + + await tokenWorker.updateTokens() + + expect(mockNetworkService.get).toHaveBeenCalled() + expect(mockTokenService.fetchTokensFromBackend).toHaveBeenCalledTimes(2) + expect(mockTokenService.fetchTokensFromBackend).toHaveBeenNthCalledWith( + 1, + mockNetworks[0].id, + ) + expect(mockTokenService.fetchTokensFromBackend).toHaveBeenNthCalledWith( + 2, + mockNetworks[1].id, + ) + expect(mockTokenService.updateTokens).toHaveBeenCalledWith( + mockTokens.flat(), + ) + }) + }) + + describe("updateTokenBalances", () => { + it("should fetch token balances for the provided account and update the token service", async () => { + // Arrange + const mockAccount: BaseWalletAccount = { + address: accountAddress1, + networkId: "1" /* other properties */, + } + const mockBaseToken = getMockBaseToken({ networkId: "1" }) + const mockTokens: Token[] = [getMockToken({ networkId: "1" })] + const mockTokensWithBalance: BaseTokenWithBalance[] = [ + { + ...mockBaseToken, + account: mockAccount, + balance: "100", + }, + ] + mockTokenService.getTokens.mockResolvedValue(mockTokens) + mockTokenService.fetchTokenBalancesFromOnChain.mockResolvedValue( + mockTokensWithBalance, + ) + + // Act + await tokenWorker.updateTokenBalances(mockAccount) + + // Assert + expect(mockTokenService.getTokens).toHaveBeenCalledWith( + expect.any(Function), + ) + expect( + mockTokenService.fetchTokenBalancesFromOnChain, + ).toHaveBeenCalledWith([mockAccount], mockTokens) + expect(mockTokenService.updateTokenBalances).toHaveBeenCalledWith( + mockTokensWithBalance, + ) + }) + + it("should fetch token balances for all accounts on the current network and update the token service when no account is provided", async () => { + const mockSelectedAccount: BaseWalletAccount = { + address: accountAddress1, + networkId: "1", + } + const mockAccounts: BaseWalletAccount[] = [mockSelectedAccount] + const mockBaseToken = getMockBaseToken({ networkId: "1" }) + const mockTokens: Token[] = [getMockToken({ networkId: "1" })] + const mockTokensWithBalances: BaseTokenWithBalance[] = [ + { + ...mockBaseToken, + account: mockSelectedAccount, + balance: "100", + }, + ] + mockWalletStore.get = vi.fn().mockReturnValue(mockSelectedAccount) + mockAccountService.get = vi.fn().mockResolvedValue(mockAccounts) + mockTokenService.getTokens = vi.fn().mockResolvedValue(mockTokens) + mockTokenService.fetchTokenBalancesFromOnChain = vi + .fn() + .mockResolvedValue(mockTokensWithBalances) + + await tokenWorker.updateTokenBalances() + expect(mockWalletStore.get).toHaveBeenCalledWith("selected") + expect(mockAccountService.get).toHaveBeenCalledWith(expect.any(Function)) + expect(mockTokenService.getTokens).toHaveBeenCalledWith( + expect.any(Function), + ) + expect( + mockTokenService.fetchTokenBalancesFromOnChain, + ).toHaveBeenCalledWith(mockAccounts, mockTokens) + expect(mockTokenService.updateTokenBalances).toHaveBeenCalledWith( + mockTokensWithBalances, + ) + }) + }) + + describe("updateTokenPrices", () => { + it("should fetch token prices for the default network and update the token service", async () => { + // Arrange + const mockTokens: Token[] = [getMockToken()] + const mockTokenPrices: TokenPriceDetails[] = [ + getMockTokenPriceDetails({ pricingId: 1 }), + ] + mockTokenService.getTokens.mockResolvedValue(mockTokens) + mockTokenService.fetchTokenPricesFromBackend.mockResolvedValue( + mockTokenPrices, + ) + + await tokenWorker.updateTokenPrices() + + // Assert + expect(mockTokenService.getTokens).toHaveBeenCalledWith( + expect.any(Function), + ) + expect(mockTokenService.fetchTokenPricesFromBackend).toHaveBeenCalledWith( + mockTokens, + defaultNetwork.id, + ) + expect(mockTokenService.updateTokenPrices).toHaveBeenCalledWith( + mockTokenPrices, + ) + }) + }) + + describe("onOpened ", () => { + it("should start the token worker schedule when opened", async () => { + // Act + tokenWorker.onOpened(true) + + // Assert + expect(mockScheduleService.every).toHaveBeenCalledTimes(3) + expect(mockScheduleService.every).toHaveBeenNthCalledWith(1, 86400, { + id: TokenWorkerSchedule.updateTokens, + }) + expect(mockScheduleService.every).toHaveBeenNthCalledWith(2, 20, { + id: TokenWorkerSchedule.updateTokenBalances, + }) + expect(mockScheduleService.every).toHaveBeenNthCalledWith(3, 60, { + id: TokenWorkerSchedule.updateTokenPrices, + }) + }) + + it("should delete the token worker schedule when closed", async () => { + // Act + tokenWorker.onOpened(false) + + // Assert + expect(mockScheduleService.delete).toHaveBeenCalledTimes(2) + expect(mockScheduleService.delete).toHaveBeenNthCalledWith(1, { + id: TokenWorkerSchedule.updateTokenBalances, + }) + expect(mockScheduleService.delete).toHaveBeenNthCalledWith(2, { + id: TokenWorkerSchedule.updateTokenPrices, + }) + }) + }) +}) diff --git a/packages/extension/src/shared/token/__new/worker/implementation.ts b/packages/extension/src/shared/token/__new/worker/implementation.ts index aacc14760..0634e548a 100644 --- a/packages/extension/src/shared/token/__new/worker/implementation.ts +++ b/packages/extension/src/shared/token/__new/worker/implementation.ts @@ -1,4 +1,7 @@ -import { IScheduleService } from "../../../schedule/interface" +import { + IScheduleService, + ImplementedScheduledTask, +} from "../../../schedule/interface" import { ITokenService } from "../service/interface" import { ITokenWorker } from "./interface" import { INetworkService } from "../../../network/service/interface" @@ -20,11 +23,16 @@ import { WalletRecoverySharedService } from "../../../../background/wallet/recov import { Recovered } from "../../../../background/wallet/recovery/interface" import { KeyValueStorage } from "../../../storage" import { RefreshInterval } from "../../../config" +import { + onInstallAndUpgrade, + onStartup, +} from "../../../../background/__new/services/worker/schedule/decorators" +import { pipe } from "../../../../background/__new/services/worker/schedule/pipe" /** * Enum for scheduling token updates */ -const enum TokenWorkerSchedule { +export const enum TokenWorkerSchedule { updateTokens = "updateTokens", // Schedule for updating tokens updateTokenBalances = "updateTokenBalances", // Schedule for updating token balances updateTokenPrices = "updateTokenPrices", // Schedule for updating token prices @@ -78,10 +86,11 @@ export class TokenWorker implements ITokenWorker { */ initialize(): void { // Register schedules - void this.scheduleService.registerImplementation({ + const updateTokensTask: ImplementedScheduledTask = { id: TokenWorkerSchedule.updateTokens, callback: this.updateTokens.bind(this), - }) + } + void this.scheduleService.registerImplementation(updateTokensTask) void this.scheduleService.registerImplementation({ id: TokenWorkerSchedule.updateTokenBalances, callback: this.updateTokenBalances.bind(this), @@ -130,17 +139,16 @@ export class TokenWorker implements ITokenWorker { void this.updateTokenBalances() // Update token balances on token change void this.updateTokenPrices() // Update token prices on token change }) - - setTimeout(() => { - void this.updateTokens() // Update tokens on startup - }, 100) } /** * Update tokens * Fetches tokens for all networks and updates the token service */ - async updateTokens(): Promise { + updateTokens = pipe( + onStartup(this.scheduleService), // This will run the function on startup + onInstallAndUpgrade(this.scheduleService), // This will run the function on update + )(async (): Promise => { const networks = await this.networkService.get() // Get all networks // // Fetch tokens for all networks in parallel @@ -155,7 +163,7 @@ export class TokenWorker implements ITokenWorker { .flat() await this.tokenService.updateTokens(tokens) // Update tokens in the token service - } + }) /** * Update token balances diff --git a/packages/extension/src/shared/token/price.ts b/packages/extension/src/shared/token/price.ts index 8466a9e4d..0ae2269db 100644 --- a/packages/extension/src/shared/token/price.ts +++ b/packages/extension/src/shared/token/price.ts @@ -114,7 +114,7 @@ export const sumTokenBalancesToCurrencyValue = ({ if (currencyValue !== undefined) { didGetValidConversion = true sumTokenBalance = - sumTokenBalance + bigDecimal.parseUnits(currencyValue, 6) + sumTokenBalance + bigDecimal.parseCurrency(currencyValue).value } } }) @@ -123,7 +123,7 @@ export const sumTokenBalancesToCurrencyValue = ({ return } /** keep as string to avoid loss of precision elsewhere */ - return bigDecimal.formatUnits(sumTokenBalance, 6) + return bigDecimal.formatCurrency(sumTokenBalance) } export interface IConvertTokenAmountToCurrencyValue { @@ -157,9 +157,13 @@ export const convertTokenAmountToCurrencyValue = ({ /** multiply to convert to currency */ const currencyValue = - BigInt(amount) * bigDecimal.parseUnits(unitCurrencyValue.toString(), 6) + BigInt(amount) * + bigDecimal.parseCurrency(unitCurrencyValue.toString()).value /** keep as string to avoid loss of precision elsewhere */ - return bigDecimal.formatUnits(currencyValue, decimalsNumber + 6) + return bigDecimal.formatUnits({ + value: currencyValue, + decimals: decimalsNumber + 6, + }) } /** @@ -235,7 +239,7 @@ export const prettifyTokenAmount = ({ isPositiveValue = balance > 0n const balanceFullString = decimalsNumber > 0 - ? bigDecimal.formatUnits(balance, decimalsNumber) + ? bigDecimal.formatUnits({ value: balance, decimals: decimalsNumber }) : balance.toString() prettyValue = decimalsNumber > 0 @@ -290,5 +294,7 @@ export const convertTokenUnitAmountWithDecimals = ({ const decimalsNumber = Number(decimals) // keep as string to avoid loss of precision elsewhere - return bigDecimal.parseUnits(unitAmount.toString(), decimalsNumber).toString() + return bigDecimal + .parseUnits(unitAmount.toString(), decimalsNumber) + .value.toString() } diff --git a/packages/extension/src/shared/utils/argentAccountVersion.ts b/packages/extension/src/shared/utils/argentAccountVersion.ts index cfe2f6cc3..a55024556 100644 --- a/packages/extension/src/shared/utils/argentAccountVersion.ts +++ b/packages/extension/src/shared/utils/argentAccountVersion.ts @@ -19,12 +19,12 @@ export async function getAccountCairoVersion( if (network.multicallAddress) { const multicall = getMulticallForNetwork(network) - const response = await multicall.call({ + const response = await multicall.callContract({ contractAddress: accountAddress, entrypoint: "getVersion", }) - encodedString = response[0] + encodedString = response.result[0] } else { const provider = getProvider(network) const response = await provider.callContract({ diff --git a/packages/extension/src/shared/utils/number.ts b/packages/extension/src/shared/utils/number.ts index 2211b6ba4..6e3b32918 100644 --- a/packages/extension/src/shared/utils/number.ts +++ b/packages/extension/src/shared/utils/number.ts @@ -72,8 +72,11 @@ export const prettifyNumber = ( // If number is greater than or equal to 1, we format with minimum decimal places if (parseFloat(numberString) >= 1) { - const numberBN = bigDecimal.parseUnits(numberString, minDecimalPlaces) - untrimmed = bigDecimal.formatUnits(numberBN, minDecimalPlaces) + const numberBN = bigDecimal.parseUnits(numberString, minDecimalPlaces).value + untrimmed = bigDecimal.formatUnits({ + value: numberBN, + decimals: minDecimalPlaces, + }) } else { // For numbers less than 1, determine the number of leading zeros after the decimal point const leadingZerosInDecimalPart = @@ -86,8 +89,14 @@ export const prettifyNumber = ( ) // Format the number with the required decimal places - const numberBN = bigDecimal.parseUnits(numberString, prettyDecimalPlaces) - untrimmed = bigDecimal.formatUnits(numberBN, prettyDecimalPlaces) + const numberBN = bigDecimal.parseUnits( + numberString, + prettyDecimalPlaces, + ).value + untrimmed = bigDecimal.formatUnits({ + value: numberBN, + decimals: prettyDecimalPlaces, + }) } // Split the number into integer and fraction parts diff --git a/packages/extension/src/shared/wallet.service.ts b/packages/extension/src/shared/wallet.service.ts index c8475f02e..fd57c104c 100644 --- a/packages/extension/src/shared/wallet.service.ts +++ b/packages/extension/src/shared/wallet.service.ts @@ -1,5 +1,3 @@ -import { isFeatureEnabled } from "@argent/shared" - import { isEqualAddress } from "../ui/services/addresses" import { BaseWalletAccount, WalletAccount } from "./wallet.model" @@ -29,18 +27,11 @@ export const isDeprecated = ({ signer, network }: WalletAccount): boolean => { } export const isDeprecatedTxV0 = (account: WalletAccount): boolean => { - const hideDeprecatedAccounts = isFeatureEnabled( - process.env.FEATURE_HIDE_DEPRECATED_ACCOUNTS, - ) - - if (!hideDeprecatedAccounts) { - return false - } - return ( !!account.classHash && - DEPRECATED_TX_V0_ACCOUNT_IMPLEMENTATION_CLASS_HASH.includes( - account.classHash.toLowerCase(), + DEPRECATED_TX_V0_ACCOUNT_IMPLEMENTATION_CLASS_HASH.some( + (deprecatedClassHash) => + isEqualAddress(deprecatedClassHash, account.classHash), ) ) } diff --git a/packages/extension/src/ui/App.tsx b/packages/extension/src/ui/App.tsx index a14dd0d41..877d179cc 100644 --- a/packages/extension/src/ui/App.tsx +++ b/packages/extension/src/ui/App.tsx @@ -18,25 +18,37 @@ import { useCaptureEntryRouteRestorationState } from "./features/stateRestoratio import { useTracking } from "./services/analytics" import SoftReloadProvider from "./services/resetAndReload" import { useSentryInit } from "./services/sentry" -import { swrCacheProvider } from "./services/swr.service" +import { onErrorRetry, swrCacheProvider } from "./services/swr.service" import { ThemeProvider, muiTheme } from "./theme" import { allAccountsView } from "./views/account" import { useView } from "./views/implementation/react" +import { allNftsView } from "./views/nft" /** TODO: refactor: remove when test `User should be able to restore default networks if network is not selected` passes without this workaround */ const useAccountsListFix = () => { useView(allAccountsView) } +/** TODO: this is a workaround for background storage updates to `nftsRepository` not propagating into ui `allNftsView` unless the hook is mounted */ +const useNftsFix = () => { + useView(allNftsView) +} + export const App: FC = () => { const emitter = useRef(new Emittery()).current useTracking() useSentryInit() useCaptureEntryRouteRestorationState() useAccountsListFix() + useNftsFix() return ( - swrCacheProvider }}> + swrCacheProvider, + onErrorRetry: onErrorRetry, + }} + > diff --git a/packages/extension/src/ui/AppBackgroundError.tsx b/packages/extension/src/ui/AppBackgroundError.tsx new file mode 100644 index 000000000..298e5cdde --- /dev/null +++ b/packages/extension/src/ui/AppBackgroundError.tsx @@ -0,0 +1,21 @@ +import { Center, Flex } from "@chakra-ui/react" +import { FC } from "react" + +import { H4, P3 } from "@argent/ui" +import { SupportFooter } from "./features/settings/SupportFooter" + +export const AppBackgroundError: FC = () => { + return ( + +
+

Argent X can’t start

+ + Sorry, an error occurred while starting the Argent X background + process. Accounts are not affected. Please contact support for further + instructions. + +
+ +
+ ) +} diff --git a/packages/extension/src/ui/AppRoutes.tsx b/packages/extension/src/ui/AppRoutes.tsx index d89b8857b..057d6f0f2 100644 --- a/packages/extension/src/ui/AppRoutes.tsx +++ b/packages/extension/src/ui/AppRoutes.tsx @@ -8,7 +8,6 @@ import { useAppState, useMessageStreamHandler } from "./app.state" import { ResponsiveBox } from "./components/Responsive" import { TransactionDetailScreen } from "./features/accountActivity/TransactionDetailScreen" import { AccountEditScreen } from "./features/accountEdit/AccountEditScreen" -import { AccountImplementationScreen } from "./features/accountEdit/AccountImplementationScreen" import { CollectionNftsContainer } from "./features/accountNfts/CollectionNftsContainer" import { NftScreenContainer } from "./features/accountNfts/NftScreenContainer" import { AddPluginScreen } from "./features/accountPlugins.tsx/AddPluginScreen" @@ -27,7 +26,6 @@ import { LoadingScreenContainer } from "./features/actions/LoadingScreenContaine import { FundingBridgeScreen } from "./features/funding/FundingBridgeScreen" import { FundingFaucetFallbackScreen } from "./features/funding/FundingFaucetFallbackScreen" import { FundingProviderScreen } from "./features/funding/FundingProviderScreen" -import { FundingQrCodeScreen } from "./features/funding/FundingQrCodeScreen" import { FundingScreen } from "./features/funding/FundingScreen" import { LockScreen } from "./features/lock/LockScreen" import { ResetScreen } from "./features/lock/ResetScreen" @@ -95,6 +93,9 @@ import { SuspenseScreen } from "./components/SuspenseScreen" import { BetaFeaturesSettings } from "./features/settings/BetaFeatureSettings" import { ChangeAccountImplementationScreen } from "./features/accountEdit/ChangeAccountImplementationScreen" import { MultisigReplaceOwnerScreen } from "./features/multisig/MultisigReplaceOwnerScreen" +import { FundingQrCodeScreenContainer } from "./features/funding/FundingQrCodeScreenContainer" +import { AppBackgroundError } from "./AppBackgroundError" +import { isRecoveringView } from "./views/recovery" interface LocationWithState extends Location { state: { @@ -129,6 +130,10 @@ const ResponsiveRoutes: FC = () => ( const nonWalletRoutes = ( <> + } + /> } /> } /> } /> @@ -206,11 +211,6 @@ const walletRoutes = ( path={routes.changeAccountImplementations.path} element={} /> - } - /> } + element={} /> { const { isLoading } = useAppState() const hasActions = useView(hasActionsView) + const isRecovering = useView(isRecoveringView) /** TODO: refactor: this should maybe be invoked by service + worker pattern */ const showActions = useMemo(() => { @@ -605,6 +606,9 @@ export const AppRoutes: FC = () => { return hasActions && !isNonWalletRoute }, [hasActions, pathname, state]) + if (isRecovering) { + return + } if (isLoading) { return } diff --git a/packages/extension/src/ui/components/Address.tsx b/packages/extension/src/ui/components/Address.tsx deleted file mode 100644 index f5dbe91cb..000000000 --- a/packages/extension/src/ui/components/Address.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import styled from "styled-components" - -export const AccountName = styled.h1` - font-style: normal; - font-weight: 700; - font-size: 22px; - line-height: 28px; - margin: 32px 0 16px 0; -` - -export const AccountAddress = styled.p` - font-style: normal; - font-weight: 400; - font-size: 17px; - line-height: 22px; - word-spacing: 5px; - color: ${({ theme }) => theme.text2}; -` diff --git a/packages/extension/src/ui/components/StatusIndicator.tsx b/packages/extension/src/ui/components/StatusIndicator.tsx index 374a0d2ee..809230aa0 100644 --- a/packages/extension/src/ui/components/StatusIndicator.tsx +++ b/packages/extension/src/ui/components/StatusIndicator.tsx @@ -6,7 +6,7 @@ import { NetworkStatus } from "../../shared/network" import { assertNever } from "../services/assertNever" import { NetworkWarningIcon } from "./Icons/NetworkWarningIcon" -export type StatusIndicatorColor = "green" | "orange" | "red" | "transparent" +export type StatusIndicatorColor = "green" | "orange" | "red" | "neutral" interface StatusIndicatorProps { color?: StatusIndicatorColor @@ -23,17 +23,17 @@ export function mapNetworkStatusToColor( case "ok": return "green" case "unknown": - return "transparent" + return "neutral" case undefined: - return "transparent" + return "neutral" default: assertNever(status) - return "transparent" + return "neutral" } } export const StatusIndicator = ({ - color = "transparent", + color = "neutral", }: { color: StatusIndicatorColor }) => ( @@ -49,13 +49,13 @@ export const StatusIndicator = ({ ? "#ffa85c" : color === "red" ? "#C12026" - : "transparent" + : "#808080" } /> ) export const NetworkStatusIndicator: FC = ({ - color = "transparent", + color = "neutral", }) => { if (color === "orange") { return diff --git a/packages/extension/src/ui/features/accountActivity/AccountActivityContainer.tsx b/packages/extension/src/ui/features/accountActivity/AccountActivityContainer.tsx index 656ff9abc..1368a1031 100644 --- a/packages/extension/src/ui/features/accountActivity/AccountActivityContainer.tsx +++ b/packages/extension/src/ui/features/accountActivity/AccountActivityContainer.tsx @@ -22,6 +22,7 @@ import { PendingTransactions } from "./PendingTransactions" import { isVoyagerTransaction } from "./transform/is" import { ActivityTransaction } from "./useActivity" import { useArgentExplorerAccountTransactionsInfinite } from "./useArgentExplorer" +import { isEqualAddress } from "@argent/shared" const { ActivityIcon } = icons @@ -108,7 +109,10 @@ export const AccountActivityLoader: FC = ({ const mergedTransactions = voyagerTransactions.map((voyagerTransaction) => { const explorerTransaction = explorerTransactions.find( (explorerTransaction) => - explorerTransaction.transactionHash === voyagerTransaction.hash, + isEqualAddress( + explorerTransaction.transactionHash, + voyagerTransaction.hash, + ), ) // TODO: remove this when after backend update @@ -133,7 +137,9 @@ export const AccountActivityLoader: FC = ({ const unmatchedExplorerTransactions = explorerTransactions.filter( (explorerTransaction) => - !matchedHashes.includes(explorerTransaction.transactionHash), + !matchedHashes.some((matchedHash) => + isEqualAddress(matchedHash, explorerTransaction.transactionHash), + ), ) const transactionsWithoutTimestamp = [] @@ -162,7 +168,11 @@ export const AccountActivityLoader: FC = ({ > = {} const { transactions, transactionsWithoutTimestamp } = mergedTransactions let lastExplorerTransactionHash - for (const transaction of transactions) { + const submittedTransactions = transactions.filter( + (transaction) => transaction.finalityStatus !== "NOT_RECEIVED", + ) + + for (const transaction of submittedTransactions) { const date = new Date(transaction.timestamp * 1000).toISOString() const dateLabel = formatDate(date) if (!mergedActivity[dateLabel]) { diff --git a/packages/extension/src/ui/features/accountActivity/TransactionListItem.tsx b/packages/extension/src/ui/features/accountActivity/TransactionListItem.tsx index 591bb52ac..3739ace58 100644 --- a/packages/extension/src/ui/features/accountActivity/TransactionListItem.tsx +++ b/packages/extension/src/ui/features/accountActivity/TransactionListItem.tsx @@ -159,6 +159,7 @@ export const TransactionListItem: FC = ({ return ( !isCancelled && onClick?.(e)} _hover={{ cursor: isCancelled ? "default" : "pointer" }} diff --git a/packages/extension/src/ui/features/accountActivity/transform/explorerTransaction/__test__/transformExplorerTransaction.test.ts b/packages/extension/src/ui/features/accountActivity/transform/explorerTransaction/__test__/transformExplorerTransaction.test.ts index 34172485d..72f60856d 100644 --- a/packages/extension/src/ui/features/accountActivity/transform/explorerTransaction/__test__/transformExplorerTransaction.test.ts +++ b/packages/extension/src/ui/features/accountActivity/transform/explorerTransaction/__test__/transformExplorerTransaction.test.ts @@ -53,6 +53,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -85,6 +86,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -117,6 +119,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -149,6 +152,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -180,6 +184,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "mainnet-alpha", "networkId": "mainnet-alpha", @@ -238,6 +243,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "mainnet-alpha", "networkId": "mainnet-alpha", @@ -452,6 +458,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -490,6 +497,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -503,6 +511,7 @@ describe("transformExplorerTransaction", () => { "address": "0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426", "decimals": 6, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png", + "id": 2, "name": "USD Coin", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -538,6 +547,7 @@ describe("transformExplorerTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", @@ -551,6 +561,7 @@ describe("transformExplorerTransaction", () => { "address": "0x03e85bfbb8e2a42b7bead9e88e9a1b19dbccf661471061807292120462396ec9", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png", + "id": 6, "name": "DAI", "network": "goerli-alpha", "networkId": "goerli-alpha", diff --git a/packages/extension/src/ui/features/accountActivity/transform/transaction/__test__/transformTransaction.test.ts b/packages/extension/src/ui/features/accountActivity/transform/transaction/__test__/transformTransaction.test.ts index 168fc1456..b4f3c4a4f 100644 --- a/packages/extension/src/ui/features/accountActivity/transform/transaction/__test__/transformTransaction.test.ts +++ b/packages/extension/src/ui/features/accountActivity/transform/transaction/__test__/transformTransaction.test.ts @@ -82,6 +82,7 @@ describe("transformTransaction", () => { "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "decimals": 18, "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png", + "id": 1, "name": "Ether", "network": "goerli-alpha", "networkId": "goerli-alpha", diff --git a/packages/extension/src/ui/features/accountEdit/AccountEditButtons.tsx b/packages/extension/src/ui/features/accountEdit/AccountEditButtons.tsx index e658d0ed4..eadb774b2 100644 --- a/packages/extension/src/ui/features/accountEdit/AccountEditButtons.tsx +++ b/packages/extension/src/ui/features/accountEdit/AccountEditButtons.tsx @@ -130,15 +130,7 @@ export const AccountEditButtons = () => { Change account implementation )} */} - {account && ( - { - navigate(routes.accountImplementation(account.address)) - }} - > - Account implementation - - )} + {canDeployAccount && ( Deploy account )} diff --git a/packages/extension/src/ui/features/accountEdit/AccountImplementationScreen.tsx b/packages/extension/src/ui/features/accountEdit/AccountImplementationScreen.tsx deleted file mode 100644 index af2e791ca..000000000 --- a/packages/extension/src/ui/features/accountEdit/AccountImplementationScreen.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - BarBackButton, - ButtonCell, - H6, - NavigationContainer, - P4, - icons, -} from "@argent/ui" -import { Box } from "@chakra-ui/react" -import { filter, partition } from "lodash-es" -import { FC, ReactNode } from "react" -import { useNavigate } from "react-router-dom" - -import { accountService } from "../../../shared/account/service" -import { ArgentAccountType } from "../../../shared/wallet.model" -import { accountsEqual } from "../../../shared/utils/accountsEqual" -import { AutoColumn } from "../../components/Column" -import { routes } from "../../routes" -import { selectedAccountView } from "../../views/account" -import { useView } from "../../views/implementation/react" -import { useRouteAccount } from "../shield/useRouteAccount" - -const { WalletIcon, PluginIcon, MulticallIcon, TickIcon } = icons - -interface Implementation { - id: ArgentAccountType - title: string - description: string - icon: ReactNode -} -const implementations: Implementation[] = [ - { - id: "standard", - title: "Default", - description: "The default Argent account implementation", - icon: , - }, - { - id: "plugin", - title: "Plugin", - description: "The Argent account implementation with plugin support", - icon: , - }, - { - id: "betterMulticall", - title: "Better multicall", - description: - "The Argent account implementation with better multicall support", - icon: , - }, -] - -interface ImplementationItemProps extends Implementation { - active: boolean - onClick?: () => void -} - -const ImplementationItem: FC = ({ - title, - description, - icon, - active, -}) => { - return ( - } - extendedDescription={description} - _hover={{ - cursor: "default", - }} - _active={{}} - disabled={!active} - > - {title} - - ) -} - -export const AccountImplementationScreen: FC = () => { - const selectedAccount = useView(selectedAccountView) - const account = useRouteAccount() - - if (!account || !selectedAccount) { - return <> - } - - const [[activeImplementation], otherImplementations] = partition( - filter(implementations, (i) => - Boolean(account.network.accountClassHash?.[i.id]), - ), - (i) => i.id === account.type, - ) - - return ( - } - > - - {activeImplementation && ( - <> -
- Current implementation -
- - - - - )} - {otherImplementations && ( - <> -
- Other implementations -
- - {otherImplementations.map((i) => ( - - ))} - - - )} -
-
- ) -} diff --git a/packages/extension/src/ui/features/accountEdit/ChangeAccountImplementationScreen.tsx b/packages/extension/src/ui/features/accountEdit/ChangeAccountImplementationScreen.tsx index bb802f437..dbf9d2379 100644 --- a/packages/extension/src/ui/features/accountEdit/ChangeAccountImplementationScreen.tsx +++ b/packages/extension/src/ui/features/accountEdit/ChangeAccountImplementationScreen.tsx @@ -8,52 +8,24 @@ import { } from "@argent/ui" import { Box } from "@chakra-ui/react" import { filter, partition } from "lodash-es" -import { FC, ReactNode } from "react" +import { FC } from "react" import { useNavigate } from "react-router-dom" import { accountService } from "../../../shared/account/service" -import { ArgentAccountType } from "../../../shared/wallet.model" import { accountsEqual } from "../../../shared/utils/accountsEqual" import { AutoColumn } from "../../components/Column" import { routes } from "../../routes" import { selectedAccountView } from "../../views/account" import { useView } from "../../views/implementation/react" import { useRouteAccount } from "../shield/useRouteAccount" +import { + CurrentImplementation, + Implementation, + ImplementationItemProps, + implementations, +} from "./Implementation" -const { WalletIcon, PluginIcon, MulticallIcon, TickIcon } = icons - -interface Implementation { - id: ArgentAccountType - title: string - description: string - icon: ReactNode -} -const implementations: Implementation[] = [ - { - id: "standard", - title: "Default", - description: "The default Argent account implementation", - icon: , - }, - { - id: "plugin", - title: "Plugin", - description: "The Argent account implementation with plugin support", - icon: , - }, - { - id: "betterMulticall", - title: "Better multicall", - description: - "The Argent account implementation with better multicall support", - icon: , - }, -] - -interface ImplementationItemProps extends Implementation { - active: boolean - onClick?: () => void -} +const { TickIcon } = icons const ImplementationItem: FC = ({ title, @@ -113,14 +85,11 @@ export const ChangeAccountImplementationScreen: FC = () => { > {activeImplementation && ( - <> -
- Current implementation -
- + - - + } + /> )} {otherImplementations && ( <> diff --git a/packages/extension/src/ui/features/accountEdit/Implementation.tsx b/packages/extension/src/ui/features/accountEdit/Implementation.tsx new file mode 100644 index 000000000..6ad3f8460 --- /dev/null +++ b/packages/extension/src/ui/features/accountEdit/Implementation.tsx @@ -0,0 +1,51 @@ +import { H6, icons } from "@argent/ui" +import { ArgentAccountType } from "../../../shared/wallet.model" +import { FC, ReactNode } from "react" +import { AutoColumn } from "../../components/Column" + +const { WalletIcon, PluginIcon, MulticallIcon } = icons + +export interface Implementation { + id: ArgentAccountType + title: string + description: string + icon: ReactNode +} + +export interface ImplementationItemProps extends Implementation { + active: boolean + onClick?: () => void +} + +export const implementations: Implementation[] = [ + { + id: "standard", + title: "Default", + description: "The default Argent account implementation", + icon: , + }, + { + id: "plugin", + title: "Plugin", + description: "The Argent account implementation with plugin support", + icon: , + }, + { + id: "betterMulticall", + title: "Better multicall", + description: + "The Argent account implementation with better multicall support", + icon: , + }, +] + +export const CurrentImplementation: FC<{ implementationItem: ReactNode }> = ({ + implementationItem, +}) => ( + <> +
+ Current implementation +
+ {implementationItem} + +) diff --git a/packages/extension/src/ui/features/accountNfts/AccountCollections.tsx b/packages/extension/src/ui/features/accountNfts/AccountCollections.tsx index a84b04576..b6abcd27e 100644 --- a/packages/extension/src/ui/features/accountNfts/AccountCollections.tsx +++ b/packages/extension/src/ui/features/accountNfts/AccountCollections.tsx @@ -31,7 +31,7 @@ export const AccountCollections: FC = ({ p="4" {...rest} > - {collections.map((collection: Collection) => ( + {collections.map((collection) => ( = ({ account, withHeader = true, customList, navigateToSend, ...rest }) => { - const isDefaultNetwork = useIsDefaultNetwork() const network = useCurrentNetwork() + const isSupported = nftService.isSupported(network) + const accountIdentifier = getAccountIdentifier(account) + const loadingState = useKeyValueStorage(nftWorkerStore, accountIdentifier) + + const lastUpdatedTimestamp = loadingState?.lastUpdatedTimestamp || 0 + const isInitialised = lastUpdatedTimestamp !== 0 const ownedCollections = useCollectionsByAccountAndNetwork( addressSchema.parse(account.address), account.networkId, ) - if (!isDefaultNetwork) { + if (!isSupported) { const displayName = network.name ?? "this network" return ( + + + ) + } + return ( <> {withHeader &&

NFTs

} diff --git a/packages/extension/src/ui/features/accountNfts/NftScreen.tsx b/packages/extension/src/ui/features/accountNfts/NftScreen.tsx index aac78c6ec..acd827a8c 100644 --- a/packages/extension/src/ui/features/accountNfts/NftScreen.tsx +++ b/packages/extension/src/ui/features/accountNfts/NftScreen.tsx @@ -16,7 +16,6 @@ import { AccordionItem, AccordionPanel, Box, - Flex, SimpleGrid, } from "@chakra-ui/react" import { FC, useCallback } from "react" @@ -83,7 +82,7 @@ export const NftScreen: FC = ({ />
-
+
{nft.name}
@@ -94,29 +93,13 @@ export const NftScreen: FC = ({ Description - {nft.description} + + {nft.description.length > 0 + ? nft.description + : nft.collection?.description} + - - - Best Offer - - {nft.best_bid_order?.payment_amount - ? bigDecimal.formatEther( - BigInt(nft.best_bid_order?.payment_amount), - ) - : "0"}{" "} - ETH - - { const navigate = useNavigate() const { contractAddress, tokenId } = useParams() const account = useView(selectedAccountView) - - const nft = useNft(addressSchema.parse(contractAddress), tokenId) + const network = useCurrentNetwork() + const { data: nft } = useRemoteNft(contractAddress, tokenId, network.id) if (!account || !contractAddress || !tokenId) { return diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx index 2fd88c432..36c40e609 100644 --- a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx +++ b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx @@ -1,6 +1,6 @@ import { CellStack, DapplandBanner, Empty, icons } from "@argent/ui" import dapplandBanner from "@argent/ui/assets/dapplandBannerBackground.png" -import { Flex, VStack } from "@chakra-ui/react" +import { Center, Flex, VStack } from "@chakra-ui/react" import { FC } from "react" import { routes } from "../../routes" @@ -17,7 +17,7 @@ import { TokenListItemVariant } from "./TokenListItem" import { UpgradeBanner } from "./UpgradeBanner" import { AccountDeprecatedBanner } from "./AccountDeprecatedBanner" -const { MultisigIcon } = icons +const { MultisigIcon, WalletIcon } = icons export interface AccountTokensProps { account: Account @@ -60,7 +60,10 @@ export const AccountTokens: FC = ({ - + {showTokensAndBanners ? ( @@ -103,9 +106,15 @@ export const AccountTokens: FC = ({ )} {showAddFundsBackdrop && ( } - title="Add funds to activate multisig" - /> + icon={} + title={"Add funds"} + > +
+ {multisig + ? "You will need some ETH to activate the multisig account" + : "You will need some ETH to use the account"} +
+
)} {!showAddFundsBackdrop && ( = ({ account }) => { +> = ({ account, hideSend = false }) => { const navigate = useNavigate() const { switcherNetworkId } = useAppState() const multisig = useMultisig(account) @@ -49,13 +50,20 @@ export const AccountTokensButtonsContainer: FC< const showSendButton = useMemo(() => { if ( showSaveRecoveryPhraseModal || - (multisig && (multisig.needsDeploy || !signerIsInMultisig)) + (multisig && (multisig.needsDeploy || !signerIsInMultisig)) || + hideSend ) { return false } return Boolean(sendToken) - }, [multisig, sendToken, signerIsInMultisig, showSaveRecoveryPhraseModal]) + }, [ + multisig, + sendToken, + signerIsInMultisig, + showSaveRecoveryPhraseModal, + hideSend, + ]) const showAddFundsButton = useMemo(() => { if (showSaveRecoveryPhraseModal || (multisig && !signerIsInMultisig)) { diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokensContainer.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokensContainer.tsx index b12a9e320..e9e0813f5 100644 --- a/packages/extension/src/ui/features/accountTokens/AccountTokensContainer.tsx +++ b/packages/extension/src/ui/features/accountTokens/AccountTokensContainer.tsx @@ -79,10 +79,6 @@ export const AccountTokensContainer: FC = ({ const hasEscape = accountHasEscape(account) const accountGuardianIsSelf = useAccountGuardianIsSelf(account) - const showAddFundsBackdrop = useMemo(() => { - return multisig?.needsDeploy && !!feeTokenBalance && feeTokenBalance <= 0n - }, [feeTokenBalance, multisig?.needsDeploy]) - const signerIsInMultisig = useIsSignerInMultisig(multisig) const isDeprecated = useIsDeprecatedTxV0(account) @@ -99,8 +95,17 @@ export const AccountTokensContainer: FC = ({ return !hasSavedRecoverySeedPhrase && isMainnet }, [hasSavedRecoverySeedPhrase, isMainnet]) + const showAddFundsBackdrop = useMemo(() => { + return ( + !showSaveRecoverySeedphraseBanner && + feeTokenBalance !== undefined && + feeTokenBalance <= 0n + ) + }, [feeTokenBalance, showSaveRecoverySeedphraseBanner]) + const showDapplandBanner = !hasSeenBanner && + !showAddFundsBackdrop && !showSaveRecoverySeedphraseBanner && !needsUpgrade && !hasPendingTransactions && diff --git a/packages/extension/src/ui/features/accountTokens/SaveRecoverySeedphraseBanner.tsx b/packages/extension/src/ui/features/accountTokens/SaveRecoverySeedphraseBanner.tsx index a8b914850..8eb9bdc72 100644 --- a/packages/extension/src/ui/features/accountTokens/SaveRecoverySeedphraseBanner.tsx +++ b/packages/extension/src/ui/features/accountTokens/SaveRecoverySeedphraseBanner.tsx @@ -41,6 +41,7 @@ export const SaveRecoverySeedphraseBanner: FC = () => { +
) } diff --git a/packages/extension/src/ui/features/funding/FundingQrCodeScreenContainer.tsx b/packages/extension/src/ui/features/funding/FundingQrCodeScreenContainer.tsx new file mode 100644 index 000000000..1d498bce6 --- /dev/null +++ b/packages/extension/src/ui/features/funding/FundingQrCodeScreenContainer.tsx @@ -0,0 +1,26 @@ +import { FC } from "react" +import { Navigate, useNavigate } from "react-router-dom" + +import { routes } from "../../routes" +import { selectedAccountView } from "../../views/account" +import { useView } from "../../views/implementation/react" +import { FundingQrCodeScreen } from "./FundingQrCodeScreen" + +export const FundingQrCodeScreenContainer: FC = () => { + const navigate = useNavigate() + const account = useView(selectedAccountView) + + const onClose = () => navigate(routes.accountTokens()) + + if (!account) { + return + } + + return ( + + ) +} diff --git a/packages/extension/src/ui/features/lock/Greetings.tsx b/packages/extension/src/ui/features/lock/Greetings.tsx index 2bc76a7db..ad18fb508 100644 --- a/packages/extension/src/ui/features/lock/Greetings.tsx +++ b/packages/extension/src/ui/features/lock/Greetings.tsx @@ -1,25 +1,8 @@ -import { FC, useEffect, useState } from "react" -import styled, { keyframes } from "styled-components" +import { FC, useEffect, useState, PropsWithChildren } from "react" +import { Box, keyframes } from "@chakra-ui/react" +import { P3 } from "@argent/ui" -import { H2 } from "../../theme/Typography" - -export const GreetingsWrapper = styled.div` - position: relative; - - margin: 16px 0px; - height: 41px; - width: 100%; -` - -const Text = styled(H2)` - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -` - -const FadeInAndOut = keyframes` +const fadeInAndOut = keyframes` 0% { opacity: 0; } @@ -33,9 +16,20 @@ const FadeInAndOut = keyframes` opacity: 0; } ` -const FadeInAndOutText = styled(Text)` - animation: ${FadeInAndOut} 3s ease-in-out forwards; -` + +const FadeInAndOutText: FC<{ duration?: string } & PropsWithChildren> = ({ + duration = "3s", + ...props +}) => { + return ( + + ) +} const useCarousel = (greetings: string[], delay = 3000): number => { const [index, setState] = useState(0) @@ -61,8 +55,8 @@ interface GreetingsProps { export const Greetings: FC = ({ greetings, ...props }) => { const index = useCarousel(greetings) return ( - + {greetings[index]} - + ) } diff --git a/packages/extension/src/ui/features/lock/LockScreen.tsx b/packages/extension/src/ui/features/lock/LockScreen.tsx index bbd14f3b5..e6894c995 100644 --- a/packages/extension/src/ui/features/lock/LockScreen.tsx +++ b/packages/extension/src/ui/features/lock/LockScreen.tsx @@ -20,7 +20,7 @@ export const LockScreen: FC = () => { setIsLoading(true) try { await sessionService.startSession(password) - unlockedExtensionTracking() + await unlockedExtensionTracking() const target = await recover() navigate(target, { replace: true }) return true diff --git a/packages/extension/src/ui/features/onboarding/OnboardingRestoreBackupScreenContainer.tsx b/packages/extension/src/ui/features/onboarding/OnboardingRestoreBackupScreenContainer.tsx index b1f9f73c2..1867aad9a 100644 --- a/packages/extension/src/ui/features/onboarding/OnboardingRestoreBackupScreenContainer.tsx +++ b/packages/extension/src/ui/features/onboarding/OnboardingRestoreBackupScreenContainer.tsx @@ -6,7 +6,7 @@ import { useNavigate } from "react-router-dom" import { useAppState } from "../../app.state" import { routes } from "../../routes" import { fileToString } from "../../services/files" -import { recoveryService } from "../../services/recovery" +import { clientRecoveryService } from "../../services/recovery" import { useOnboardingScreen } from "./hooks/useOnboardingScreen" import { OnboardingRestoreBackupScreen } from "./OnboardingRestoreBackupScreen" @@ -18,7 +18,7 @@ export const OnboardingRestoreBackupScreenContainer: FC = () => { async (acceptedFile: File) => { try { const data = await fileToString(acceptedFile) - await recoveryService.byBackup(data) + await clientRecoveryService.byBackup(data) navigate(routes.onboardingFinish.path, { replace: true }) } catch (err: any) { const error = `${err}` diff --git a/packages/extension/src/ui/features/onboarding/OnboardingRestorePasswordScreenContainer.tsx b/packages/extension/src/ui/features/onboarding/OnboardingRestorePasswordScreenContainer.tsx index 496f60b88..22292ce9c 100644 --- a/packages/extension/src/ui/features/onboarding/OnboardingRestorePasswordScreenContainer.tsx +++ b/packages/extension/src/ui/features/onboarding/OnboardingRestorePasswordScreenContainer.tsx @@ -3,7 +3,7 @@ import { FC } from "react" import { useNavigate } from "react-router-dom" import { routes } from "../../routes" -import { recoveryService } from "../../services/recovery" +import { clientRecoveryService } from "../../services/recovery" import { useSeedRecovery, validateAndSetPassword, @@ -40,8 +40,8 @@ export const OnboardingRestorePasswordScreenContainer: FC = () => { throw new RecoveryError({ code: "SEED_RECOVERY_INCOMPLETE" }) } - // should throw right away, no return value needed; to be replaced with a service which uses the new transport - await recoveryService.bySeedPhrase(state.seedPhrase, state.password) + // should throw right away, no return value needed; + await clientRecoveryService.bySeedPhrase(state.seedPhrase, state.password) setHasSavedRecoverySeedphrase(true) // as the user recovered their seed, we can assume they have a backup navigate(routes.onboardingFinish.path, { replace: true }) @@ -65,7 +65,6 @@ export const OnboardingRestorePasswordScreenContainer: FC = () => { status: "error", duration: 3000, }) - throw err } } diff --git a/packages/extension/src/ui/features/recovery/SeedPhrase.tsx b/packages/extension/src/ui/features/recovery/SeedPhrase.tsx index cfa4ec860..ac5ca8f30 100644 --- a/packages/extension/src/ui/features/recovery/SeedPhrase.tsx +++ b/packages/extension/src/ui/features/recovery/SeedPhrase.tsx @@ -1,10 +1,10 @@ -import { wordlists } from "ethers" import { FC } from "react" import { LoadingSeedWordBadge } from "./ui/LoadingSeedWordBadge" import { SeedPhraseGrid } from "./ui/SeedPhraseGrid" import { SeedWordBadge } from "./ui/SeedWordBadge" import { SeedWordBadgeNumber } from "./ui/SeedWordBadgeNumber" +import { splitPhrase } from "./phraseUtils" interface SeedPhraseProps { seedPhrase?: string @@ -13,7 +13,7 @@ interface SeedPhraseProps { export const SeedPhrase: FC = ({ seedPhrase }) => seedPhrase ? ( - {wordlists.en.split(seedPhrase).map((word, index) => ( + {splitPhrase(seedPhrase).map((word, index) => ( {index + 1} {word} diff --git a/packages/extension/src/ui/features/recovery/phraseUtils.ts b/packages/extension/src/ui/features/recovery/phraseUtils.ts new file mode 100644 index 000000000..92595973f --- /dev/null +++ b/packages/extension/src/ui/features/recovery/phraseUtils.ts @@ -0,0 +1,3 @@ +export function splitPhrase(phrase: string): string[] { + return phrase.toLowerCase().split(/\s+/g) +} diff --git a/packages/extension/src/ui/features/recovery/seedRecovery.state.ts b/packages/extension/src/ui/features/recovery/seedRecovery.state.ts index c2f983f7e..b04cc7620 100644 --- a/packages/extension/src/ui/features/recovery/seedRecovery.state.ts +++ b/packages/extension/src/ui/features/recovery/seedRecovery.state.ts @@ -1,6 +1,8 @@ -import { ethers, wordlists } from "ethers" import { z } from "zod" import { create } from "zustand" +import { splitPhrase } from "./phraseUtils" +import { validateMnemonic } from "@scure/bip39" +import { wordlist } from "@scure/bip39/wordlists/english" interface State { seedPhrase?: string @@ -10,24 +12,15 @@ interface State { export const useSeedRecovery = create()(() => ({})) export const validateSeedPhrase = (seedPhrase: string): boolean => { - const words = wordlists.en.split(seedPhrase.trim()) + const words = splitPhrase(seedPhrase.trim()) // check seed phrase has correct number of words if (words.length !== 12) { return false } - // check every word is in the wordlist - if (!words.every((word) => wordlists.en.getWordIndex(word) >= 0)) { - return false - } - // check if seedphrase is valid with HDNode - try { - ethers.utils.HDNode.fromMnemonic(seedPhrase) - } catch { - return false - } + const isValid = validateMnemonic(seedPhrase.trim(), wordlist) - return true + return isValid } export const passwordSchema = z diff --git a/packages/extension/src/ui/features/send/SendAmountAndAssetNftScreenContainer.tsx b/packages/extension/src/ui/features/send/SendAmountAndAssetNftScreenContainer.tsx index ec52f4489..b9982b634 100644 --- a/packages/extension/src/ui/features/send/SendAmountAndAssetNftScreenContainer.tsx +++ b/packages/extension/src/ui/features/send/SendAmountAndAssetNftScreenContainer.tsx @@ -1,4 +1,4 @@ -import { addressSchema, isStarknetId } from "@argent/shared" +import { addressSchema } from "@argent/shared" import { FC, useCallback } from "react" import { useNavigate } from "react-router-dom" @@ -7,46 +7,34 @@ import { nftService } from "../../services/nfts" import { selectedAccountView } from "../../views/account" import { useView } from "../../views/implementation/react" import { useNft } from "../accountNfts/nfts.state" +import { useCurrentNetwork } from "../networks/hooks/useCurrentNetwork" import { NftInput } from "./NftInput" import { SendAmountAndAssetScreen, SendAmountAndAssetScreenProps, } from "./SendAmountAndAssetScreen" -import { getAddressFromStarkName } from "../../services/useStarknetId" -import { useCurrentNetwork } from "../networks/hooks/useCurrentNetwork" export const SendAmountAndAssetNftScreenContainer: FC< SendAmountAndAssetScreenProps > = ({ onCancel, returnTo, ...rest }) => { const navigate = useNavigate() const account = useView(selectedAccountView) - const { id: currentNetworkId } = useCurrentNetwork() + const network = useCurrentNetwork() const { recipientAddress, tokenAddress, tokenId } = rest const nft = useNft(addressSchema.parse(tokenAddress), tokenId) const onSubmit = useCallback(async () => { if (account && nft && recipientAddress && tokenAddress && tokenId) { - let recipient = recipientAddress - if (isStarknetId(recipient)) { - recipient = await getAddressFromStarkName(recipient, currentNetworkId) - } await nftService.transferNft( tokenAddress, account.address, - recipient, + recipientAddress, tokenId, nft.spec ?? "", + network, ) } onCancel() - }, [ - account, - currentNetworkId, - nft, - onCancel, - recipientAddress, - tokenAddress, - tokenId, - ]) + }, [account, network, nft, onCancel, recipientAddress, tokenAddress, tokenId]) const onTokenClick = useCallback(() => { navigate( diff --git a/packages/extension/src/ui/features/send/SendAmountAndAssetTokenScreenContainer.tsx b/packages/extension/src/ui/features/send/SendAmountAndAssetTokenScreenContainer.tsx index d89292dbc..9235783d6 100644 --- a/packages/extension/src/ui/features/send/SendAmountAndAssetTokenScreenContainer.tsx +++ b/packages/extension/src/ui/features/send/SendAmountAndAssetTokenScreenContainer.tsx @@ -1,12 +1,13 @@ -import { isStarknetId, parseAmount } from "@argent/shared" +import { parseAddress, parseAmount } from "@argent/shared" import { FieldError } from "@argent/ui" import { zodResolver } from "@hookform/resolvers/zod" -import { formatUnits } from "ethers/lib/utils" +import { formatUnits } from "ethers" import { FC, useCallback, useMemo } from "react" import { SubmitHandler, useForm } from "react-hook-form" import { useNavigate } from "react-router-dom" import { z } from "zod" +import { getMulticallForNetwork } from "../../../shared/multicall" import { prettifyCurrencyValue, prettifyTokenBalance, @@ -33,7 +34,6 @@ import { } from "./SendAmountAndAssetScreen" import { TokenAmountInput } from "./TokenAmountInput" import { genericErrorSchema } from "../actions/feeEstimation/feeError" -import { getAddressFromStarkName } from "../../services/useStarknetId" import { useCurrentNetwork } from "../networks/hooks/useCurrentNetwork" import { useLiveTokenBalanceForAccount } from "../accountTokens/useLiveTokenBalanceForAccount" import { Spinner } from "@chakra-ui/react" @@ -85,7 +85,7 @@ interface GuardedSendAmountAndAssetTokenScreenContainerProps extends SendAmountAndAssetScreenProps { token: Token balance?: bigint - tokenBalanceLoading?: boolean + tokenBalanceLoading: boolean account: WalletAccount } @@ -117,7 +117,8 @@ const GuardedSendAmountAndAssetTokenScreenContainer: FC< const inputAmount = watch().amount const { ref, onChange, ...amountInputRest } = register("amount") const inputRef = useAutoFocusInputRef() - const { id: currentNetworkId } = useCurrentNetwork() + + const network = useCurrentNetwork() const feeToken = useNetworkFeeToken(account?.networkId) @@ -131,25 +132,27 @@ const GuardedSendAmountAndAssetTokenScreenContainer: FC< const onSubmit = useCallback(async () => { if (token && recipientAddress && inputAmount) { - let recipient = recipientAddress + const to = await parseAddress({ address: token.address }) - if (isStarknetId(recipient)) { - recipient = await getAddressFromStarkName(recipient, currentNetworkId) - } + const recipient = await parseAddress({ + address: recipientAddress, + networkId: network.id, + multicallProvider: getMulticallForNetwork(network), + }) await sendTransaction({ - to: token.address, + to, method: "transfer", calldata: { recipient, amount: getUint256CalldataFromBN( - parseAmount(inputAmount, token.decimals).toString(), + parseAmount(inputAmount, token.decimals).value, ), }, }) } onCancel() - }, [currentNetworkId, inputAmount, onCancel, recipientAddress, token]) + }, [network, inputAmount, onCancel, recipientAddress, token]) const onMaxClick = useCallback(() => { if (balance && maxFee) { @@ -173,11 +176,12 @@ const GuardedSendAmountAndAssetTokenScreenContainer: FC< } }, [balance, maxFee, token.decimals, account?.networkId, setValue]) - const parsedInputAmount = inputAmount - ? parseAmount(inputAmount, token.decimals) - : parseAmount("0", token.decimals) + const parsedInputAmount = parseAmount( + inputAmount || "0", + token.decimals, + ).value - const parsedTokenBalance = balance || parseAmount("0", token.decimals) + const parsedTokenBalance = balance || 0n const isInputAmountGtBalance = useMemo(() => { return ( @@ -233,6 +237,7 @@ const GuardedSendAmountAndAssetTokenScreenContainer: FC< if (!maxFeeError) { return prettifyCurrencyValue(currencyValue) } + const genericError = genericErrorSchema.safeParse(maxFeeError) if (genericError.success) { return {genericError.data.message} diff --git a/packages/extension/src/ui/features/send/SendRecipientScreen.tsx b/packages/extension/src/ui/features/send/SendRecipientScreen.tsx index 267f4c399..04deb651b 100644 --- a/packages/extension/src/ui/features/send/SendRecipientScreen.tsx +++ b/packages/extension/src/ui/features/send/SendRecipientScreen.tsx @@ -113,6 +113,7 @@ export const SendRecipientScreen: FC = ({ inputRef.current = e }} px={10} + pt={3} autoComplete="off" placeholder="Address (0x) or Starknet ID" isInvalid={hasQueryError} @@ -184,7 +185,7 @@ export const SendRecipientScreen: FC = ({ {hasFilteredAccounts ? ( - + {filteredAccounts.map((account) => ( = ({ if (!hasContacts) { return ( } title={`No saved contacts`}> - } onClick={onAddContact}> + } onClick={onAddContact}> New contact diff --git a/packages/extension/src/ui/features/settings/BeforeYouContinueScreen.tsx b/packages/extension/src/ui/features/settings/BeforeYouContinueScreen.tsx index 37b5b4330..d91e4aa98 100644 --- a/packages/extension/src/ui/features/settings/BeforeYouContinueScreen.tsx +++ b/packages/extension/src/ui/features/settings/BeforeYouContinueScreen.tsx @@ -1,4 +1,4 @@ -import { P4, icons, Button, NavigationContainer, H3 } from "@argent/ui" +import { P3, icons, Button, NavigationContainer, H3 } from "@argent/ui" import { Box, Circle, Flex } from "@chakra-ui/react" import { useNavigate } from "react-router-dom" import { routes } from "../../routes" @@ -37,10 +37,10 @@ export const BeforeYouContinueScreen = () => { Before you continue... - + Please save your recovery phrase. This is the only way you will be able to recover your Argent X accounts - +
- - - Privacy statement - - - - Version: v{process.env.VERSION} - + {privacyStatement && ( + + + Privacy statement + + + )} + Version: v{process.env.VERSION} ) diff --git a/packages/extension/src/ui/features/shield/WithArgentShieldVerified.tsx b/packages/extension/src/ui/features/shield/WithArgentShieldVerified.tsx index b1a6adc83..412202370 100644 --- a/packages/extension/src/ui/features/shield/WithArgentShieldVerified.tsx +++ b/packages/extension/src/ui/features/shield/WithArgentShieldVerified.tsx @@ -1,4 +1,4 @@ -import { AlertDialog } from "@argent/ui" +import { AlertDialog, useToast } from "@argent/ui" import { Skeleton, VStack } from "@chakra-ui/react" import { useAtom } from "jotai" import { isArray } from "lodash-es" @@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, + useRef, useState, } from "react" import { Call } from "starknet" @@ -29,6 +30,7 @@ import { changeGuardianCallDataToType, } from "./usePendingChangingGuardian" import { useShieldVerifiedEmail } from "./useShieldVerifiedEmail" +import { IS_DEV } from "../../../shared/utils/dev" enum ArgentShieldVerifiedState { INITIALISING = "INITIALISING", @@ -37,7 +39,6 @@ enum ArgentShieldVerifiedState { VERIFY_OTP = "VERIFY_OTP", VERIFIED = "VERIFIED", USER_ABORTED = "USER_ABORTED", - ERROR = "ERROR", } /** @@ -66,6 +67,10 @@ const WithArgentShieldVerifiedScreen: FC = ({ children, transactions, }) => { + // TODO: refactor all this logic into service and pass only clean state into React + // this flag prevents the expiry flow re-triggering on internal state change + const isTokenExpiryFlow = useRef(false) + const [shieldState, setShieldState] = useAtom(shieldStateAtom) const { unverifiedEmail } = shieldState const account = useView(selectedAccountView) @@ -73,7 +78,7 @@ const WithArgentShieldVerifiedScreen: FC = ({ const verifiedEmail = useShieldVerifiedEmail() const hasGuardian = Boolean(account?.guardian) const accountGuardianIsSelf = useAccountGuardianIsSelf(account) - const [error, setError] = useState() + const toast = useToast() const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false) const [state, setState] = useState( @@ -182,9 +187,16 @@ const WithArgentShieldVerifiedScreen: FC = ({ ? await getVerifiedEmailIsExpiredForRemoval() : await argentAccountService.isTokenExpired() if (isTokenExpired) { + // this ref guards against this flow re-running and sending > 1 emails + if (isTokenExpiryFlow.current) { + return + } + isTokenExpiryFlow.current = true // need to re-verify existing email try { // reflect unverifiedEmail in overall state + // the isTokenExpiryFlow ref guards against this state change + // triggering this flow again before completion setShieldState({ unverifiedEmail: verifiedEmail }) // need to use immediate local copy too const _unverifiedEmail = verifiedEmail @@ -192,10 +204,15 @@ const WithArgentShieldVerifiedScreen: FC = ({ await argentAccountService.requestEmail(_unverifiedEmail) onEmailRequested(_unverifiedEmail) } catch (error) { - console.error(coerceErrorToString(error)) - - setError(coerceErrorToString(error)) - setState(ArgentShieldVerifiedState.ERROR) + // user can navigate back to re-enter their email + IS_DEV && console.warn(coerceErrorToString(error)) + toast({ + title: "Unable to verify email", + status: "error", + duration: 3000, + }) + } finally { + isTokenExpiryFlow.current = false } } else { setState(ArgentShieldVerifiedState.VERIFIED) @@ -211,6 +228,7 @@ const WithArgentShieldVerifiedScreen: FC = ({ onEmailRequested, setShieldState, state, + toast, unverifiedEmail, verifiedEmail, ]) @@ -258,8 +276,7 @@ const WithArgentShieldVerifiedScreen: FC = ({ case ArgentShieldVerifiedState.VERIFIED: case ArgentShieldVerifiedState.USER_ABORTED: return <>{children} - - case ArgentShieldVerifiedState.ERROR: - throw new Error(error) // should be caught by error boundary + default: + state satisfies never } } diff --git a/packages/extension/src/ui/features/swap/ui/CurrencyValue.tsx b/packages/extension/src/ui/features/swap/ui/CurrencyValue.tsx index 2c1a2acd9..d48e0dbb2 100644 --- a/packages/extension/src/ui/features/swap/ui/CurrencyValue.tsx +++ b/packages/extension/src/ui/features/swap/ui/CurrencyValue.tsx @@ -16,7 +16,7 @@ interface CurrencyValueProps { const CurrencyValue: FC = ({ amount, approx, token }) => { const currencyValue = useTokenAmountToCurrencyValue( token as Token, - amount ? bigDecimal.parseUnits(amount, token.decimals) : 0, + amount ? bigDecimal.parseUnits(amount, token.decimals).value : 0, ) return ( diff --git a/packages/extension/src/ui/features/swap/ui/TokenPrice.tsx b/packages/extension/src/ui/features/swap/ui/TokenPrice.tsx index 9acb0c9be..16e8b2638 100644 --- a/packages/extension/src/ui/features/swap/ui/TokenPrice.tsx +++ b/packages/extension/src/ui/features/swap/ui/TokenPrice.tsx @@ -31,7 +31,7 @@ const TokenPrice: FC = ({ currency, onClick }) => { const currencyValue = useTokenAmountToCurrencyValue( token, - bigDecimal.parseUnits("1", token?.decimals ?? 18), + bigDecimal.parseUnits("1", token?.decimals ?? 18).value, ) const priceDetails = useTokenPriceDetails(token) diff --git a/packages/extension/src/ui/routes.ts b/packages/extension/src/ui/routes.ts index 8e94d79fa..0da3385e2 100644 --- a/packages/extension/src/ui/routes.ts +++ b/packages/extension/src/ui/routes.ts @@ -259,6 +259,7 @@ export const routes = { ), userReview: route("/user-review"), userReviewFeedback: route("/user-review/feedback"), + backgroundError: route("/background-error"), error: route("/error"), ledgerEntry: route("/ledger/start"), ledgerSelect: route("/ledger/select"), diff --git a/packages/extension/src/ui/services/analytics.ts b/packages/extension/src/ui/services/analytics.ts index 1709965d2..48a0f094c 100644 --- a/packages/extension/src/ui/services/analytics.ts +++ b/packages/extension/src/ui/services/analytics.ts @@ -51,7 +51,7 @@ export const useTimeSpentWithSuccessTracking = ( } didTrack.current = true const resolvedArgs = isFunction(args) ? await args() : args - analytics.track(event, { + void analytics.track(event, { ...resolvedArgs, success, timeSpent, @@ -76,7 +76,7 @@ export const useTimeSpentWithSuccessTracking = ( const timeSpent = new Date().getTime() - startedAt.current /** don't track failure unless window was open > 500ms */ if (timeSpent > 500) { - trackFailure() + void trackFailure() } } } @@ -96,30 +96,31 @@ const N_24_HOURS = 24 * 60 * 60 * 1000 const N_1_WEEK = 7 * N_24_HOURS const N_1_MONTH = 4 * N_1_WEEK -function openedExtensionTodayTracking() { +async function openedExtensionTodayTracking() { try { - if (Date.now() - activeStore.getState().lastOpened > N_24_HOURS) { - activeStore.getState().update("lastOpened") - analytics.track("openedExtensionToday") + const lastOpened = await activeStore.get("lastOpened") + if (Date.now() - lastOpened > N_24_HOURS) { + await activeStore.update("lastOpened") + void analytics.track("openedExtensionToday") } } catch (e) { // nothing of this should be blocking } } -export function unlockedExtensionTracking() { +export async function unlockedExtensionTracking() { try { - const { lastUnlocked } = activeStore.getState() + const lastUnlocked = await activeStore.get("lastUnlocked") // track once every 24h if (Date.now() - lastUnlocked > N_24_HOURS) { - activeStore.getState().update("lastUnlocked") - analytics.track("unlockedExtensionToday") + await activeStore.update("lastUnlocked") + void analytics.track("unlockedExtensionToday") if (Date.now() - lastUnlocked > N_1_WEEK) { - analytics.track("unlockedExtensionWeekly") + void analytics.track("unlockedExtensionWeekly") } if (Date.now() - lastUnlocked > N_1_MONTH) { - analytics.track("unlockedExtensionMonthly") + void analytics.track("unlockedExtensionMonthly") } } } catch (e) { @@ -127,19 +128,22 @@ export function unlockedExtensionTracking() { } } -export function sessionStartTracking() { +async function sessionStartTracking() { try { + const [lastSession, lastClosed] = await Promise.all([ + activeStore.get("lastSession"), + activeStore.get("lastClosed"), + ]) // track once every 5 minutes - if (Date.now() - activeStore.getState().lastSession > N_5_MINUTES) { - analytics.track("sessionStart") + if (Date.now() - lastSession > N_5_MINUTES) { + await analytics.track("sessionStart") // ...and also if extension was closed for 5 minutes - if (Date.now() - activeStore.getState().lastClosed > N_5_MINUTES) { - const length = - activeStore.getState().lastClosed - activeStore.getState().lastSession - analytics.track("sessionEnded", { length }) + if (Date.now() - lastClosed > N_5_MINUTES) { + const length = lastClosed - lastSession + await analytics.track("sessionEnded", { length }) } } - activeStore.getState().update("lastSession") + await activeStore.update("lastSession") } catch (e) { // nothing of this should be blocking } @@ -155,8 +159,8 @@ export function trackAddFundsService( export const useTracking = () => { useEffect(() => { // as React in strict mode renders every component twice, this will be called 2x in development. This is not the case in production. - sessionStartTracking() - openedExtensionTodayTracking() + void sessionStartTracking() + void openedExtensionTodayTracking() return () => { /** * NOTE: any code here may run in dev but will not be triggered in a production build diff --git a/packages/extension/src/ui/services/knownDapps/index.ts b/packages/extension/src/ui/services/knownDapps/index.ts index 43cda03ac..42ecec38d 100644 --- a/packages/extension/src/ui/services/knownDapps/index.ts +++ b/packages/extension/src/ui/services/knownDapps/index.ts @@ -29,7 +29,7 @@ export function useDappFromKnownDappsByContractAddress( return useMemo( () => knownDapps?.find((knownDapp) => - knownDapp.contracts.some( + knownDapp.contracts?.some( (contract) => contract.address === contractAddress && contract.chain === "starknet", diff --git a/packages/extension/src/ui/services/messaging/trpc.ts b/packages/extension/src/ui/services/messaging/trpc.ts index 86bcec84b..2687b2ac0 100644 --- a/packages/extension/src/ui/services/messaging/trpc.ts +++ b/packages/extension/src/ui/services/messaging/trpc.ts @@ -1,10 +1,52 @@ import { createTRPCProxyClient } from "@trpc/client" import { chromeLink } from "trpc-browser/link" +import { autoReconnect } from "trpc-browser/shared/chrome" import browser from "webextension-polyfill" import type { AppRouter } from "../../../background/__new/router" -const port = browser.runtime.connect() -export const messageClient = createTRPCProxyClient({ - links: [chromeLink({ port })], +const initalPort = browser.runtime.connect() +let _messageClient = createTRPCProxyClient({ + links: [chromeLink({ port: initalPort })], }) + +// only if in UI +if (typeof window !== "undefined") { + // setup auto-reconnect + void autoReconnect( + initalPort, + () => browser.runtime.connect(), + (newPort) => { + console.log("Reconnecting to new port", newPort.name) + _messageClient = createTRPCProxyClient({ + links: [chromeLink({ port: newPort })], + }) + }, + ) +} + +const getProxyHandler = (path: string[] = []): ProxyHandler => ({ + get: function (target: any, prop: string) { + const isTrpcMethod = ["mutate", "query", "subscription"].includes(prop) + const canBeProxied = + (typeof target[prop] === "object" && target[prop] !== null) || // objects other than null + typeof target[prop] === "function" // and functions + + if (isTrpcMethod || !canBeProxied) { + // if it's a trpc method or can't be proxied, return the value from the mutable instance + return [...path, prop].reduce( + (acc, curr) => acc[curr], + _messageClient as any, + ) + } + + // otherwise, return a new proxy of the nested property, and run this function recursively + return new Proxy(target[prop], getProxyHandler([...path, prop])) + }, +}) + +// Proxy to export a stable reference +export const messageClient: typeof _messageClient = new Proxy( + _messageClient, + getProxyHandler(), +) diff --git a/packages/extension/src/ui/services/nfts/implementation.ts b/packages/extension/src/ui/services/nfts/implementation.ts index 1a55d9553..df758ee44 100644 --- a/packages/extension/src/ui/services/nfts/implementation.ts +++ b/packages/extension/src/ui/services/nfts/implementation.ts @@ -5,6 +5,7 @@ import { NftItem, PaginatedItems, getUint256CalldataFromBN, + parseAddress, } from "@argent/shared" import { differenceWith, groupBy, isEqual } from "lodash-es" import { CallData, constants, num, shortString } from "starknet" @@ -18,8 +19,10 @@ import { import { AllowArray } from "../../../shared/storage/types" import { isEqualAddress } from "../addresses" import { messageClient } from "../messaging/trpc" -import { INFTService } from "./interface" +import { INFTService } from "../../../shared/nft/interface" import { networkService } from "../../../shared/network/service" +import { Network } from "../../../shared/network" +import { getMulticallForNetwork } from "../../../shared/multicall" const chainIdToPandoraNetwork = (chainId: string): "mainnet" | "goerli" => { const encodedChainId = num.isHex(chainId) @@ -31,9 +34,8 @@ const chainIdToPandoraNetwork = (chainId: string): "mainnet" | "goerli" => { return "mainnet" case constants.StarknetChainId.SN_GOERLI: return "goerli" - default: - throw new Error(`Unsupported network ${chainId}`) } + throw new Error(`Unsupported network ${chainId}`) } export class NFTService implements INFTService { @@ -45,6 +47,15 @@ export class NFTService implements INFTService { private readonly argentNftService: ArgentBackendNftService, ) {} + isSupported(network: Network) { + try { + chainIdToPandoraNetwork(network.chainId) // throws if not supported + return true + } catch { + return false + } + } + async getAsset( chain: string, networkId: string, @@ -82,7 +93,7 @@ export class NFTService implements INFTService { networkId, })) } catch (e) { - throw new Error((e as Error).message) + throw new Error(`An error occured ${e}`) } } @@ -200,15 +211,42 @@ export class NFTService implements INFTService { recipient: string, tokenId: string, schema: string, + network: Network, ) { + let parsedRecipient = null + let parsedContractAddress = null + let parsedAccountAddress = null + + try { + parsedContractAddress = await parseAddress({ + address: contractAddress, + networkId: network.id, + multicallProvider: getMulticallForNetwork(network), + }) + + parsedAccountAddress = await parseAddress({ + address: accountAddress, + networkId: network.id, + multicallProvider: getMulticallForNetwork(network), + }) + + parsedRecipient = await parseAddress({ + address: recipient, + networkId: network.id, + multicallProvider: getMulticallForNetwork(network), + }) + } catch (e) { + throw new Error(`An error occured ${e}`) + } + let compiledCalldata = CallData.toCalldata({ - from_: accountAddress, - to: recipient, + from_: parsedAccountAddress, + to: parsedRecipient, tokenId: getUint256CalldataFromBN(tokenId), // OZ specs need a uint256 as tokenId }) const transactions = { - contractAddress, + contractAddress: parsedContractAddress, entrypoint: "transferFrom", calldata: compiledCalldata, } @@ -217,8 +255,8 @@ export class NFTService implements INFTService { transactions.entrypoint = "safeTransferFrom" compiledCalldata = CallData.toCalldata({ - from_: accountAddress, - to: recipient, + from_: parsedAccountAddress, + to: parsedRecipient, tokenId: getUint256CalldataFromBN(tokenId), amount: getUint256CalldataFromBN(1), data_len: "0", diff --git a/packages/extension/src/ui/services/nfts/test/index.test.ts b/packages/extension/src/ui/services/nfts/test/index.test.ts index e47a031e7..eb006047b 100644 --- a/packages/extension/src/ui/services/nfts/test/index.test.ts +++ b/packages/extension/src/ui/services/nfts/test/index.test.ts @@ -17,6 +17,7 @@ import { invalidJson, validJson, } from "./nft.mock" +import { constants } from "starknet" const messageClientMock = { transfer: { @@ -173,13 +174,112 @@ describe("NFTService", () => { const txHash = await testClass.transferNft( "0x0798e884450c19e072d6620fefdbeb7387d0453d3fd51d95f5ace1f17633d88b", - "0x123", - "0x456", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", "854406733492", "ERC721", + { + name: "testnet", + id: "goerli-alpha", + chainId: constants.StarknetChainId.SN_GOERLI, + sequencerUrl: "https://alpha4.starknet.io", + }, ) expect(txHash).toEqual("0x999") }) + + it("should throw error for contract address", async () => { + const result = await testClass.getAssets( + "starknet", + "goerli-alpha", + "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25", + ) + + expect(result).toEqual(expectedValidRes) + await testClass.upsert(result, "0x123", "goerli-alpha") + + messageClientMock.transfer.send.mutate = vi + .fn() + .mockResolvedValue("0x999") + + await expect( + testClass.transferNft( + "0x0798e884450c19e072d6620fefdbeb738", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", + "854406733492", + "ERC721", + { + name: "testnet", + id: "goerli-alpha", + chainId: constants.StarknetChainId.SN_GOERLI, + sequencerUrl: "https://alpha4.starknet.io", + }, + ), + ).rejects.toThrow(`Invalid address`) + }) + + it("should throw error for account address", async () => { + const result = await testClass.getAssets( + "starknet", + "goerli-alpha", + "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25", + ) + + expect(result).toEqual(expectedValidRes) + await testClass.upsert(result, "0x123", "goerli-alpha") + + messageClientMock.transfer.send.mutate = vi + .fn() + .mockResolvedValue("0x999") + + await expect( + testClass.transferNft( + "0x0798e884450c19e072d6620fefdbeb7387d0453d3fd51d95f5ace1f17633d88b", + "0x070C58360B2493D3Ab8C42f8f66Df", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", + "854406733492", + "ERC721", + { + name: "testnet", + id: "goerli-alpha", + chainId: constants.StarknetChainId.SN_GOERLI, + sequencerUrl: "https://alpha4.starknet.io", + }, + ), + ).rejects.toThrow(`Invalid address`) + }) + + it("should throw error for invalid recipient", async () => { + const result = await testClass.getAssets( + "starknet", + "goerli-alpha", + "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25", + ) + + expect(result).toEqual(expectedValidRes) + await testClass.upsert(result, "0x123", "goerli-alpha") + + messageClientMock.transfer.send.mutate = vi + .fn() + .mockResolvedValue("0x999") + + await expect( + testClass.transferNft( + "0x0798e884450c19e072d6620fefdbeb7387d0453d3fd51d95f5ace1f17633d88b", + "0x070C58360B2493D3Ab8C42f8f66Df15fcFc3B77E76bAc1C690E68819B5511911", + "0x070C58360B2493D3Ab8", + "854406733492", + "ERC721", + { + name: "testnet", + id: "goerli-alpha", + chainId: constants.StarknetChainId.SN_GOERLI, + sequencerUrl: "https://alpha4.starknet.io", + }, + ), + ).rejects.toThrow("Invalid address") + }) }) }) diff --git a/packages/extension/src/ui/services/recovery/implementation.ts b/packages/extension/src/ui/services/recovery/implementation.ts index 05b5138db..15f77e65e 100644 --- a/packages/extension/src/ui/services/recovery/implementation.ts +++ b/packages/extension/src/ui/services/recovery/implementation.ts @@ -1,8 +1,8 @@ import { encryptForBackground } from "../crypto" import { messageClient } from "../messaging/trpc" -import { IRecoveryService } from "./interface" +import { IRecoveryService } from "../../../shared/recovery/service/interface" -export class RecoveryService implements IRecoveryService { +export class ClientRecoveryService implements IRecoveryService { async byBackup(backup: string) { await messageClient.recovery.recoverBackup.mutate({ backup }) } diff --git a/packages/extension/src/ui/services/recovery/index.ts b/packages/extension/src/ui/services/recovery/index.ts index e0ce8f9a9..42b95a597 100644 --- a/packages/extension/src/ui/services/recovery/index.ts +++ b/packages/extension/src/ui/services/recovery/index.ts @@ -1,3 +1,3 @@ -import { RecoveryService } from "./implementation" +import { ClientRecoveryService } from "./implementation" -export const recoveryService = new RecoveryService() +export const clientRecoveryService = new ClientRecoveryService() diff --git a/packages/extension/src/ui/services/swr.service.ts b/packages/extension/src/ui/services/swr.service.ts index 90db1af59..3b67614b3 100644 --- a/packages/extension/src/ui/services/swr.service.ts +++ b/packages/extension/src/ui/services/swr.service.ts @@ -6,10 +6,13 @@ import useSWR, { SWRConfiguration, unstable_serialize, useSWRConfig, + Revalidator, + RevalidatorOptions, } from "swr" import { reviveJsonBigNumber } from "../../shared/json" import { checkStorageAndPrune } from "../../shared/storage/__new/prune" +import { isFunction, isUndefined } from "lodash-es" export interface SWRConfigCommon { suspense?: boolean @@ -115,3 +118,38 @@ export const swrCacheProvider: Cache = { } }, } + +const exponentialBackoff = (retryCount: number) => + (Math.pow(2, retryCount) - 1) * 1000 + +export function onErrorRetry( + error: any, + key: string, + config: SWRConfiguration, + revalidate: Revalidator, + opts: RevalidatorOptions, + getTimeout?: (retryCount: number) => number, +) { + // We only want to retry on 429 and 5xx http errors + if (error?.status < 500 && error?.status !== 429) { + return + } + + const maxRetryCount = config.errorRetryCount || 5 // A maximum of 5 retries + const currentRetryCount = opts.retryCount + + if (isUndefined(maxRetryCount) || isUndefined(currentRetryCount)) { + return + } + + // 1s, 3s, 7s, 15s, 31s + const timeout = isFunction(getTimeout) + ? getTimeout(currentRetryCount) + : exponentialBackoff(currentRetryCount) + + if (currentRetryCount >= maxRetryCount) { + return + } + + setTimeout(revalidate, timeout, opts) +} diff --git a/packages/extension/src/ui/services/tokens/utils.ts b/packages/extension/src/ui/services/tokens/utils.ts index c275c0c80..211b04aeb 100644 --- a/packages/extension/src/ui/services/tokens/utils.ts +++ b/packages/extension/src/ui/services/tokens/utils.ts @@ -13,7 +13,7 @@ export function formatTokenBalance( if (balanceBn === 0n) { balanceFullString = `0.${"0".repeat(length)}` } else { - balanceFullString = bigDecimal.formatUnits(balanceBn, decimals) + balanceFullString = bigDecimal.formatUnits({ value: balanceBn, decimals }) } // show max ${length} characters or what's needed to show everything before the decimal point diff --git a/packages/extension/src/ui/services/ui/client.ts b/packages/extension/src/ui/services/ui/client.ts index 2c2c902cd..c9f74bff5 100644 --- a/packages/extension/src/ui/services/ui/client.ts +++ b/packages/extension/src/ui/services/ui/client.ts @@ -1,3 +1,4 @@ +import { autoConnect } from "trpc-browser/shared/chrome" import { IUIService } from "../../../shared/__new/services/ui/interface" import { DeepPick } from "../../../shared/types/deepPick" import { IClientUIService } from "./interface" @@ -9,6 +10,9 @@ export default class ClientUIService implements IClientUIService { registerUIProcess() { /** connect to the background port from the UI */ - this.browser.runtime.connect({ name: this.uiService.connectId }) + void autoConnect( + () => this.browser.runtime.connect({ name: this.uiService.connectId }), + () => {}, // just ignore the new port, the important part is that the connection got established + ) } } diff --git a/packages/extension/src/ui/services/useStarknetId.ts b/packages/extension/src/ui/services/useStarknetId.ts index ace88a1a0..0d34de089 100644 --- a/packages/extension/src/ui/services/useStarknetId.ts +++ b/packages/extension/src/ui/services/useStarknetId.ts @@ -34,9 +34,11 @@ export async function getStarknetId(account: BaseWalletAccount) { calldata: [account.address], } - const response = await multicall.call(call) + const response = await multicall.callContract(call) - const decimalDomain = response.map((element) => BigInt(element)).slice(1) + const decimalDomain = response.result + .map((element) => BigInt(element)) + .slice(1) const stringDomain = starknetId.useDecoded(decimalDomain) @@ -72,8 +74,8 @@ export async function getAddressFromStarkName( let response, starkNameAddress try { - response = await multicall.call(call) - starkNameAddress = response[0] + response = await multicall.callContract(call) + starkNameAddress = response.result[0] } catch (error) { throw Error("Could not get address from stark name") } diff --git a/packages/extension/src/ui/useEntryRoute.tsx b/packages/extension/src/ui/useEntryRoute.tsx index 3f81dbb02..a5420adf7 100644 --- a/packages/extension/src/ui/useEntryRoute.tsx +++ b/packages/extension/src/ui/useEntryRoute.tsx @@ -17,7 +17,7 @@ export const useEntryRoute = () => { const isPasswordSet = useView(isPasswordSetView) useEffect(() => { - ;(async () => { + void (async () => { if (isFirstRender) { const query = new URLSearchParams(window.location.search) const entry = await determineEntry(query, isBackupStored, isPasswordSet) @@ -40,6 +40,10 @@ const determineEntry = async ( isBackupStored: boolean, isPasswordSet: boolean, ) => { + if (query.get("goto") === "background-error") { + return routes.backgroundError() + } + if (query.get("goto") === "ledger") { return routes.ledgerEntry() } diff --git a/packages/extension/src/ui/views/nft.ts b/packages/extension/src/ui/views/nft.ts index 6ffa16306..82e0bd4d7 100644 --- a/packages/extension/src/ui/views/nft.ts +++ b/packages/extension/src/ui/views/nft.ts @@ -8,7 +8,13 @@ import { } from "../../shared/storage/__new/repositories/nft" import { atomFromRepo } from "./implementation/atomFromRepo" -import { Address, Collection, NftItem, isEqualAddress } from "@argent/shared" +import { + Address, + Collection, + NftItem, + isEqualAddress, + ensureArray, +} from "@argent/shared" const allNftsAtom = atomFromRepo(nftsRepository) @@ -65,11 +71,11 @@ export const accountNftsView = accountNftsAtomFamily(allNftsView) export const nftAssetView = nftAssetAtomFamily(allNftsView) /* Collections */ -const allNftsColletionsAtom = atomFromRepo(nftsCollectionsRepository) +const allNftsCollectionsAtom = atomFromRepo(nftsCollectionsRepository) export const allCollectionsView = atom(async (get) => { - const nfts = await get(allNftsColletionsAtom) - return nfts + const nfts = await get(allNftsCollectionsAtom) + return ensureArray(nfts) }) export const collectionAtomFamily = (view: Atom>) => @@ -92,7 +98,6 @@ export const collectionsByNetworkAtomFamily = ( atom(async (get) => { const collections = await get(view) return collections.filter((collection) => { - console.log(collection.networkId, networkId) return collection.networkId === networkId }) }), @@ -166,7 +171,7 @@ const allNftsContractsAtom = atomFromRepo(nftsContractsRepository) export const allNftsContractsView = atom(async (get) => { const nfts = await get(allNftsContractsAtom) - return nfts + return ensureArray(nfts) }) export const contractAddressesAtomFamily = ( diff --git a/packages/extension/src/ui/views/recovery.ts b/packages/extension/src/ui/views/recovery.ts new file mode 100644 index 000000000..bc859206d --- /dev/null +++ b/packages/extension/src/ui/views/recovery.ts @@ -0,0 +1,11 @@ +import { atom } from "jotai" + +import { recoveryStore } from "../../shared/recovery/storage" +import { atomFromStore } from "./implementation/atomFromStore" + +export const recoveryStoreView = atomFromStore(recoveryStore) + +export const isRecoveringView = atom(async (get) => { + const { isRecovering } = await get(recoveryStoreView) + return isRecovering +}) diff --git a/packages/extension/test/network.mock.ts b/packages/extension/test/network.mock.ts index 904d993a3..f7b40198e 100644 --- a/packages/extension/test/network.mock.ts +++ b/packages/extension/test/network.mock.ts @@ -5,7 +5,7 @@ const defaultNetwork: Network = { name: "mockNetwork", chainId: "1", rpcUrl: "rpcUrl", - multicallAddress: "multicallAddress", + multicallAddress: "0xmulticallAddress", accountClassHash: { standard: "standard", }, diff --git a/packages/extension/test/setup.ts b/packages/extension/test/setup.ts index 400e9f237..e3559241b 100644 --- a/packages/extension/test/setup.ts +++ b/packages/extension/test/setup.ts @@ -20,7 +20,11 @@ vi.stubGlobal("Response", Response) vi.stubGlobal("chrome", { runtime: { id: "test", - connect: noop, + connect: () => ({ + onDisconnect: { + addListener: noop, + }, + }), onConnect: { addListener: noop, }, diff --git a/packages/extension/test/swr.test.tsx b/packages/extension/test/swr.test.tsx index 0b727f270..e22108247 100644 --- a/packages/extension/test/swr.test.tsx +++ b/packages/extension/test/swr.test.tsx @@ -5,10 +5,13 @@ import { describe, expect, test, vi } from "vitest" import { delay } from "../src/shared/utils/delay" import { + onErrorRetry, useConditionallyEnabledSWR, withPolling, } from "../src/ui/services/swr.service" +const getTimeout = (retryCount: number) => 100 // no need for backoff in testing + describe("swr", () => { describe("useConditionallyEnabledSWR()", () => { test("should use the fetcher and return data when enabled, set data to undefined when disabled", async () => { @@ -157,4 +160,181 @@ describe("swr", () => { expect(fetcher).toHaveBeenCalledTimes(5) }) }) + + describe("onErrorRetry", () => { + test.each([200, 404, 401])( + "should not retry, given %i error code", + async (errorCode) => { + const fetcher = vi.fn().mockRejectedValue({ status: errorCode }) + + function Component() { + const { data, error } = useSWR( + `test-key-${errorCode}`, + fetcher, + ) + return ( +
{(data || error) && Initial rendering done}
+ ) + } + + render( + + onErrorRetry( + err, + key, + config, + revalidate, + revalidateOpts, + getTimeout, + ), + }} + > + + , + ) + + await screen.findAllByText("Initial rendering done") + + // Initial fetch + expect(fetcher).toHaveBeenCalledTimes(1) + + await delay(500) + + // offset to check for retries + expect(fetcher).toHaveBeenCalledTimes(1) + }, + ) + + test.each([429, 500, 503])( + "should stop retrying after success, given %i error code", + async (errorCode) => { + const fetcher = vi + .fn() + .mockRejectedValueOnce({ status: errorCode }) + .mockRejectedValueOnce({ status: errorCode }) + .mockResolvedValue("foo") + + function Component() { + const { data, error } = useSWR( + `test-key-${errorCode}`, + fetcher, + ) + return ( +
{(data || error) && Initial rendering done}
+ ) + } + + render( + + onErrorRetry( + err, + key, + config, + revalidate, + revalidateOpts, + getTimeout, + ), + }} + > + + , + ) + + await screen.findAllByText("Initial rendering done") + + // initial rendering + expect(fetcher).toHaveBeenCalledTimes(1) + + await delay(1000) + + expect(fetcher).toHaveBeenCalledTimes(3) + }, + ) + + test.each([429, 500, 503])( + "should stop retrying after hitting the max retry count", + async (errorCode) => { + const fetcher = vi.fn().mockRejectedValue({ status: errorCode }) + + function Component() { + const { data, error } = useSWR( + `test-key-${errorCode}`, + fetcher, + ) + return ( +
{(data || error) && Initial rendering done}
+ ) + } + + render( + + onErrorRetry( + err, + key, + config, + revalidate, + revalidateOpts, + getTimeout, + ), + }} + > + + , + ) + + await screen.findAllByText("Initial rendering done") + + // initial rendering + expect(fetcher).toHaveBeenCalledTimes(1) + + await delay(1000) + + expect(fetcher).toHaveBeenCalledTimes(5) + }, + ) + + test("should retry an unknown error maximum 5 times", async () => { + const fetcher = vi.fn().mockRejectedValue({}) + + function Component() { + const { data, error } = useSWR(`test-key`, fetcher) + return ( +
{(data || error) && Initial rendering done}
+ ) + } + + render( + + onErrorRetry( + err, + key, + config, + revalidate, + revalidateOpts, + getTimeout, + ), + }} + > + + , + ) + + await screen.findAllByText("Initial rendering done") + + // initial rendering + expect(fetcher).toHaveBeenCalledTimes(1) + + await delay(1000) + + expect(fetcher).toHaveBeenCalledTimes(5) + }) + }) }) diff --git a/packages/extension/test/token.mock.ts b/packages/extension/test/token.mock.ts new file mode 100644 index 000000000..ef871b979 --- /dev/null +++ b/packages/extension/test/token.mock.ts @@ -0,0 +1,108 @@ +import { + ApiTokenDataResponse, + ApiTokenDetails, + BaseToken, + Token, +} from "../src/shared/token/__new/types/token.model" +import { TokenWithBalance } from "../src/shared/token/__new/types/tokenBalance.model" +import { + TokenPriceDetails, + TokenWithPrice, +} from "../src/shared/token/__new/types/tokenPrice.model" +import { getMockWalletAccount } from "./walletAccount.mock" + +const defaultBaseToken: BaseToken = { + address: "0x123", + networkId: "mainnet-alpha", +} + +const defaultToken: Token = { + ...defaultBaseToken, + symbol: "TKN", + name: "Token", + decimals: 18, +} + +const defaultApiTokenDetails: ApiTokenDetails = { + id: 1, + address: "0x123", + name: "Token", + symbol: "TKN", + decimals: 18, + iconUrl: "https://example.com", + sendable: true, + popular: true, + refundable: true, + listed: true, + tradable: true, + category: "tokens", + pricingId: 1, +} + +const defaultApiTokenData: ApiTokenDataResponse = { + tokens: [defaultApiTokenDetails], +} + +const defaultTokenWithBalance: TokenWithBalance = { + ...defaultToken, + balance: BigInt(100).toString(), + account: getMockWalletAccount({}), +} + +export const defaultTokenWithPrice: TokenWithPrice = { + ...defaultToken, + usdValue: "100", + pricingId: 1, +} + +export const defaultTokenPriceDetails: TokenPriceDetails = { + ...defaultBaseToken, + pricingId: 1, + ethValue: "0.6", + ccyValue: "2000", + ethDayChange: "0.1", + ccyDayChange: "0.2", +} + +export const getMockBaseToken = (overrides?: Partial) => ({ + ...defaultBaseToken, + ...(overrides ?? {}), +}) + +export const getMockToken = (overrides?: Partial) => ({ + ...defaultToken, + ...(overrides ?? {}), +}) + +export const getMockApiTokenDetails = ( + overrides?: Partial, +) => ({ + ...defaultApiTokenDetails, + ...(overrides ?? {}), +}) + +export const getMockApiTokenData = ( + overrides?: Partial, +) => ({ + ...defaultApiTokenData, + ...(overrides ?? {}), +}) + +export const getMockTokenWithBalance = ( + overrides?: Partial, +) => ({ + ...defaultTokenWithBalance, + ...(overrides ?? {}), +}) + +export const getMockTokenWithPrice = (overrides?: Partial) => ({ + ...defaultTokenWithPrice, + ...(overrides ?? {}), +}) + +export const getMockTokenPriceDetails = ( + overrides?: Partial, +) => ({ + ...defaultTokenPriceDetails, + ...(overrides ?? {}), +}) diff --git a/packages/extension/test/walletAccount.mock.ts b/packages/extension/test/walletAccount.mock.ts index 791a9730d..90a160352 100644 --- a/packages/extension/test/walletAccount.mock.ts +++ b/packages/extension/test/walletAccount.mock.ts @@ -32,7 +32,7 @@ const defaultWalletAccount: WalletAccount = { hidden: false, } -export const getMockWalletAccount = (overrides: Partial) => ({ +export const getMockWalletAccount = (overrides?: Partial) => ({ ...defaultWalletAccount, - ...overrides, + ...(overrides ?? {}), }) diff --git a/packages/extension/webpack.config.js b/packages/extension/webpack.config.js index a41cdf984..6dafc8c1d 100644 --- a/packages/extension/webpack.config.js +++ b/packages/extension/webpack.config.js @@ -21,7 +21,7 @@ const htmlPlugin = new HtmlWebPackPlugin({ }) const isProd = process.env.NODE_ENV === "production" -const useManifestV3 = process.env.MANIFEST_VERSION === "v3" +const useManifestV2 = process.env.MANIFEST_VERSION === "v2" const safeEnvVars = process.env.SAFE_ENV_VARS === "true" function safeGetCommitHash() { const dotenvPath = path.resolve(__dirname, ".env") @@ -97,7 +97,7 @@ module.exports = { patterns: [ { from: "./src/ui/favicon.ico", to: "favicon.ico" }, { - from: `./manifest/${useManifestV3 ? "v3" : "v2"}.json`, + from: `./manifest/${!useManifestV2 ? "v3" : "v2"}.json`, to: "manifest.json", }, { from: "./src/assets", to: "assets" }, @@ -105,12 +105,11 @@ module.exports = { }), new DefinePlugin({ "process.env.VERSION": JSON.stringify( - useManifestV3 ? manifestV3.version : manifestV2.version, // doesn't matter much, but why not + !useManifestV2 ? manifestV3.version : manifestV2.version, // doesn't matter much, but why not ), "process.env.COMMIT_HASH": JSON.stringify(commitHash), }), new ProvidePlugin({ - Buffer: ["buffer", "Buffer"], React: "react", }), diff --git a/packages/get-starknet/package.json b/packages/get-starknet/package.json index 319d2bcdb..498a516ac 100644 --- a/packages/get-starknet/package.json +++ b/packages/get-starknet/package.json @@ -64,7 +64,7 @@ "ws": "^8.8.1" }, "peerDependencies": { - "starknet": "5.18.0" + "starknet": "5.19.5" }, "gitHead": "b16688a8638cc138938e74e1a39d004760165d75" } diff --git a/packages/guardian/package.json b/packages/guardian/package.json index 0f758dd4e..725e561a2 100644 --- a/packages/guardian/package.json +++ b/packages/guardian/package.json @@ -32,6 +32,6 @@ "vite-plugin-dts": "^3.0.0" }, "peerDependencies": { - "starknet": "5.18.0" + "starknet": "5.19.5" } } diff --git a/packages/multicall/.eslintrc.json b/packages/multicall/.eslintrc.json deleted file mode 100644 index 66709acb2..000000000 --- a/packages/multicall/.eslintrc.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-extra-semi": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "vars": "all", - "ignoreRestSiblings": true, - "argsIgnorePattern": "^_" - } - ], - "@typescript-eslint/no-non-null-assertion": "error", - "curly": "error" - } -} diff --git a/packages/multicall/.vscode/settings.json b/packages/multicall/.vscode/settings.json deleted file mode 100644 index cc43ff1bb..000000000 --- a/packages/multicall/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "json.schemaDownload.enable": true, - "typescript.tsdk": "../../node_modules/typescript/lib" -} diff --git a/packages/multicall/CHANGELOG.md b/packages/multicall/CHANGELOG.md deleted file mode 100644 index b18cc8994..000000000 --- a/packages/multicall/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @argent/x-multicall - -## 6.4.1 - -### Patch Changes - -- 5cfc58de7: Fix for rpc diff --git a/packages/multicall/README.md b/packages/multicall/README.md deleted file mode 100644 index e8ee939bb..000000000 --- a/packages/multicall/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Multicall - -This library allows you to make multiple calls to a contract in a single network request. diff --git a/packages/multicall/__tests__/__snapshots__/integration.test.ts.snap b/packages/multicall/__tests__/__snapshots__/integration.test.ts.snap deleted file mode 100644 index 804be2eef..000000000 --- a/packages/multicall/__tests__/__snapshots__/integration.test.ts.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Multicall with address '0xdead' > should error when all fail > - [ - "rejected", - "rejected", - ] - 1`] = ` -[ - "rejected", - "rejected", -] -`; - -exports[`Multicall with address '0xdead' > should partially error with a single error > - [ - "fulfilled", - "rejected", - ] - 1`] = ` -[ - "fulfilled", - "rejected", -] -`; - -exports[`Multicall with address '0xdead' > should partially error with multiple errors > - [ - "rejected", - "fulfilled", - "rejected", - ] - 1`] = ` -[ - "rejected", - "fulfilled", - "rejected", -] -`; - -exports[`Multicall with address undefined > should error when all fail > - [ - "rejected", - "rejected", - ] - 1`] = ` -[ - "rejected", - "rejected", -] -`; - -exports[`Multicall with address undefined > should partially error with a single error > - [ - "fulfilled", - "rejected", - ] - 1`] = ` -[ - "fulfilled", - "rejected", -] -`; - -exports[`Multicall with address undefined > should partially error with multiple errors > - [ - "rejected", - "fulfilled", - "rejected", - ] - 1`] = ` -[ - "rejected", - "fulfilled", - "rejected", -] -`; diff --git a/packages/multicall/__tests__/integration.test.ts b/packages/multicall/__tests__/integration.test.ts deleted file mode 100644 index ac74ca084..000000000 --- a/packages/multicall/__tests__/integration.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { SequencerProvider, constants } from "starknet" -import { uint256 } from "starknet" -import { beforeAll, describe, expect, test } from "vitest" - -import { Multicall } from ".." - -describe.each([ - { - // Test with default provider on testnet2 and default multicall contract address - baseUrl: "https://alpha4-2.starknet.io/", - multicallAddress: undefined, - }, - { - // Test with default provider on testnet2 and not deployed multicall contract address (should fallback to one request per call) - baseUrl: "https://alpha4-2.starknet.io/", - multicallAddress: "0xdead", - }, -])( - "Multicall with address $multicallAddress", - ({ baseUrl, multicallAddress }) => { - let mc: Multicall - beforeAll(() => { - mc = new Multicall( - new SequencerProvider({ - baseUrl, - }), - multicallAddress, - { - batchInterval: 10, // 10ms - }, - ) - }) - - test("should aggregate multiple calls into one multicall", async () => { - const results = await Promise.all([ - // promises resolve at the same time as they use multicall contract - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "balanceOf", - calldata: [ - "0x04a79cA7FDE3dd9C5CBadcBDCB39f95A0619da26767af0B52fD0901cd556a035".toLowerCase(), - ], - }), - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "balanceOf", - calldata: [ - "0x0472eb42746E4b7b426B6DC45B9ac0345bA38502d0928209016F8a1323330CF4".toLowerCase(), - ], - }), - ]) - - for (const result of results) { - const [low, high] = result - const balance = uint256.uint256ToBN({ low, high }) - expect(balance > constants.ZERO).toBeTruthy() - } - }) - test("should partially error with a single error", async () => { - const results = await Promise.allSettled([ - // promises resolve at the same time as they use multicall contract - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "balanceOf", - calldata: [ - "0x0472eb42746E4b7b426B6DC45B9ac0345bA38502d0928209016F8a1323330CF4".toLowerCase(), - ], - }), - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "get_balance", // this will fail - calldata: [ - "0x04a79cA7FDE3dd9C5CBadcBDCB39f95A0619da26767af0B52fD0901cd556a035".toLowerCase(), - ], - }), - ]) - - expect(results.map((x) => x.status)).toMatchSnapshot(` - [ - "fulfilled", - "rejected", - ] - `) - }) - test("should partially error with multiple errors", async () => { - const results = await Promise.allSettled([ - // promises resolve at the same time as they use multicall contract - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "get_balance", // this will fail - calldata: [ - "0x0472eb42746E4b7b426B6DC45B9ac0345bA38502d0928209016F8a1323330CF4".toLowerCase(), - ], - }), - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "balanceOf", - calldata: [ - "0x0472eb42746E4b7b426B6DC45B9ac0345bA38502d0928209016F8a1323330CF4".toLowerCase(), - ], - }), - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "get_balance", // this will fail - calldata: [ - "0x04a79cA7FDE3dd9C5CBadcBDCB39f95A0619da26767af0B52fD0901cd556a035".toLowerCase(), - ], - }), - ]) - - expect(results.map((x) => x.status)).toMatchSnapshot(` - [ - "rejected", - "fulfilled", - "rejected", - ] - `) - }) - test("should error when all fail", async () => { - const results = await Promise.allSettled([ - // promises resolve at the same time as they use multicall contract - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "get_balance", // this will fail - calldata: [ - "0x0472eb42746E4b7b426B6DC45B9ac0345bA38502d0928209016F8a1323330CF4".toLowerCase(), - ], - }), - mc.call({ - contractAddress: - "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", - entrypoint: "get_balance", // this will fail - calldata: [ - "0x04a79cA7FDE3dd9C5CBadcBDCB39f95A0619da26767af0B52fD0901cd556a035".toLowerCase(), - ], - }), - ]) - - expect(results.map((x) => x.status)).toMatchSnapshot(` - [ - "rejected", - "rejected", - ] - `) - }) - }, -) diff --git a/packages/multicall/package.json b/packages/multicall/package.json deleted file mode 100644 index 60a1876e1..000000000 --- a/packages/multicall/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "@argent/x-multicall", - "version": "6.4.1", - "description": "A library for batched calls to Starknet smart contracts", - "private": false, - "keywords": [ - "starknet", - "starkware", - "multicall", - "argent", - "argentx", - "wallet", - "dapp" - ], - "author": "Janek Rahrt ", - "homepage": "https://github.com/argentlabs/argent-x/tree/main/packages/multicall#readme", - "license": "GPL-3.0-only", - "type": "module", - "exports": { - ".": { - "import": "./dist/multicall.js", - "require": "./dist/multicall.umd.cjs" - } - }, - "main": "./dist/multicall.umd.cjs", - "module": "./dist/multicall.js", - "types": "./dist/multicall.d.ts", - "files": [ - "dist" - ], - "publishConfig": { - "registry": "https://registry.npmjs.org" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/argentlabs/argent-x.git" - }, - "scripts": { - "test": "vitest run", - "test:ci": "vitest run --coverage", - "test:watch": "vitest", - "dev": "vite build --watch", - "setup": "vite build", - "build": "vite build" - }, - "bugs": { - "url": "https://github.com/argentlabs/argent-x/issues" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "^15.0.0", - "@vitest/coverage-c8": "^0.33.0", - "happy-dom": "^12.0.0", - "vite": "^4.3.8", - "vite-plugin-dts": "^3.0.0", - "vitest": "^0.33.0" - }, - "dependencies": { - "dataloader": "^2.1.0" - }, - "peerDependencies": { - "starknet": "5.18.0" - }, - "gitHead": "b16688a8638cc138938e74e1a39d004760165d75" -} diff --git a/packages/multicall/src/aggregate.ts b/packages/multicall/src/aggregate.ts deleted file mode 100644 index 8b2940291..000000000 --- a/packages/multicall/src/aggregate.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - Call, - GatewayError, - ProviderInterface, - num, - transaction, - CallData, -} from "starknet" - -const partitionResponses = (responses: string[]): string[][] => { - if (responses.length === 0) { - return [] - } - - const [responseLength, ...restResponses] = responses - const responseLengthInt = Number(num.toBigInt(responseLength)) - const response = restResponses.slice(0, responseLengthInt) - const remainingResponses = restResponses.slice(responseLengthInt) - - return [response, ...partitionResponses(remainingResponses)] -} - -const extractErrorCallIndex = (e: Error) => { - try { - const errorCallIndex = (e as any) - .toString() - .match(/Error message: multicall (\d+) failed/)?.[1] - if (errorCallIndex === undefined) { - throw e - } - return parseInt(errorCallIndex, 10) - } catch { - throw e - } -} - -const fallbackAggregate = async ( - provider: ProviderInterface, - calls: Call[], -): Promise<(string[] | Error)[]> => { - const results = await Promise.allSettled( - calls.map((call) => - provider - .callContract({ - contractAddress: call.contractAddress, - entrypoint: call.entrypoint, - calldata: CallData.toCalldata(call.calldata), - }) - .then((res) => res.result), - ), - ) - - return results.map((result) => { - if (result.status === "fulfilled") { - return result.value - } - return result.reason - }) -} - -const shouldFallbackToProvider = (e: unknown) => { - if ( - e instanceof GatewayError && - e.errorCode === "StarknetErrorCode.UNINITIALIZED_CONTRACT" - ) { - return true - } - if (e instanceof Error && e.toString().includes("-32603")) { - // RPC ERROR - return true - } - return false -} - -export const aggregate = async ( - provider: ProviderInterface, - multicallAddress: string, - calls: Call[], -): Promise<(string[] | Error)[]> => { - if (calls.length === 0) { - return [] - } - try { - const res = await provider.callContract({ - contractAddress: multicallAddress, - entrypoint: "aggregate", - calldata: transaction.fromCallsToExecuteCalldata([...calls]), - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_blockNumber, _totalLength, ...results] = res.result - - return partitionResponses(results) - } catch (e) { - if (!(e instanceof Error)) { - throw e - } - - if (shouldFallbackToProvider(e)) { - return fallbackAggregate(provider, calls) - } - - const errorCallIndex = extractErrorCallIndex(e) - const remainingCalls = calls.filter((_, i) => i !== errorCallIndex) - const remainingResults = await aggregate( - provider, - multicallAddress, - remainingCalls, - ) - return [ - ...remainingResults.slice(0, errorCallIndex), - e, - ...remainingResults.slice(errorCallIndex), - ] - } -} diff --git a/packages/multicall/src/dataloader.ts b/packages/multicall/src/dataloader.ts deleted file mode 100644 index c981448a1..000000000 --- a/packages/multicall/src/dataloader.ts +++ /dev/null @@ -1,41 +0,0 @@ -import DataLoader from "dataloader" -import { Call, CallData, ProviderInterface, hash, num } from "starknet" - -import { aggregate } from "./aggregate" - -export interface DataLoaderOptions { - maxBatchSize?: number - batchInterval?: number -} - -export const getDataLoader = ( - provider: ProviderInterface, - multicallAddress: string, - options: DataLoaderOptions = { - batchInterval: 500, - maxBatchSize: 10, - }, -) => { - const dl = new DataLoader( - async (calls: readonly Call[]): Promise<(string[] | Error)[]> => { - dl.clearAll() - return aggregate(provider, multicallAddress, calls as Call[]) - }, - { - maxBatchSize: options.maxBatchSize, - batchScheduleFn(callback) { - setTimeout(callback, options.batchInterval) - }, - cacheKeyFn(call) { - const { contractAddress, entrypoint, calldata = [] } = call - const cacheKeyContractAddress = num.toHexString(contractAddress) - const cacheKeyEntrypoint = hash.getSelector(entrypoint) - const cacheKeyCalldata = CallData.toCalldata(calldata) - .map((c) => num.toHexString(c)) - .join("-") - return `${cacheKeyContractAddress}--${cacheKeyEntrypoint}--${cacheKeyCalldata}` - }, - }, - ) - return dl -} diff --git a/packages/multicall/src/main.ts b/packages/multicall/src/main.ts deleted file mode 100644 index 0d4ffce0e..000000000 --- a/packages/multicall/src/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import DataLoader from "dataloader" -import { Call, ProviderInterface } from "starknet" - -import { DataLoaderOptions, getDataLoader } from "./dataloader" - -const DEFAULT_MULTICALL_ADDRESS = - "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4" - -export class Multicall { - public readonly dataloader: DataLoader - - constructor( - public readonly provider: ProviderInterface, - public readonly address: string = DEFAULT_MULTICALL_ADDRESS, - dataLoaderOptions?: DataLoaderOptions, - ) { - this.dataloader = getDataLoader(provider, address, dataLoaderOptions) - } - - public call(call: Call): Promise { - return this.dataloader.load(call) - } -} diff --git a/packages/multicall/tsconfig.json b/packages/multicall/tsconfig.json deleted file mode 100644 index 73fdc9e80..000000000 --- a/packages/multicall/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ESNext", "DOM"], - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "declaration": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true - }, - "include": ["src"] -} diff --git a/packages/multicall/vite.config.ts b/packages/multicall/vite.config.ts deleted file mode 100644 index 3a884e535..000000000 --- a/packages/multicall/vite.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -// vite.config.js -import { resolve as resolvePath } from "path" - -import dts from "vite-plugin-dts" -import { defineConfig } from "vitest/config" - -export default defineConfig({ - build: { - rollupOptions: { - external: ["starknet"], - output: { - exports: "named", - }, - }, - emptyOutDir: false, - lib: { - entry: resolvePath(__dirname, "src/main.ts"), - name: "multicall", - // the proper extensions will be added - fileName: "multicall", - }, - }, - optimizeDeps: { - include: ["starknet"], - }, - plugins: [ - dts({ - entryRoot: resolvePath(__dirname, "src"), - insertTypesEntry: true, - }), - ], - test: { - environment: "happy-dom", - exclude: ["**/node_modules/**", "**/*.mock.ts"], - coverage: { - exclude: [ - "**/*.mock.ts", - "**/setup.ts", - "**/*.json", - "**/*.config.{js,ts}", - "test{,s}/**", - "test{,-*}.{js,cjs,mjs,ts,tsx,jsx}", - "**/*.d.ts", - ], - reportsDirectory: "./coverage", - excludeNodeModules: true, - reporter: ["text", "lcov"], - all: true, - }, - }, - esbuild: { - pure: process.env.NODE_ENV === "production" ? ["console.log"] : [], - }, -}) diff --git a/packages/sessions/package.json b/packages/sessions/package.json index 68a5f5e7f..05ccd9015 100644 --- a/packages/sessions/package.json +++ b/packages/sessions/package.json @@ -54,11 +54,11 @@ "happy-dom": "^12.0.0", "vite": "^4.3.8", "vite-plugin-dts": "^3.0.0", - "vitest": "^0.33.0" + "vitest": "^0.34.0" }, "dependencies": { "minimalistic-assert": "^1.0.1", - "starknet": "5.18.0", + "starknet": "5.19.5", "starknet4": "npm:starknet@4.22.0" }, "gitHead": "b16688a8638cc138938e74e1a39d004760165d75" diff --git a/packages/sessions/vite.config.ts b/packages/sessions/vite.config.ts index ee3d3aaf7..22bc8b4a9 100644 --- a/packages/sessions/vite.config.ts +++ b/packages/sessions/vite.config.ts @@ -30,6 +30,13 @@ export default defineConfig({ }), ], test: { + deps: { + optimizer: { + web: { + enabled: false, + }, + }, + }, environment: "happy-dom", exclude: ["**/node_modules/**", "**/*.mock.ts"], coverage: { diff --git a/packages/shared/package.json b/packages/shared/package.json index 7f1aa6e2f..80015418e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -31,11 +31,12 @@ "typescript": "^5.0.4", "vite": "^4.3.8", "vite-plugin-dts": "^3.0.0", - "vitest": "^0.33.0", + "vitest": "^0.34.0", "happy-dom": "^12.0.0" }, "dependencies": { - "@argent/x-multicall": "^6.3.1", + "@argent/x-multicall": "^7.0.8", + "@noble/hashes": "^1.3.1", "emittery": "^1.0.1", "neverthrow": "^6.0.0", "url-join": "^5.0.0" @@ -45,6 +46,6 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.0.1", - "starknet": "5.18.0" + "starknet": "5.19.5" } } diff --git a/packages/shared/src/account/index.ts b/packages/shared/src/account/index.ts index aff6d5070..5b0df5ccc 100644 --- a/packages/shared/src/account/index.ts +++ b/packages/shared/src/account/index.ts @@ -1 +1,2 @@ export * from "./accountv4" +export * from "./preferences.model" diff --git a/packages/shared/src/account/preferences.model.ts b/packages/shared/src/account/preferences.model.ts new file mode 100644 index 000000000..7b36b5d5d --- /dev/null +++ b/packages/shared/src/account/preferences.model.ts @@ -0,0 +1,14 @@ +import { z } from "zod" + +export const preferencesSchema = z.object({ + value: z.string(), + platform: z.enum(["ios", "argentx", "android", "webwallet"]).nullable(), +}) + +export type Preferences = z.infer + +export const preferencesEndpointPayload = z.object({ + preferences: z.record(preferencesSchema), +}) + +export type PreferencesPayload = z.infer diff --git a/packages/shared/src/bigdecimal/formatUnits.test.ts b/packages/shared/src/bigdecimal/formatUnits.test.ts index 776a8f5a8..d1987ba04 100644 --- a/packages/shared/src/bigdecimal/formatUnits.test.ts +++ b/packages/shared/src/bigdecimal/formatUnits.test.ts @@ -2,30 +2,30 @@ import { formatUnits } from "./formatUnits" describe("formatUnits function", () => { test("Should return string version of a value when decimals are 0", () => { - expect(formatUnits(123456789n, 0)).toBe("123456789") - expect(formatUnits(-123456789n, 0)).toBe("-123456789") + expect(formatUnits({ value: 123456789n, decimals: 0 })).toBe("123456789") + expect(formatUnits({ value: -123456789n, decimals: 0 })).toBe("-123456789") }) test("Should handle negative values correctly", () => { - expect(formatUnits(-123456789n, 2)).toBe("-1234567.89") - expect(formatUnits(-100n, 2)).toBe("-1") + expect(formatUnits({ value: -123456789n, decimals: 2 })).toBe("-1234567.89") + expect(formatUnits({ value: -100n, decimals: 2 })).toBe("-1") }) test("Should format value correctly when decimals are not 0", () => { - expect(formatUnits(123456789n, 2)).toBe("1234567.89") - expect(formatUnits(123456789n, 3)).toBe("123456.789") - expect(formatUnits(123456789n, 4)).toBe("12345.6789") + expect(formatUnits({ value: 123456789n, decimals: 2 })).toBe("1234567.89") + expect(formatUnits({ value: 123456789n, decimals: 3 })).toBe("123456.789") + expect(formatUnits({ value: 123456789n, decimals: 4 })).toBe("12345.6789") }) test("Should correctly pad and format the value", () => { - expect(formatUnits(5n, 4)).toBe("0.0005") - expect(formatUnits(50n, 4)).toBe("0.005") - expect(formatUnits(500n, 4)).toBe("0.05") - expect(formatUnits(5000n, 4)).toBe("0.5") + expect(formatUnits({ value: 5n, decimals: 4 })).toBe("0.0005") + expect(formatUnits({ value: 50n, decimals: 4 })).toBe("0.005") + expect(formatUnits({ value: 500n, decimals: 4 })).toBe("0.05") + expect(formatUnits({ value: 5000n, decimals: 4 })).toBe("0.5") }) test("Should correctly handle values with trailing zeros", () => { - expect(formatUnits(100n, 2)).toBe("1") - expect(formatUnits(10000n, 4)).toBe("1") + expect(formatUnits({ value: 100n, decimals: 2 })).toBe("1") + expect(formatUnits({ value: 10000n, decimals: 4 })).toBe("1") }) }) diff --git a/packages/shared/src/bigdecimal/formatUnits.ts b/packages/shared/src/bigdecimal/formatUnits.ts index 939f532fd..c720284a8 100644 --- a/packages/shared/src/bigdecimal/formatUnits.ts +++ b/packages/shared/src/bigdecimal/formatUnits.ts @@ -1,11 +1,12 @@ +import { BigDecimal } from "./types" + /** * Formats a BigInt value to a string with a given number of decimal places. * - * @param {bigint} value - The BigInt value to be formatted. - * @param {number} decimals - The number of decimal places to format the value to. + * @param {BigDecimal} The amount to format * @returns {string} The formatted string. */ -export function formatUnits(value: bigint, decimals: number): string { +export function formatUnits({ value, decimals }: BigDecimal): string { if (decimals === 0) { return value.toString() } diff --git a/packages/shared/src/bigdecimal/index.ts b/packages/shared/src/bigdecimal/index.ts index 02216c72c..4b1a1df57 100644 --- a/packages/shared/src/bigdecimal/index.ts +++ b/packages/shared/src/bigdecimal/index.ts @@ -1,3 +1,4 @@ export * from "./utils" +export * from "./lib" export * from "./parseUnits" export * from "./formatUnits" diff --git a/packages/shared/src/bigdecimal/lib.test.ts b/packages/shared/src/bigdecimal/lib.test.ts new file mode 100644 index 000000000..796125e69 --- /dev/null +++ b/packages/shared/src/bigdecimal/lib.test.ts @@ -0,0 +1,71 @@ +import { test } from "vitest" +import { add, sub, div, mul, mod, abs, eq, lt, gt, lte, gte, not } from "./lib" +import { BigDecimal } from "./types" + +const a: BigDecimal = { value: BigInt(10), decimals: 2 } +const b: BigDecimal = { value: BigInt(5), decimals: 2 } + +test("add", () => { + const result = add(a, b) + expect(result.value).toBe(BigInt(15)) + expect(result.decimals).toBe(2) +}) + +test("sub", () => { + const result = sub(a, b) + expect(result.value).toBe(BigInt(5)) + expect(result.decimals).toBe(2) +}) + +test("div", () => { + const result = div(a, b) + expect(result.value).toBe(BigInt(200)) + expect(result.decimals).toBe(2) +}) + +test("mul", () => { + const result = mul(a, b) + expect(result.value).toBe(BigInt(5)) + expect(result.decimals).toBe(3) +}) + +test("mod", () => { + const result = mod(a, b) + expect(result.value).toBe(BigInt(0)) + expect(result.decimals).toBe(2) +}) + +test("abs", () => { + const result = abs({ ...a, value: -a.value }) + expect(result).toEqual(a) +}) + +test("eq", () => { + expect(eq(a, a)).toBe(true) + expect(eq(a, b)).toBe(false) +}) + +test("lt", () => { + expect(lt(a, b)).toBe(false) + expect(lt(b, a)).toBe(true) +}) + +test("gt", () => { + expect(gt(a, b)).toBe(true) + expect(gt(b, a)).toBe(false) +}) + +test("lte", () => { + expect(lte(a, a)).toBe(true) + expect(lte(a, b)).toBe(false) +}) + +test("gte", () => { + expect(gte(a, a)).toBe(true) + expect(gte(b, a)).toBe(false) +}) + +test("not", () => { + expect(not(a, a)).toBe(false) + expect(not(a, b)).toBe(true) +}) diff --git a/packages/shared/src/bigdecimal/lib.ts b/packages/shared/src/bigdecimal/lib.ts new file mode 100644 index 000000000..3b54e075e --- /dev/null +++ b/packages/shared/src/bigdecimal/lib.ts @@ -0,0 +1,86 @@ +import { BigDecimal } from "./types" + +export const toFixedDecimals = ( + amount: BigDecimal, + decimals: number, +): BigDecimal => ({ + decimals: decimals, + value: + decimals >= amount.decimals + ? amount.value * BigInt(10) ** BigInt(decimals - amount.decimals) + : amount.value / BigInt(10) ** BigInt(amount.decimals - decimals), +}) + +export const toTiniestPossibleDecimal = (amount: BigDecimal): BigDecimal => { + const decimalsReduction = amount.value + .toString() + .split("") + .reverse() + .reduce((acc, char) => (char === "0" ? acc + 1 : acc), 0) + return toFixedDecimals(amount, amount.decimals - decimalsReduction) +} + +const getAdjustedValues = ( + a: BigDecimal, + b: BigDecimal, +): [bigint, bigint, number] => { + const decimals = Math.max(a.decimals, b.decimals) + const [adjustedA, adjustedB] = [a, b].map((amount) => + toFixedDecimals(amount, decimals), + ) + return [adjustedA.value, adjustedB.value, decimals] +} + +type ArithmeticOpCallback = (a: bigint, b: bigint, decimals: number) => bigint + +const arithmeticOp = + (op: ArithmeticOpCallback) => + (a: BigDecimal, b: BigDecimal): BigDecimal => { + const [adjustedA, adjustedB, decimals] = getAdjustedValues(a, b) + return { + decimals, + value: op(adjustedA, adjustedB, decimals), + } + } + +type CompareOpCallback = (a: bigint, b: bigint) => boolean + +const compareOp = + (op: CompareOpCallback) => + (a: BigDecimal, b: BigDecimal): boolean => { + const [adjustedA, adjustedB] = getAdjustedValues(a, b) + return op(adjustedA, adjustedB) + } + +// arithmetic: add, sub, mul, div, mod, abs +export const add = arithmeticOp((a, b) => a + b) +export const sub = arithmeticOp((a, b) => a - b) +export const div = arithmeticOp((a, b, decimals) => { + if (b === BigInt(0)) { + throw new Error("Division by zero is not allowed") + } + return (a * BigInt(10) ** BigInt(decimals)) / b +}) +export const mul = (a: BigDecimal, b: BigDecimal): BigDecimal => + toTiniestPossibleDecimal({ + decimals: a.decimals + b.decimals, + value: a.value * b.value, + }) +export const mod = arithmeticOp((a, b) => { + if (b === BigInt(0)) { + throw new Error("Modulus by zero is not allowed") + } + return a % b +}) +export const abs = (amount: BigDecimal): BigDecimal => ({ + decimals: amount.decimals, + value: amount.value >= BigInt(0) ? amount.value : -amount.value, +}) + +// compare: eq, lt, gt, lte, gte, not +export const eq = compareOp((a, b) => a === b) +export const lt = compareOp((a, b) => a < b) +export const gt = compareOp((a, b) => a > b) +export const lte = compareOp((a, b) => a <= b) +export const gte = compareOp((a, b) => a >= b) +export const not = (a: BigDecimal, b: BigDecimal): boolean => !eq(a, b) diff --git a/packages/shared/src/bigdecimal/parseUnits.test.ts b/packages/shared/src/bigdecimal/parseUnits.test.ts index 72f1f5e0f..70f4df298 100644 --- a/packages/shared/src/bigdecimal/parseUnits.test.ts +++ b/packages/shared/src/bigdecimal/parseUnits.test.ts @@ -2,42 +2,42 @@ import { parseUnits } from "./parseUnits" describe("parseUnits function", () => { test("Should correctly parse string value to BigInt", () => { - expect(parseUnits("1234567.89", 2)).toBe(123456789n) - expect(parseUnits("123456.789", 3)).toBe(123456789n) - expect(parseUnits("12345.6789", 4)).toBe(123456789n) + expect(parseUnits("1234567.89", 2).value).toBe(123456789n) + expect(parseUnits("123456.789", 3).value).toBe(123456789n) + expect(parseUnits("12345.6789", 4).value).toBe(123456789n) }) test("Should handle negative values correctly", () => { - expect(parseUnits("-1234567.89", 2)).toBe(-123456789n) - expect(parseUnits("-1", 2)).toBe(-100n) + expect(parseUnits("-1234567.89", 2).value).toBe(-123456789n) + expect(parseUnits("-1", 2).value).toBe(-100n) }) test("Should correctly handle values with insufficient fraction digits", () => { - expect(parseUnits("0.0005", 4)).toBe(5n) - expect(parseUnits("0.005", 4)).toBe(50n) - expect(parseUnits("0.05", 4)).toBe(500n) - expect(parseUnits("0.5", 4)).toBe(5000n) + expect(parseUnits("0.0005", 4).value).toBe(5n) + expect(parseUnits("0.005", 4).value).toBe(50n) + expect(parseUnits("0.05", 4).value).toBe(500n) + expect(parseUnits("0.5", 4).value).toBe(5000n) }) test("Should correctly round-off fractions longer than the specified decimals", () => { - expect(parseUnits("1.00005", 4)).toBe(10001n) - expect(parseUnits("1.00004", 4)).toBe(10000n) - expect(parseUnits("1.000049", 5)).toBe(100005n) - expect(parseUnits("1.000050", 5)).toBe(100005n) - expect(parseUnits("1.000499", 6)).toBe(1000499n) - expect(parseUnits("1.000500", 6)).toBe(1000500n) - expect(parseUnits("1.1234567890", 9)).toBe(1123456789n) - expect(parseUnits("-1.00005", 4)).toBe(-10001n) - expect(parseUnits("-1.00004", 4)).toBe(-10000n) - expect(parseUnits("-1.000049", 5)).toBe(-100005n) - expect(parseUnits("-1.000050", 5)).toBe(-100005n) - expect(parseUnits("-1.000499", 6)).toBe(-1000499n) - expect(parseUnits("-1.000500", 6)).toBe(-1000500n) - expect(parseUnits("-1.1234567890", 9)).toBe(-1123456789n) + expect(parseUnits("1.00005", 4).value).toBe(10001n) + expect(parseUnits("1.00004", 4).value).toBe(10000n) + expect(parseUnits("1.000049", 5).value).toBe(100005n) + expect(parseUnits("1.000050", 5).value).toBe(100005n) + expect(parseUnits("1.000499", 6).value).toBe(1000499n) + expect(parseUnits("1.000500", 6).value).toBe(1000500n) + expect(parseUnits("1.1234567890", 9).value).toBe(1123456789n) + expect(parseUnits("-1.00005", 4).value).toBe(-10001n) + expect(parseUnits("-1.00004", 4).value).toBe(-10000n) + expect(parseUnits("-1.000049", 5).value).toBe(-100005n) + expect(parseUnits("-1.000050", 5).value).toBe(-100005n) + expect(parseUnits("-1.000499", 6).value).toBe(-1000499n) + expect(parseUnits("-1.000500", 6).value).toBe(-1000500n) + expect(parseUnits("-1.1234567890", 9).value).toBe(-1123456789n) }) test("Should correctly pad the fraction part", () => { - expect(parseUnits("1", 2)).toBe(100n) - expect(parseUnits("1", 4)).toBe(10000n) + expect(parseUnits("1", 2).value).toBe(100n) + expect(parseUnits("1", 4).value).toBe(10000n) }) }) diff --git a/packages/shared/src/bigdecimal/parseUnits.ts b/packages/shared/src/bigdecimal/parseUnits.ts index b9b060771..1de617cfb 100644 --- a/packages/shared/src/bigdecimal/parseUnits.ts +++ b/packages/shared/src/bigdecimal/parseUnits.ts @@ -1,3 +1,5 @@ +import { BigDecimal } from "./types" + /** * Parses a string value representing a number to a BigInt, with a specified number of decimals. * If the fraction part of the string value is longer than the allowed decimal places, it rounds the value. @@ -6,7 +8,7 @@ * @param {number} decimals - The number of decimals to consider during parsing. * @returns {bigint} The parsed BigInt value. */ -export function parseUnits(value: string, decimals: number): bigint { +export function parseUnits(value: string, decimals: number): BigDecimal { let [integer, fraction = ""] = value.split(".") const negative = integer.startsWith("-") @@ -29,5 +31,10 @@ export function parseUnits(value: string, decimals: number): bigint { fraction = fraction.padEnd(decimals, "0") } - return BigInt(`${negative ? "-" : ""}${integer}${fraction}`) + const parsedValue = BigInt(`${negative ? "-" : ""}${integer}${fraction}`) + + return { + value: parsedValue, + decimals, + } } diff --git a/packages/shared/src/bigdecimal/types.ts b/packages/shared/src/bigdecimal/types.ts new file mode 100644 index 000000000..e22472ba4 --- /dev/null +++ b/packages/shared/src/bigdecimal/types.ts @@ -0,0 +1,4 @@ +export interface BigDecimal { + value: bigint + decimals: number +} diff --git a/packages/shared/src/bigdecimal/utils.test.ts b/packages/shared/src/bigdecimal/utils.test.ts index d21031558..18038f24e 100644 --- a/packages/shared/src/bigdecimal/utils.test.ts +++ b/packages/shared/src/bigdecimal/utils.test.ts @@ -15,9 +15,9 @@ describe("Utility functions", () => { }) test("parseEther function should correctly parse ether string to BigInt", () => { - expect(parseEther("1")).toBe(1000000000000000000n) - expect(parseEther("1.23456789")).toBe(1234567890000000000n) - expect(parseEther("-1")).toBe(-1000000000000000000n) + expect(parseEther("1").value).toBe(1000000000000000000n) + expect(parseEther("1.23456789").value).toBe(1234567890000000000n) + expect(parseEther("-1").value).toBe(-1000000000000000000n) }) test("formatCurrency function should correctly format BigInt to currency string", () => { @@ -27,9 +27,9 @@ describe("Utility functions", () => { }) test("parseCurrency function should correctly parse currency string to BigInt", () => { - expect(parseCurrency("1")).toBe(1000000n) - expect(parseCurrency("1.234567")).toBe(1234567n) - expect(parseCurrency("-1")).toBe(-1000000n) + expect(parseCurrency("1").value).toBe(1000000n) + expect(parseCurrency("1.234567").value).toBe(1234567n) + expect(parseCurrency("-1").value).toBe(-1000000n) }) test("absBigInt function should return absolute value of BigInt", () => { @@ -39,8 +39,8 @@ describe("Utility functions", () => { }) test("parseCurrencyAbs function should return absolute value of parsed currency string", () => { - expect(parseCurrencyAbs("1")).toBe(1000000n) - expect(parseCurrencyAbs("1.234567")).toBe(1234567n) - expect(parseCurrencyAbs("-1")).toBe(1000000n) + expect(parseCurrencyAbs("1").value).toBe(1000000n) + expect(parseCurrencyAbs("1.234567").value).toBe(1234567n) + expect(parseCurrencyAbs("-1").value).toBe(1000000n) }) }) diff --git a/packages/shared/src/bigdecimal/utils.ts b/packages/shared/src/bigdecimal/utils.ts index eb4fff13a..8ab69bcfc 100644 --- a/packages/shared/src/bigdecimal/utils.ts +++ b/packages/shared/src/bigdecimal/utils.ts @@ -1,5 +1,7 @@ import { parseUnits } from "./parseUnits" import { formatUnits } from "./formatUnits" +import { BigDecimal } from "./types" +import { abs } from "." /** * Formats a BigInt representing wei into a string representing ether, @@ -14,7 +16,7 @@ import { formatUnits } from "./formatUnits" * @returns {string} The amount in ether. */ export function formatEther(wei: bigint): string { - return formatUnits(wei, 18) + return formatUnits({ value: wei, decimals: 18 }) } /** @@ -23,7 +25,7 @@ export function formatEther(wei: bigint): string { * @param {string} ether - The amount in ether to be parsed to wei. * @returns {bigint} The amount in wei. */ -export function parseEther(ether: string): bigint { +export function parseEther(ether: string): BigDecimal { return parseUnits(ether, 18) } @@ -36,11 +38,11 @@ export function parseEther(ether: string): bigint { * decimals: 6 * } * - * @param {bigint} amount - The amount to be formatted. + * @param {bigint} value - The amount to be formatted. * @returns {string} The formatted amount. */ -export function formatCurrency(amount: bigint): string { - return formatUnits(amount, 6) +export function formatCurrency(value: bigint): string { + return formatUnits({ value, decimals: 6 }) } /** @@ -49,7 +51,7 @@ export function formatCurrency(amount: bigint): string { * @param {string} amount - The amount to be parsed. * @returns {bigint} The parsed amount. */ -export function parseCurrency(amount: string): bigint { +export function parseCurrency(amount: string): BigDecimal { return parseUnits(amount, 6) } @@ -60,7 +62,7 @@ export function parseCurrency(amount: string): bigint { * @returns {bigint} The absolute value of the number. */ export function absBigInt(num: bigint): bigint { - return num < 0n ? -num : num + return abs({ value: num, decimals: 0 }).value } /** @@ -70,6 +72,6 @@ export function absBigInt(num: bigint): bigint { * @param {string} amount - The amount to be parsed and then converted to absolute value. * @returns {bigint} The absolute value of the parsed amount. */ -export function parseCurrencyAbs(amount: string): bigint { - return absBigInt(parseCurrency(amount)) +export function parseCurrencyAbs(amount: string): BigDecimal { + return abs(parseCurrency(amount)) } diff --git a/packages/shared/src/chains/index.ts b/packages/shared/src/chains/index.ts index 225bae057..f2a837192 100644 --- a/packages/shared/src/chains/index.ts +++ b/packages/shared/src/chains/index.ts @@ -1,3 +1,6 @@ +export * from "./starknet/addressStarknetId" export * from "./starknet/address" export * from "./starknet/addressInput" export * from "./starknet/starknetId" +export * from "./starknet/parseAddress" +export * from "./starknet/network" diff --git a/packages/shared/src/chains/starknet/address.ts b/packages/shared/src/chains/starknet/address.ts index 823063124..323ed10c6 100644 --- a/packages/shared/src/chains/starknet/address.ts +++ b/packages/shared/src/chains/starknet/address.ts @@ -58,6 +58,11 @@ export const addressSchema = addressSchemaLooseLength return `0x${padded}` }) +export const addressOrEmptyUndefinedSchema = addressSchema + .or(z.literal("")) + .transform((v) => (v === "" ? undefined : v)) + .optional() + export const addressSchemaArgentBackend = addressSchemaBase.transform( (value) => { // 0 padded, 0x prefixed, lowercase hex with a length of 66 diff --git a/packages/shared/src/chains/starknet/addressStarknetId.test.ts b/packages/shared/src/chains/starknet/addressStarknetId.test.ts new file mode 100644 index 000000000..fd0e15ac8 --- /dev/null +++ b/packages/shared/src/chains/starknet/addressStarknetId.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "vitest" + +import { addressOrStarknetIdSchema } from "./addressStarknetId" + +describe("chains/starknet/address", () => { + describe("addressOrStarknetIdSchema", () => { + describe("when valid", () => { + test("success should be true", () => { + expect( + addressOrStarknetIdSchema.safeParse("foo.stark").success, + ).toBeTruthy() + expect( + addressOrStarknetIdSchema.safeParse( + "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a", + ).success, + ).toBeTruthy() + }) + }) + describe("when invalid", () => { + test("success should be false with empty string", () => { + expect(addressOrStarknetIdSchema.safeParse("").success).toBeFalsy() + }) + test("success should be false with only .stark suffix", () => { + expect( + addressOrStarknetIdSchema.safeParse(".stark").success, + ).toBeFalsy() + }) + test("success should be false when address too long", () => { + expect( + addressOrStarknetIdSchema.safeParse( + "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a00", + ).success, + ).toBeFalsy() + }) + test("success should be false when address too short", () => { + expect( + addressOrStarknetIdSchema.safeParse("0x7e00d496e32").success, + ).toBeFalsy() + }) + test("success should be false when starkname not exists", () => { + expect( + addressOrStarknetIdSchema.safeParse("123.star").success, + ).toBeFalsy() + }) + test("success should be false when starkname has random characters", () => { + expect( + addressOrStarknetIdSchema.safeParse("123###1@@! ^%.star").success, + ).toBeFalsy() + }) + }) + }) +}) diff --git a/packages/shared/src/chains/starknet/addressStarknetId.ts b/packages/shared/src/chains/starknet/addressStarknetId.ts new file mode 100644 index 000000000..61f14266f --- /dev/null +++ b/packages/shared/src/chains/starknet/addressStarknetId.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +import { addressSchema } from "./address" +import { starknetIdSchema } from "./starknetId" + +export const addressOrStarknetIdSchema = z.union([ + addressSchema, + starknetIdSchema, +]) + +export type AddressOrStarknetId = z.infer diff --git a/packages/shared/src/chains/starknet/network.test.ts b/packages/shared/src/chains/starknet/network.test.ts new file mode 100644 index 000000000..618db0b3d --- /dev/null +++ b/packages/shared/src/chains/starknet/network.test.ts @@ -0,0 +1,53 @@ +import { constants } from "starknet" + +import { describe, expect, test } from "vitest" + +import { getChainIdFromNetworkId } from "./network" +import { NetworkError } from "../../errors/network" + +describe("chains/starknet/network", () => { + describe("network", () => { + describe("get chain id", () => { + test("should retrieve mainnet using networkId", () => { + const chainId = getChainIdFromNetworkId("mainnet-alpha") + expect(chainId).toBe(constants.StarknetChainId.SN_MAIN) + }) + + test("should retrieve mainnet using starknetjs network name", () => { + const chainId = getChainIdFromNetworkId(constants.NetworkName.SN_MAIN) + expect(chainId).toBe(constants.StarknetChainId.SN_MAIN) + }) + + test("should retrieve testnet using networkId", () => { + const chainId = getChainIdFromNetworkId("goerli-alpha") + expect(chainId).toBe(constants.StarknetChainId.SN_GOERLI) + }) + + test("should retrieve testnet using starknetjs network name", () => { + const chainId = getChainIdFromNetworkId(constants.NetworkName.SN_GOERLI) + expect(chainId).toBe(constants.StarknetChainId.SN_GOERLI) + }) + }) + describe("when invalid", () => { + test("should throw error when empty", () => { + expect(() => getChainIdFromNetworkId("")).toThrowError( + new NetworkError({ + code: "NOT_FOUND", + message: `Unknown networkId: `, + }), + ) + }) + + test("should throw error when random string", () => { + expect(() => + getChainIdFromNetworkId("this-network-does-not-exist"), + ).toThrowError( + new NetworkError({ + code: "NOT_FOUND", + message: `Unknown networkId: this-network-does-not-exist`, + }), + ) + }) + }) + }) +}) diff --git a/packages/shared/src/chains/starknet/network.ts b/packages/shared/src/chains/starknet/network.ts new file mode 100644 index 000000000..887be3b4a --- /dev/null +++ b/packages/shared/src/chains/starknet/network.ts @@ -0,0 +1,22 @@ +import { constants } from "starknet" +import { NetworkError } from "../../errors/network" + +export function getChainIdFromNetworkId( + networkId: string, +): constants.StarknetChainId { + switch (networkId) { + case "mainnet-alpha": + case constants.NetworkName.SN_MAIN: + return constants.StarknetChainId.SN_MAIN + + case "goerli-alpha": + case constants.NetworkName.SN_GOERLI: + return constants.StarknetChainId.SN_GOERLI + + default: + throw new NetworkError({ + code: "NOT_FOUND", + message: `Unknown networkId: ${networkId}`, + }) + } +} diff --git a/packages/shared/src/chains/starknet/parseAddress.ts b/packages/shared/src/chains/starknet/parseAddress.ts new file mode 100644 index 000000000..a3f21da4e --- /dev/null +++ b/packages/shared/src/chains/starknet/parseAddress.ts @@ -0,0 +1,103 @@ +import { MinimalProviderInterface } from "@argent/x-multicall" +import { num } from "starknet" + +import { isStarknetId } from "./starknetId" + +import { Call, constants, starknetId } from "starknet" +import { addressSchema, isValidAddress, normalizeAddress } from "./address" +import { getChainIdFromNetworkId } from "./network" +import { AddressError } from "../../errors/address" +import { NetworkError } from "../../errors/network" +import { callSchema } from "../../utils/starknet" +import { CallError } from "../../errors/call" + +export async function getAddressFromStarkName( + starkName: string, + networkId?: string, + multicall?: MinimalProviderInterface, +) { + if (!networkId || !multicall) { + throw new NetworkError({ + code: "NO_NETWORK_OR_MULTICALL", + }) + } + + const chainId = getChainIdFromNetworkId(networkId) + const starknetIdContractAddress = starknetId.getStarknetIdContract(chainId) + const sanitisedStarkName = starkName.replace(".stark", "") + + let call: Call | null = null + try { + call = callSchema.parse({ + contractAddress: starknetIdContractAddress, + entrypoint: "domain_to_address", + calldata: { + domain: sanitisedStarkName + .split(".") + .map((element) => starknetId.useEncoded(element).toString(10)), + }, + }) + } catch (error) { + throw new CallError({ + code: "NOT_VALID", + options: { error }, + }) + } + + let response, starkNameAddress + try { + response = await multicall.callContract(call) + starkNameAddress = response.result[0] + } catch (error) { + throw new AddressError({ + code: "NO_ADDRESS_FROM_STARKNAME", + }) + } + + const isZero = num.toBigInt(starkNameAddress) === constants.ZERO + if (isZero) { + /** service returned but not found */ + throw new AddressError({ + code: "STARKNAME_NOT_FOUND", + message: `${starkName} not found`, + }) + } + + const isValid = isValidAddress(starkNameAddress) + if (!isValid) { + /** service returned but not a valid address */ + throw new AddressError({ + code: "STARKNAME_INVALID_ADDRESS", + message: `${starkName} resolved to an invalid address (${starkNameAddress})`, + }) + } + + return normalizeAddress(starkNameAddress) +} +interface ParseAddressProps { + address: string + networkId?: string + multicallProvider?: MinimalProviderInterface +} + +export const parseAddress = async ({ + address, + networkId, + multicallProvider, +}: ParseAddressProps) => { + let parsedAddress = null + try { + parsedAddress = addressSchema.parse(address) + } catch (e) { + if (isStarknetId(address)) { + parsedAddress = await getAddressFromStarkName( + address, + networkId, + multicallProvider, + ) + } else { + throw new AddressError({ code: "NOT_VALID" }) + } + } + return parsedAddress +} diff --git a/packages/shared/src/chains/starknet/starknetId.test.ts b/packages/shared/src/chains/starknet/starknetId.test.ts index 156f24175..a9371d82c 100644 --- a/packages/shared/src/chains/starknet/starknetId.test.ts +++ b/packages/shared/src/chains/starknet/starknetId.test.ts @@ -1,11 +1,6 @@ import { describe, expect, test } from "vitest" -import { - addressOrStarknetIdSchema, - isEqualStarknetId, - isStarknetId, - starknetIdSchema, -} from "./starknetId" +import { isEqualStarknetId, isStarknetId, starknetIdSchema } from "./starknetId" describe("chains/starknet/address", () => { describe("starknetIdSchema", () => { @@ -38,33 +33,6 @@ describe("chains/starknet/address", () => { }) }) }) - describe("addressOrStarknetIdSchema", () => { - describe("when valid", () => { - test("success should be true", () => { - expect( - addressOrStarknetIdSchema.safeParse("foo.stark").success, - ).toBeTruthy() - expect( - addressOrStarknetIdSchema.safeParse( - "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a", - ).success, - ).toBeTruthy() - }) - }) - describe("when invalid", () => { - test("success should be false", () => { - expect(addressOrStarknetIdSchema.safeParse("").success).toBeFalsy() - expect( - addressOrStarknetIdSchema.safeParse(".stark").success, - ).toBeFalsy() - expect( - addressOrStarknetIdSchema.safeParse( - "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a00", - ).success, - ).toBeFalsy() - }) - }) - }) describe("isStarknetId", () => { describe("when valid", () => { test("success should be true", () => { diff --git a/packages/shared/src/chains/starknet/starknetId.ts b/packages/shared/src/chains/starknet/starknetId.ts index 714f3edd0..788bb6ea6 100644 --- a/packages/shared/src/chains/starknet/starknetId.ts +++ b/packages/shared/src/chains/starknet/starknetId.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { Address, addressSchema, normalizeAddress } from "./address" +import { Address, normalizeAddress } from "./address" /** * https://docs.starknet.id/for-devs/encoding-algorithm#the-basic-alphabet @@ -42,10 +42,3 @@ export const normalizeAddressOrStarknetId = ( } return normalizeAddress(addressOrStarknetId) } - -export const addressOrStarknetIdSchema = z.union([ - addressSchema, - starknetIdSchema, -]) - -export type AddressOrStarknetId = z.infer diff --git a/packages/shared/src/errors/address.ts b/packages/shared/src/errors/address.ts new file mode 100644 index 000000000..613bab5ad --- /dev/null +++ b/packages/shared/src/errors/address.ts @@ -0,0 +1,18 @@ +import { BaseError, BaseErrorPayload } from "./baseErrors" + +export enum ADDRESS_ERROR_MESSAGES { + NOT_VALID = "Invalid address", + NOT_FOUND = "Address not found", + STARKNAME_NOT_FOUND = "Stark name not found", + NO_ADDRESS_FROM_STARKNAME = "Could not get address from stark name", + STARKNAME_INVALID_ADDRESS = "Stark name resolved to an invalid address", +} + +export type AddressErrorMessage = keyof typeof ADDRESS_ERROR_MESSAGES + +export class AddressError extends BaseError { + constructor(payload: BaseErrorPayload) { + super(payload, ADDRESS_ERROR_MESSAGES) + this.name = "AddressError" + } +} diff --git a/packages/shared/src/errors/baseErrors.ts b/packages/shared/src/errors/baseErrors.ts new file mode 100644 index 000000000..954ab4241 --- /dev/null +++ b/packages/shared/src/errors/baseErrors.ts @@ -0,0 +1,40 @@ +import { JsonArray, JsonObject } from "type-fest" + +export type JsonValue = + | string + | number + | boolean + | null + | JsonObject + | JsonArray + +export interface BaseErrorPayload { + message?: string + options?: { error?: unknown; context?: JsonValue } + code?: T +} + +export class BaseError extends Error { + public readonly context?: JsonValue + public readonly code?: T + + constructor( + { code, message, options = {} }: BaseErrorPayload, + private errorMessages?: { [key in T]: string }, + ) { + const { error, context } = options + super(message, { cause: error }) + this.name = "BaseError" + this.context = context + this.code = code + this.setMessageByCode(code, message) + } + + private setMessageByCode(code: T | undefined, message: string | undefined) { + if (!code || !this.errorMessages || message) { + return + } + + this.message = this.errorMessages[code] + } +} diff --git a/packages/shared/src/errors/call.ts b/packages/shared/src/errors/call.ts new file mode 100644 index 000000000..f45d0555d --- /dev/null +++ b/packages/shared/src/errors/call.ts @@ -0,0 +1,14 @@ +import { BaseError, BaseErrorPayload } from "./baseErrors" + +export enum CALL_ERROR_MESSAGES { + NOT_VALID = "Invalid call", +} + +export type CallErrorMessage = keyof typeof CALL_ERROR_MESSAGES + +export class CallError extends BaseError { + constructor(payload: BaseErrorPayload) { + super(payload, CALL_ERROR_MESSAGES) + this.name = "CallError" + } +} diff --git a/packages/shared/src/errors/network.ts b/packages/shared/src/errors/network.ts new file mode 100644 index 000000000..afeafb049 --- /dev/null +++ b/packages/shared/src/errors/network.ts @@ -0,0 +1,15 @@ +import { BaseError, BaseErrorPayload } from "./baseErrors" + +export enum NETWORK_ERROR_MESSAGES { + NO_NETWORK_OR_MULTICALL = "Missing networkId or multicall", + NOT_FOUND = "Network not found", +} + +export type NetworkErrorMessage = keyof typeof NETWORK_ERROR_MESSAGES + +export class NetworkError extends BaseError { + constructor(payload: BaseErrorPayload) { + super(payload, NETWORK_ERROR_MESSAGES) + this.name = "NetworkError" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 65da802fe..63eab8028 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -10,3 +10,5 @@ export * from "./chains" export * as bigDecimal from "./bigdecimal" export * from "./hooks" export * from "./knownDapps" + +export type { BigDecimal } from "./bigdecimal/types" diff --git a/packages/shared/src/knownDapps/model.ts b/packages/shared/src/knownDapps/model.ts index 5b352b002..52e0b87b1 100644 --- a/packages/shared/src/knownDapps/model.ts +++ b/packages/shared/src/knownDapps/model.ts @@ -8,12 +8,14 @@ export const dappLinksSchema = z.array( }), ) -export const dappContractsSchema = z.array( - z.object({ - address: z.string(), - chain: z.string(), - }), -) +export const dappContractsSchema = z + .array( + z.object({ + address: z.string(), + chain: z.string(), + }), + ) + .optional() export const knownDappSchema = z.object({ dappId: z.string(), diff --git a/packages/shared/src/tokens/balances.ts b/packages/shared/src/tokens/balances.ts index 95a660f26..1c5345b6e 100644 --- a/packages/shared/src/tokens/balances.ts +++ b/packages/shared/src/tokens/balances.ts @@ -1,5 +1,4 @@ -import { Multicall } from "@argent/x-multicall" -import { number, uint256 } from "starknet" +import { number, uint256, ProviderInterface } from "starknet" import tokens from "../assets/tokens.json" import { Address } from "../chains" @@ -7,7 +6,7 @@ import { Token, TokenWithBalance } from "./token" export const getTokensBalances = async ( networkId: string, - multicallProvider: Multicall, + provider: Pick, address: Address, ): Promise => { const filtered = tokens.filter( @@ -16,7 +15,7 @@ export const getTokensBalances = async ( const res = await Promise.allSettled( filtered.map((token) => - multicallProvider.call({ + provider.callContract({ contractAddress: token.address, entrypoint: "balanceOf", calldata: [address], @@ -27,7 +26,7 @@ export const getTokensBalances = async ( return res.reduce((accumulator, r, i) => { if ( r.status === "rejected" || - (r.value[0] === "0x0" && r.value[1] === "0x0") + (r.value.result[0] === "0x0" && r.value.result[1] === "0x0") ) { return accumulator } @@ -35,8 +34,8 @@ export const getTokensBalances = async ( const balance = BigInt( number.toHex( uint256.uint256ToBN({ - low: r.value[0], - high: r.value[1], + low: r.value.result[0], + high: r.value.result[1], }), ), ) diff --git a/packages/shared/src/transactions/amount.ts b/packages/shared/src/transactions/amount.ts index 2407f6df3..d58cdf38e 100644 --- a/packages/shared/src/transactions/amount.ts +++ b/packages/shared/src/transactions/amount.ts @@ -18,7 +18,7 @@ export const inputAmountSchema = z .refine( (amount) => { try { - const bn = parseAmount(amount) + const bn = parseAmount(amount).value if (bn < 0n) { throw new Error("Amount must be positive") } diff --git a/packages/shared/src/transactions/useAggregatedSimData.ts b/packages/shared/src/transactions/useAggregatedSimData.ts index 7741ba5ff..2689e4f7f 100644 --- a/packages/shared/src/transactions/useAggregatedSimData.ts +++ b/packages/shared/src/transactions/useAggregatedSimData.ts @@ -248,10 +248,10 @@ export const useAggregatedSimData = ({ const isTokenTranfer = checkIsTokenTransfer(t) if (isTokenTranfer && t.from === account?.address) { - return acc - parseCurrency(t.usdValue) + return acc - parseCurrency(t.usdValue).value } - return acc + parseCurrency(t.usdValue) + return acc + parseCurrency(t.usdValue).value }, ZERO) const usdValue = formatCurrency(usdValueBigInt) diff --git a/packages/shared/src/utils/arrays.test.ts b/packages/shared/src/utils/arrays.test.ts new file mode 100644 index 000000000..f5a63cbdb --- /dev/null +++ b/packages/shared/src/utils/arrays.test.ts @@ -0,0 +1,46 @@ +import { describe, expect } from "vitest" +import { ensureArray } from "./arrays" + +describe("arrays", () => { + describe("ensureArray", () => { + it("should return an empty array, given undefined", () => { + const result = ensureArray(undefined) + expect(result).toEqual([]) + }) + + it("should return the input array as is, given a valid array", () => { + const inputArray = [1, 2, 3] + const result = ensureArray(inputArray) + expect(result).toEqual(inputArray) + }) + + it("should wrap a non-array value in an array, given a non-array value", () => { + const nonArrayValue = "Not an array" + const result = ensureArray(nonArrayValue) + expect(result).toEqual([nonArrayValue]) + }) + + it("should not modify an already wrapped value, given a valid single element array", () => { + const wrappedValue = [42] + const result = ensureArray(wrappedValue) + expect(result).toEqual(wrappedValue) + }) + + it("should return an empty array, given null", () => { + const nullValue = null + const result = ensureArray(nullValue) + expect(result).toEqual([]) + }) + + it("should wrap an empty object in an array, given an empty object", () => { + const emptyObject = {} + const result = ensureArray(emptyObject) + expect(result).toEqual([{}]) + }) + + it("should wrap false an array, given false", () => { + const result = ensureArray(false) + expect(result).toEqual([false]) + }) + }) +}) diff --git a/packages/shared/src/utils/arrays.ts b/packages/shared/src/utils/arrays.ts new file mode 100644 index 000000000..d9c666bf4 --- /dev/null +++ b/packages/shared/src/utils/arrays.ts @@ -0,0 +1,16 @@ +export const ensureArray = (maybeArray: T | T[] | undefined): T[] => { + /** + * Ensures that the input value is always returned as an array, even if it's already an array, + * a single value, or undefined. This function is useful for standardizing input values to arrays. + * + * @param maybeArray - The input value that should be converted to an array if necessary. + * @returns An array containing the input value, or an empty array if the input is undefined or null. + */ + if (maybeArray === undefined || maybeArray === null) { + return [] + } + if (Array.isArray(maybeArray)) { + return maybeArray + } + return [maybeArray] +} diff --git a/packages/shared/src/utils/avatarImage.test.ts b/packages/shared/src/utils/avatarImage.test.ts index 68968f9e3..6dc89ac50 100644 --- a/packages/shared/src/utils/avatarImage.test.ts +++ b/packages/shared/src/utils/avatarImage.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from "vitest" -import { getInitials } from "./avatarImage" +import { getInitials } from "./initials" describe("avatarImage", () => { describe("getInitials", () => { describe("when valid", () => { - test("should return uppercase initials up to two characters ", () => { + test("should return uppercase initials up to two characters", () => { expect(getInitials("f")).toEqual("F") expect(getInitials("foo")).toEqual("FO") expect(getInitials(" foo ")).toEqual("FO") @@ -17,11 +17,24 @@ describe("avatarImage", () => { expect(getInitials("FooBar")).toEqual("FB") expect(getInitials("FooBarQuxZod")).toEqual("FZ") expect(getInitials("Account 1")).toEqual("A1") - expect(getInitials("Account 10")).toEqual("A1") expect(getInitials("Account 9")).toEqual("A9") expect(getInitials("€ 9")).toEqual("€9") expect(getInitials("€ 9", true)).toEqual("9") }) + test("when ending with a whole number, include the number up to 2 characters", () => { + expect(getInitials("1")).toEqual("1") + expect(getInitials("10")).toEqual("10") + expect(getInitials("100")).toEqual("10") + expect(getInitials("ABC1")).toEqual("A1") + expect(getInitials("ABC10")).toEqual("A10") + expect(getInitials("ABC100")).toEqual("A10") + expect(getInitials("1ABC1")).toEqual("11") + expect(getInitials("1ABC10")).toEqual("110") + expect(getInitials("1ABC100")).toEqual("110") + expect(getInitials("Account 1")).toEqual("A1") + expect(getInitials("Account 10")).toEqual("A10") + expect(getInitials("Account 100")).toEqual("A10") + }) }) describe("when invalid", () => { test("should return an empty string", () => { diff --git a/packages/shared/src/utils/avatarImage.ts b/packages/shared/src/utils/avatarImage.ts index dc06f8bfc..c81bf58eb 100644 --- a/packages/shared/src/utils/avatarImage.ts +++ b/packages/shared/src/utils/avatarImage.ts @@ -1,24 +1,9 @@ -import { ethers } from "ethers" -import { isString, upperCase } from "lodash-es" import { num } from "starknet" +import { getInitials } from "./initials" +import { id } from "./id" const { toBigInt } = num -export const getInitials = (name: string, alphanumeric = false) => { - if (!isString(name)) { - return "" - } - const filtered = alphanumeric ? name.replace(/[^0-9a-z ]/gi, "") : name - const uppercase = upperCase(filtered) - const uppercaseElements = uppercase.split(" ") - - if (uppercaseElements.length === 1) { - return uppercaseElements[0].substring(0, 2) - } - const initials = uppercaseElements.map((n) => n[0]) - return [initials[0], initials[initials.length - 1]].join("") -} - const parseColor = (color: string) => { const hex = color.replace("#", "") if (!/^[0-9A-F]{6}$/i.test(hex)) { @@ -38,11 +23,13 @@ export const generateAvatarImage = ( // get alphanumeric initials (to avoid issues with being outside range of btoa) const initials = getInitials(name, true) + // note it's not possible to use Barlow here without embedding the file directly into the svg @see https://css-tricks.com/using-custom-fonts-with-svg-in-an-image-tag/ + // generate 64x64 svg with initials in the center (horizontal and vertical) with font color and background color and font family Helvetica (plus fallbacks) - const svg = ` - - ${initials} - ` + const svg = ` + + ${initials} + ` return `data:image/svg+xml;base64,${btoa(svg)}` } @@ -58,7 +45,7 @@ const argentColorsArray = [ ] export const getColor = (name: string) => { - const hash = ethers.utils.id(name).slice(-2) + const hash = id(name).slice(-2) const index = parseInt(hash, 16) % argentColorsArray.length return argentColorsArray[index] } diff --git a/packages/shared/src/utils/base58.test.ts b/packages/shared/src/utils/base58.test.ts new file mode 100644 index 000000000..54a276be9 --- /dev/null +++ b/packages/shared/src/utils/base58.test.ts @@ -0,0 +1,53 @@ +import { test } from "vitest" +import { + encodeBase58, + encodeBase58Array, + decodeBase58, + decodeBase58Array, +} from "./base58" + +test("encodeBase58", () => { + const val = 12n + expect(encodeBase58(val)).toMatchInlineSnapshot('"D"') + const bigVal = BigInt(Number.MAX_SAFE_INTEGER) + expect(encodeBase58(bigVal)).toMatchInlineSnapshot('"2DLNrMSKug"') + expect(() => encodeBase58(-1n)).toThrow() + expect(encodeBase58(0n)).toMatchInlineSnapshot('"1"') +}) + +test("encodeBase58Array", () => { + const arr = [12n, 15n, 18n] + expect(encodeBase58Array(arr)).toMatchInlineSnapshot(` + [ + "D", + "G", + "K", + ] + `) + const bigArr = [BigInt(Number.MAX_SAFE_INTEGER), 0n, -1n] + expect(() => encodeBase58Array(bigArr)).toThrow() +}) + +test("decodeBase58", () => { + const val = "D" + expect(decodeBase58(val)).toMatchInlineSnapshot('"0x0c"') + const bigVal = "1AVQH8GD6PWRMKGZ6JPZLR8HVGF1P8ZZZ" + expect(decodeBase58(bigVal)).toMatchInlineSnapshot( + '"0x0001cb73b00deccc9252827587d5641cc7535d2d88eb7f1f58"', + ) + expect(() => decodeBase58("-1")).toThrow() + expect(() => decodeBase58("0")).toThrow() +}) + +test("decodeBase58Array", () => { + const arr = ["D", "G", "K"] + expect(decodeBase58Array(arr)).toMatchInlineSnapshot(` + [ + "0x0c", + "0x0f", + "0x12", + ] + `) + const bigArr = ["1AVQH8GD6PWRMKGZ6JPZLR8HVGF1P8ZZZ", "0", "-1"] + expect(() => decodeBase58Array(bigArr)).toThrow() +}) diff --git a/packages/shared/src/utils/base58.ts b/packages/shared/src/utils/base58.ts index 14f283ab2..30317013e 100644 --- a/packages/shared/src/utils/base58.ts +++ b/packages/shared/src/utils/base58.ts @@ -1,10 +1,10 @@ -import { utils } from "ethers" +import { base58, hex } from "@scure/base" import { BigNumberish, encode, num } from "starknet" export const encodeBase58 = (val: BigNumberish) => { - const bytes = encode.sanitizeHex(num.toHex(val)) - const base58 = utils.base58.encode(bytes) - return base58 + const hexValue = encode.removeHexPrefix(encode.sanitizeHex(num.toHex(val))) + const bytesValue = hex.decode(hexValue) + return base58.encode(bytesValue) } export const encodeBase58Array = (arr: BigNumberish[]) => { @@ -12,9 +12,9 @@ export const encodeBase58Array = (arr: BigNumberish[]) => { } export const decodeBase58 = (val: string) => { - const bytes = utils.base58.decode(val) - const hex = encode.sanitizeHex(utils.hexlify(bytes)) - return hex + const bytesValue = base58.decode(val) + const hexValue = encode.sanitizeHex(hex.encode(bytesValue)) + return hexValue } export const decodeBase58Array = (arr: string[]) => { diff --git a/packages/shared/src/utils/id.test.ts b/packages/shared/src/utils/id.test.ts new file mode 100644 index 000000000..59670d238 --- /dev/null +++ b/packages/shared/src/utils/id.test.ts @@ -0,0 +1,9 @@ +import { test } from "vitest" +import { id } from "./id" + +test("id", () => { + const identifier = id("hello world") + expect(identifier).toMatchInlineSnapshot( + '"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"', + ) +}) diff --git a/packages/shared/src/utils/id.ts b/packages/shared/src/utils/id.ts new file mode 100644 index 000000000..44c5c279d --- /dev/null +++ b/packages/shared/src/utils/id.ts @@ -0,0 +1,17 @@ +import { keccak_256 } from "@noble/hashes/sha3" +import { utf8ToBytes, bytesToHex } from "@noble/hashes/utils" + +/** + * A simple hashing function which operates on UTF-8 strings to + * compute an 32-byte identifier. + * + * This simply computes the [UTF-8 bytes](toUtf8Bytes) and computes + * the [[keccak256]]. + * + * @example: + * id("hello world") + * //_result: "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" + */ +export function id(value: string): string { + return `0x${bytesToHex(keccak_256(utf8ToBytes(value)))}` +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 976ee3fbb..e141f7952 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -11,3 +11,6 @@ export * from "./readFileAsString" export * from "./schemas" export * from "./transactions" export * from "./useEventEmitter" +export * from "./initials" +export * from "./id" +export * from "./arrays" diff --git a/packages/shared/src/utils/initials.ts b/packages/shared/src/utils/initials.ts new file mode 100644 index 000000000..d9aabf103 --- /dev/null +++ b/packages/shared/src/utils/initials.ts @@ -0,0 +1,39 @@ +import { isString, upperCase } from "lodash-es" + +const getWholeNumberAtEnd = (inputString: string) => { + if (!isString(inputString)) { + return undefined + } + const regex = /(\d+)$/ + const match = inputString.match(regex) + if (match) { + return match[1] + } + return undefined +} + +export const getInitials = (name: string, alphanumeric = false) => { + if (!isString(name)) { + return "" + } + const filtered = alphanumeric ? name.replace(/[^0-9a-z ]/gi, "") : name + const uppercase = upperCase(filtered) + const uppercaseElements = uppercase.split(" ") + const wholeNumberAtEnd = getWholeNumberAtEnd(filtered) + + const initials = uppercaseElements.map((n) => n[0]) + + /** if it ends with a whole number, include that up to 2 characters */ + if (wholeNumberAtEnd && wholeNumberAtEnd.length > 1) { + if (wholeNumberAtEnd.length < filtered.length) { + return [initials[0], wholeNumberAtEnd.substring(0, 2)].join("") + } + } + + /** if it's a single string, return first two characters */ + if (uppercaseElements.length === 1) { + return uppercaseElements[0].substring(0, 2) + } + + return [initials[0], initials[initials.length - 1]].join("") +} diff --git a/packages/shared/src/utils/number.ts b/packages/shared/src/utils/number.ts index 0169a45f9..5fccc6ed0 100644 --- a/packages/shared/src/utils/number.ts +++ b/packages/shared/src/utils/number.ts @@ -72,8 +72,8 @@ export const prettifyNumber = ( // If number is greater than or equal to 1, we format with minimum decimal places if (parseFloat(numberString) >= 1) { - const numberBN = parseUnits(numberString, minDecimalPlaces) - untrimmed = formatUnits(numberBN, minDecimalPlaces) + const numberBN = parseUnits(numberString, minDecimalPlaces).value + untrimmed = formatUnits({ value: numberBN, decimals: minDecimalPlaces }) } else { // For numbers less than 1, determine the number of leading zeros after the decimal point const leadingZerosInDecimalPart = @@ -86,8 +86,8 @@ export const prettifyNumber = ( ) // Format the number with the required decimal places - const numberBN = parseUnits(numberString, prettyDecimalPlaces) - untrimmed = formatUnits(numberBN, prettyDecimalPlaces) + const numberBN = parseUnits(numberString, prettyDecimalPlaces).value + untrimmed = formatUnits({ value: numberBN, decimals: prettyDecimalPlaces }) } // Split the number into integer and fraction parts diff --git a/packages/shared/src/utils/parseAmount.ts b/packages/shared/src/utils/parseAmount.ts index 4f4e9ec88..04d04f392 100644 --- a/packages/shared/src/utils/parseAmount.ts +++ b/packages/shared/src/utils/parseAmount.ts @@ -12,7 +12,7 @@ export const parseAmountValue = ( return 0n } - return parseUnits(amountNoComma, Number(decimals)) + return parseUnits(amountNoComma, Number(decimals)).value } export function getUint256CalldataFromBN(bn: BigNumberish) { return uint256.bnToUint256(bn) diff --git a/packages/shared/vite.config.ts b/packages/shared/vite.config.ts index 1cc53aa6d..a91e2393a 100644 --- a/packages/shared/vite.config.ts +++ b/packages/shared/vite.config.ts @@ -34,4 +34,13 @@ export default defineConfig({ insertTypesEntry: true, }), ], + test: { + deps: { + optimizer: { + web: { + enabled: false, + }, + }, + }, + }, }) diff --git a/packages/stack-router/package.json b/packages/stack-router/package.json index c5d6dee44..3e9576cf8 100644 --- a/packages/stack-router/package.json +++ b/packages/stack-router/package.json @@ -48,7 +48,7 @@ "typescript": "^5.0.4", "vite": "^4.3.8", "vite-plugin-dts": "^3.0.0", - "vitest": "^0.33.0" + "vitest": "^0.34.0" }, "peerDependencies": { "colord": "^2.9.2", diff --git a/packages/starknet-react-webwallet-connector/package.json b/packages/starknet-react-webwallet-connector/package.json index 449a5ef02..5b4842a8c 100644 --- a/packages/starknet-react-webwallet-connector/package.json +++ b/packages/starknet-react-webwallet-connector/package.json @@ -47,7 +47,7 @@ "vite-plugin-dts": "^3.0.0" }, "peerDependencies": { - "starknet": "5.18.0" + "starknet": "5.19.5" }, "gitHead": "b16688a8638cc138938e74e1a39d004760165d75" } diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 770265e42..c152fcede 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -4,6 +4,7 @@ "private": true, "devDependencies": { "@argent-x/extension": "^6.3.1", + "@argent/shared": "^6.3.1", "@argent/ui": "^6.3.1", "@babel/core": "^7.18.5", "@babel/preset-env": "^7.21.4", @@ -11,25 +12,25 @@ "@babel/preset-typescript": "^7.21.4", "@chakra-ui/react": "^2.6.1", "@chakra-ui/storybook-addon": "^5.0.1", - "@storybook/addon-actions": "^7.4.5", - "@storybook/addon-essentials": "^7.4.5", - "@storybook/addon-interactions": "^7.4.5", - "@storybook/addon-links": "^7.4.5", - "@storybook/addon-mdx-gfm": "^7.4.5", - "@storybook/nextjs": "^7.4.5", - "@storybook/react": "^7.4.5", - "@storybook/testing-library": "^0.2.1", + "@storybook/addon-actions": "^7.4.6", + "@storybook/addon-essentials": "^7.4.6", + "@storybook/addon-interactions": "^7.4.6", + "@storybook/addon-links": "^7.4.6", + "@storybook/addon-mdx-gfm": "^7.4.6", + "@storybook/nextjs": "^7.4.6", + "@storybook/react": "^7.4.6", + "@storybook/testing-library": "^0.2.2", "@swc/core": "^1.3.57", "@types/dotenv-webpack": "^7.0.4", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "eslint": "^8.7.0", - "eslint-plugin-storybook": "^0.6.14", + "eslint-plugin-storybook": "^0.6.15", "lodash-es": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", - "starknet": "5.18.0", - "storybook": "^7.4.5", + "starknet": "5.19.5", + "storybook": "^7.4.6", "storybook-addon-swc": "^1.2.0", "typescript": "^5.0.4" }, diff --git a/packages/storybook/src/features/accounts/AccountListItem.stories.tsx b/packages/storybook/src/features/accounts/AccountListItem.stories.tsx index 4d4937935..4eb1a2709 100644 --- a/packages/storybook/src/features/accounts/AccountListItem.stories.tsx +++ b/packages/storybook/src/features/accounts/AccountListItem.stories.tsx @@ -17,7 +17,7 @@ export default { } const account = { - accountName: "Account 1 Lorem Ipsum Dolor Sit Amet", + accountName: "Account Lorem Ipsum Dolor Sit Amet 10", accountAddress: "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a", networkId: "goerli-alpha", diff --git a/packages/storybook/src/features/actions/AddTokenScreen.stories.tsx b/packages/storybook/src/features/actions/AddTokenScreen.stories.tsx index d0c5d51ab..4f1ea6bde 100644 --- a/packages/storybook/src/features/actions/AddTokenScreen.stories.tsx +++ b/packages/storybook/src/features/actions/AddTokenScreen.stories.tsx @@ -1,4 +1,4 @@ -import { parsedDefaultTokens } from "@argent-x/extension/src/shared/token/utils" +import { parsedDefaultTokens } from "@argent-x/extension/src/shared/token/__new/utils" import { AddTokenScreen } from "@argent-x/extension/src/ui/features/actions/AddTokenScreen" import { decorators } from "../../decorators/routerDecorators" diff --git a/packages/storybook/src/features/funding/FundingQrCodeScreen.stories.tsx b/packages/storybook/src/features/funding/FundingQrCodeScreen.stories.tsx new file mode 100644 index 000000000..ba61a6335 --- /dev/null +++ b/packages/storybook/src/features/funding/FundingQrCodeScreen.stories.tsx @@ -0,0 +1,26 @@ +import { FundingQrCodeScreen } from "@argent-x/extension/src/ui/features/funding/FundingQrCodeScreen" + +import { decorators } from "../../decorators/routerDecorators" + +export default { + component: FundingQrCodeScreen, + decorators, + parameters: { + layout: "fullscreen", + }, +} + +export const Default = { + args: { + accountName: "Account 1", + accountAddress: + "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a", + }, +} + +export const ShortAddress = { + args: { + accountName: "Account 123", + accountAddress: "0x123", + }, +} diff --git a/packages/storybook/src/tokens.ts b/packages/storybook/src/tokens.ts index ab75cecb8..054c7aa66 100644 --- a/packages/storybook/src/tokens.ts +++ b/packages/storybook/src/tokens.ts @@ -1,17 +1,15 @@ -import { Token } from "@argent-x/extension/src/shared/token/type" import { getFeeToken, parsedDefaultTokens, -} from "@argent-x/extension/src/shared/token/utils" -import { TokenDetailsWithBalance } from "@argent-x/extension/src/ui/features/accountTokens/tokens.state" +} from "@argent-x/extension/src/shared/token/__new/utils" -export const tokensByNetwork: Token[] = parsedDefaultTokens.filter( +export const tokensByNetwork = parsedDefaultTokens.filter( ({ networkId }) => networkId === "goerli-alpha", ) -export const feeToken = getFeeToken("goerli-alpha") as Token +export const feeToken = getFeeToken("goerli-alpha") -export const tokenWithSymbol = (symbol: string): Token => { +export const tokenWithSymbol = (symbol: string) => { const token = parsedDefaultTokens.find((token) => token.symbol === symbol) if (!token) { throw `No token found for symbol ${symbol}` @@ -24,7 +22,7 @@ const ethToken = tokenWithSymbol("ETH") export const tokenWithBalance = ( balance?: number | string, token = ethToken, -): TokenDetailsWithBalance => { +) => { return { ...token, balance: balance ? BigInt(balance) : undefined, diff --git a/packages/storybook/src/ui/components/AppBackgroundError.stories.tsx b/packages/storybook/src/ui/components/AppBackgroundError.stories.tsx new file mode 100644 index 000000000..799e8f8bd --- /dev/null +++ b/packages/storybook/src/ui/components/AppBackgroundError.stories.tsx @@ -0,0 +1,14 @@ +import { AppBackgroundError } from "@argent-x/extension/src/ui/AppBackgroundError" +import { decorators } from "../../decorators/routerDecorators" + +export default { + component: AppBackgroundError, + decorators, + parameters: { + layout: "fullscreen", + }, +} + +export const Default = { + args: {}, +} diff --git a/packages/storybook/src/utils/generateAvatarImage.stories.tsx b/packages/storybook/src/utils/generateAvatarImage.stories.tsx new file mode 100644 index 000000000..c69b5fa9b --- /dev/null +++ b/packages/storybook/src/utils/generateAvatarImage.stories.tsx @@ -0,0 +1,33 @@ +import { generateAvatarImage } from "@argent/shared" +import { Box, Img, SimpleGrid } from "@chakra-ui/react" +import { PropsWithChildren } from "react" + +export default { + component: Box, + render: (props: PropsWithChildren) => , +} + +export const Examples = { + args: { + children: ( + + + + + + + ), + }, +} diff --git a/packages/swap/package.json b/packages/swap/package.json index 13ab82da0..8f533298b 100644 --- a/packages/swap/package.json +++ b/packages/swap/package.json @@ -23,8 +23,9 @@ "lint": "eslint . --cache --ext .ts,.tsx --fix" }, "dependencies": { - "@argent/x-multicall": "^6.3.0", + "@argent/x-multicall": "^7.0.8", "@chakra-ui/react": "^2.6.1", + "@ethersproject/units": "^5.7.0", "big.js": "^6.2.1", "decimal.js-light": "^2.5.1", "jsbi": "^4.3.0", @@ -47,11 +48,11 @@ "react-dom": "^18.2.0", "typescript": "^5.0.4", "vite": "^4.3.8", - "vite-plugin-dts": "3.5.4" + "vite-plugin-dts": "3.6.0" }, "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "starknet": "5.18.0" + "starknet": "5.19.5" } } diff --git a/packages/swap/src/lib/constants.ts b/packages/swap/src/lib/constants.ts index 0a6eed562..dce6bc97d 100644 --- a/packages/swap/src/lib/constants.ts +++ b/packages/swap/src/lib/constants.ts @@ -27,7 +27,6 @@ type ChainTokenList = { const WETH_ONLY: ChainTokenList = { [SupportedNetworks.MAINNET]: [WETH[SupportedNetworks.MAINNET]], [SupportedNetworks.TESTNET]: [WETH[SupportedNetworks.TESTNET]], - [SupportedNetworks.TESTNET2]: [WETH[SupportedNetworks.TESTNET2]], } export const DAI = { diff --git a/packages/swap/src/lib/hooks/Reserves.ts b/packages/swap/src/lib/hooks/Reserves.ts index b21c8de43..100747d60 100644 --- a/packages/swap/src/lib/hooks/Reserves.ts +++ b/packages/swap/src/lib/hooks/Reserves.ts @@ -46,7 +46,7 @@ export function useReserves(pairAddresses: (string | undefined)[]) { const promises = pairAddresses.map((pairAddress) => pairAddress - ? multicall.call({ + ? multicall.callContract({ contractAddress: pairAddress, entrypoint: "get_reserves", }) @@ -61,8 +61,8 @@ export function useReserves(pairAddresses: (string | undefined)[]) { } return { - reserve0: uint256ToHex({ low: res[0], high: res[1] }), - reserve1: uint256ToHex({ low: res[2], high: res[3] }), + reserve0: uint256ToHex({ low: res.result[0], high: res.result[1] }), + reserve1: uint256ToHex({ low: res.result[2], high: res.result[3] }), } as Reserves }) }) diff --git a/packages/swap/src/lib/hooks/Tokens.ts b/packages/swap/src/lib/hooks/Tokens.ts index 977c59074..3358e8f65 100644 --- a/packages/swap/src/lib/hooks/Tokens.ts +++ b/packages/swap/src/lib/hooks/Tokens.ts @@ -1,11 +1,9 @@ -// import { parseBytes32String } from "@ethersproject/strings" import { useMemo } from "react" import { Currency, ETHER, Token } from "../../sdk" import { validateAndParseAddress } from "../../sdk/utils" import { useArgentTokenList } from "../../tokenlist" import { useSwapProvider } from "../providers" -import { useNetworkIdFromChainId } from "../services/network" export function useAllTokens(): { [address: string]: Token } { const { selectedAccount, networkId } = useSwapProvider() diff --git a/packages/swap/src/lib/hooks/Wallet.ts b/packages/swap/src/lib/hooks/Wallet.ts index 1da64ccaa..0779ba6da 100644 --- a/packages/swap/src/lib/hooks/Wallet.ts +++ b/packages/swap/src/lib/hooks/Wallet.ts @@ -42,7 +42,7 @@ export function useMultipleTokenBalances( const promises = tokenAddresses.map((pairAddress) => pairAddress - ? multicall.call({ + ? multicall.callContract({ contractAddress: pairAddress, entrypoint: "balanceOf", calldata: [accountAddress], @@ -57,7 +57,7 @@ export function useMultipleTokenBalances( return undefined } - return uint256ToHex({ low: res[0], high: res[1] }) + return uint256ToHex({ low: res.result[0], high: res.result[1] }) }) }) diff --git a/packages/swap/src/lib/hooks/useMulticall.ts b/packages/swap/src/lib/hooks/useMulticall.ts index bbc3a147e..57bbe9e45 100644 --- a/packages/swap/src/lib/hooks/useMulticall.ts +++ b/packages/swap/src/lib/hooks/useMulticall.ts @@ -24,12 +24,13 @@ export const useMulticall = (call: Call, unique = false) => { ...rest } = useSWR( key, - () => { + async () => { if (!multicall) { throw new NoMulticallError("Multicall not available") } - return multicall.call(call) + const { result } = await multicall.callContract(call) + return result }, { shouldRetryOnError: (err) => { diff --git a/packages/swap/src/lib/hooks/useSwapCallback.ts b/packages/swap/src/lib/hooks/useSwapCallback.ts index 3b6e6b17b..1be885355 100644 --- a/packages/swap/src/lib/hooks/useSwapCallback.ts +++ b/packages/swap/src/lib/hooks/useSwapCallback.ts @@ -62,7 +62,7 @@ function useSwapCallArguments( feeOnTransfer: false, allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), recipient, - deadline: deadline.toNumber(), + deadline, }), ) @@ -72,7 +72,7 @@ function useSwapCallArguments( feeOnTransfer: true, allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), recipient, - deadline: deadline.toNumber(), + deadline, }), ) } diff --git a/packages/swap/src/lib/hooks/useTransactionDeadline.ts b/packages/swap/src/lib/hooks/useTransactionDeadline.ts index 272f3e77d..21d67d87b 100644 --- a/packages/swap/src/lib/hooks/useTransactionDeadline.ts +++ b/packages/swap/src/lib/hooks/useTransactionDeadline.ts @@ -1,15 +1,15 @@ -import { BigNumber } from "ethers" import { useMemo } from "react" import { DEFAULT_DEADLINE_FROM_NOW } from "../constants" import useCurrentBlockTimestamp from "./useCurrentBlockTimestamp" // combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction -export default function useTransactionDeadline(): BigNumber | undefined { +export default function useTransactionDeadline(): number | undefined { const blockTimestamp = useCurrentBlockTimestamp() return useMemo(() => { if (blockTimestamp) { - return BigNumber.from(blockTimestamp).add(DEFAULT_DEADLINE_FROM_NOW) + // normal number is enough here, as timestamps wont be bigger + return blockTimestamp + DEFAULT_DEADLINE_FROM_NOW } return undefined }, [blockTimestamp]) diff --git a/packages/swap/src/lib/providers/types.ts b/packages/swap/src/lib/providers/types.ts index 97bd5ac19..17eeee9ff 100644 --- a/packages/swap/src/lib/providers/types.ts +++ b/packages/swap/src/lib/providers/types.ts @@ -1,5 +1,4 @@ -import { Multicall } from "@argent/x-multicall" - +import { ProviderInterface } from "starknet" import { SupportedNetworks } from "../../sdk" interface BaseWalletAccount { @@ -7,14 +6,16 @@ interface BaseWalletAccount { networkId: string } +type MinimalProvider = Pick + export interface SwapContextInterface { selectedAccount?: BaseWalletAccount - multicall?: Multicall + multicall?: MinimalProvider networkId?: SupportedNetworks } export interface SwapProviderArgs { selectedAccount?: BaseWalletAccount - multicall?: Multicall + multicall?: MinimalProvider children: React.ReactNode } diff --git a/packages/swap/src/lib/services/multicall.ts b/packages/swap/src/lib/services/multicall.ts index b4ff57cd9..727a0f995 100644 --- a/packages/swap/src/lib/services/multicall.ts +++ b/packages/swap/src/lib/services/multicall.ts @@ -1,8 +1,9 @@ -import { Multicall } from "@argent/x-multicall" +import { getBatchProvider } from "@argent/x-multicall" import { memoize } from "lodash-es" import { SupportedNetworks } from "../../sdk" import { getProviderForNetworkId } from "./provider" +import type { ProviderInterface } from "starknet" export class NoMulticallError extends Error { constructor(message?: string) { @@ -12,9 +13,8 @@ export class NoMulticallError extends Error { } export const getMulticallForNetwork = memoize( - (networkId: SupportedNetworks) => { - const multicall = new Multicall(getProviderForNetworkId(networkId)) - return multicall + (networkId: SupportedNetworks): Pick => { + return getBatchProvider(getProviderForNetworkId(networkId)) }, (networkId) => networkId, ) diff --git a/packages/ui/package.json b/packages/ui/package.json index 8d9c9adbc..b4be88e9e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -32,14 +32,13 @@ "eslint": "^8.7.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.6.0", - "ethers": "^6.3.0", "jsdom": "^22.0.0", "rollup-plugin-visualizer": "^5.9.0", "ts-custom-error": "^3.3.1", "typescript": "^5.0.4", "vite": "^4.3.8", "vite-plugin-dts": "^3.0.0", - "vitest": "^0.33.0" + "vitest": "^0.34.0" }, "scripts": { "gen:theme-typings": "chakra-cli tokens ./src/theme/index.tsx", @@ -57,7 +56,7 @@ "@chakra-ui/system": "^2.5.7", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", - "@ethersproject/wordlists": "^5.7.0", + "@scure/bip39": "^1.2.1", "framer-motion": "^10.0.0", "popmotion": "^11.0.5" }, @@ -65,7 +64,6 @@ "@zxcvbn-ts/language-common": "^2.0.1 || ^3.0.0", "@zxcvbn-ts/language-en": "^2.1.0", "colord": "^2.9.2", - "ethers": "^6.3.0", "lodash-es": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index e94c4f130..eb3d62aa9 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -116,6 +116,19 @@ export const buttonTheme = defineStyleConfig({ bg: mode(`gray.100`, `neutrals.800`)(props), }, } + } else if (c === "inverted-primary") { + return { + bg: "transparent", + border: "solid", + borderColor: `primary.500`, + color: `primary.500`, + _hover: { + bg: `gray.50`, + }, + _active: { + bg: `primary.600`, + }, + } } /** same for dark or light mode */ diff --git a/packages/ui/src/components/Empty.tsx b/packages/ui/src/components/Empty.tsx index 2e3ada135..77bd4cc09 100644 --- a/packages/ui/src/components/Empty.tsx +++ b/packages/ui/src/components/Empty.tsx @@ -21,7 +21,7 @@ export const Empty: FC = ({ {icon} -
+
{title}
{children} diff --git a/packages/ui/src/components/SeedInput.tsx b/packages/ui/src/components/SeedInput.tsx index 950c2653c..2bac434c1 100644 --- a/packages/ui/src/components/SeedInput.tsx +++ b/packages/ui/src/components/SeedInput.tsx @@ -6,7 +6,7 @@ import { SimpleGrid, SimpleGridProps, } from "@chakra-ui/react" -import { wordlists } from "@ethersproject/wordlists" +import { wordlist } from "@scure/bip39/wordlists/english" import { FC, SetStateAction, @@ -15,15 +15,13 @@ import { useRef, useState, } from "react" +import { generateFakeWords } from "./generateFakeWords" interface SeedInputProps extends Omit { length?: 12 onChange?: (seed: string) => void } -const WORDLIST_LENGTH = 2048 -const JUNK_MULTIPLIER = 12 - export const SeedInput: FC = ({ length = 12, onChange, @@ -41,16 +39,9 @@ export const SeedInput: FC = ({ [onChange, seedInput], ) - const fakeWords = useMemo( - () => - [...Array(length * JUNK_MULTIPLIER)].map((_, index) => - wordlists.en.getWord( - (Math.floor(Math.random() * WORDLIST_LENGTH) * (index + 1)) % - WORDLIST_LENGTH, - ), - ), - [length], - ) + const fakeWords = useMemo(() => { + return generateFakeWords(wordlist, length) + }, [length]) return ( diff --git a/packages/ui/src/components/TextWithAmount/index.tsx b/packages/ui/src/components/TextWithAmount/index.tsx index 4a6504f5e..3e9a6b6b2 100644 --- a/packages/ui/src/components/TextWithAmount/index.tsx +++ b/packages/ui/src/components/TextWithAmount/index.tsx @@ -16,7 +16,7 @@ export const TextWithAmount = ({ try { const convertedAmount = BigInt(amount) - dataValue = bigDecimal.formatUnits(convertedAmount, decimals) + dataValue = bigDecimal.formatUnits({ value: convertedAmount, decimals }) } catch (e) { // ignore parsing error } diff --git a/packages/ui/src/components/TextareaAutosize.tsx b/packages/ui/src/components/TextareaAutosize.tsx index 18a0009ac..319f37eb5 100644 --- a/packages/ui/src/components/TextareaAutosize.tsx +++ b/packages/ui/src/components/TextareaAutosize.tsx @@ -19,7 +19,6 @@ const TextareaAutosize = forwardRef( return (