diff --git a/.changeset/old-boxes-cheat.md b/.changeset/old-boxes-cheat.md new file mode 100644 index 000000000..a03b3cada --- /dev/null +++ b/.changeset/old-boxes-cheat.md @@ -0,0 +1,7 @@ +--- +"@fuel-wallet/connections": minor +"@fuel-wallet/types": minor +"fuels-wallet": minor +--- + +feat: update wallet to the latest fuel core (0.26.0) diff --git a/.changeset/stupid-flowers-provide.md b/.changeset/stupid-flowers-provide.md new file mode 100644 index 000000000..e140a6941 --- /dev/null +++ b/.changeset/stupid-flowers-provide.md @@ -0,0 +1,5 @@ +--- +"fuels-wallet": minor +--- + +feat: add custom network fees, offering options like regular, fast and custom tip. \ No newline at end of file diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 185a0de9c..79a689afa 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -28,12 +28,10 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start Test Node - run: pnpm node:up:test - - - name: Graphql Codegen - run: pnpm codegen:app - env: - NODE_ENV: test + run: pnpm node:up + + - name: Generate .env + run: cp packages/app/.env.example packages/app/.env # Unit tests running with JEST - name: Find PR number @@ -64,7 +62,7 @@ jobs: base-coverage-file: ${{ env.COVERAGE_FILE }} - name: Stop Test Node - run: pnpm node:clean:test + run: pnpm node:clean tests-e2e: name: E2E Tests @@ -81,20 +79,17 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start Test Node - run: pnpm node:up:test - - - name: Graphql Codegen - run: pnpm codegen:app - env: - NODE_ENV: test + run: pnpm node:up + + - name: Generate .env + run: cp packages/app/.env.example packages/app/.env - name: Build Application - run: pnpm build:test + run: pnpm build:app env: ## increase node.js m memory limit for building ## with sourcemaps NODE_OPTIONS: "--max-old-space-size=4096" - NODE_ENV: test # E2E tests running with Playwright - name: Install Playwright Browsers @@ -113,60 +108,66 @@ jobs: retention-days: 30 - name: Stop Test Node - run: pnpm node:clean:test + run: pnpm node:clean - tests-e2e-contracts: - name: E2E Contract Tests - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - - uses: actions/checkout@v3 - - uses: FuelLabs/github-actions/setups/node@master - with: - node-version: 18.14.1 - pnpm-version: 8.15.7 - - uses: FuelLabs/github-actions/setups/docker@master - with: - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Run PNPM install - id: pnpm-cache - run: - pnpm recursive install --frozen-lockfile - - - name: Start Test Node - run: pnpm node:up:test - - - name: Build Application - run: pnpm build:all - env: - ## increase node.js m memory limit for building - ## with sourcemaps - NODE_OPTIONS: "--max-old-space-size=4096" - NODE_ENV: test - - - name: Build & Deploy Contracts - run: pnpm deploy:contracts - working-directory: ./packages/e2e-contract-tests - - # E2E tests running with Playwright - - name: Install Playwright Browsers - run: npx playwright install --with-deps chromium - - - name: Run E2E Contract Tests - run: xvfb-run --auto-servernum -- pnpm test:e2e:contracts - env: - NODE_ENV: test - - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: | - packages/app/playwright-report/ - packages/app/playwright-html/ - retention-days: 30 - - - name: Stop Test Node - run: pnpm node:clean:test + # tests-e2e-contracts: + # name: E2E Contract Tests + # runs-on: buildjet-4vcpu-ubuntu-2204 + # steps: + # - uses: actions/checkout@v3 + # - uses: FuelLabs/github-actions/setups/node@master + # with: + # node-version: 18.14.1 + # pnpm-version: 8.15.7 + # - uses: FuelLabs/github-actions/setups/docker@master + # with: + # username: ${{ github.repository_owner }} + # password: ${{ secrets.GITHUB_TOKEN }} + + # - name: Run PNPM install + # id: pnpm-cache + # run: + # pnpm recursive install --frozen-lockfile + + # - name: Start Test Node + # run: pnpm node:up + + # - name: Generate .env app + # run: cp packages/app/.env.example packages/app/.env + + # - name: Generate .env e2e-contracts + # run: cp packages/e2e-contract-tests/.env.example packages/e2e-contract-tests/.env + + # - name: Build Application + # run: pnpm build:all + # env: + # ## increase node.js m memory limit for building + # ## with sourcemaps + # NODE_OPTIONS: "--max-old-space-size=4096" + # NODE_ENV: test + + # - name: Build & Deploy Contracts + # run: pnpm deploy:contracts + # working-directory: ./packages/e2e-contract-tests + + # # E2E tests running with Playwright + # - name: Install Playwright Browsers + # run: npx playwright install --with-deps chromium + + # - name: Run E2E Contract Tests + # run: xvfb-run --auto-servernum -- pnpm test:e2e:contracts + # env: + # NODE_ENV: test + + # - uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: playwright-report + # path: | + # packages/app/playwright-report/ + # packages/app/playwright-html/ + # retention-days: 30 + + # - name: Stop Test Node + # run: pnpm node:clean diff --git a/Forc.toml b/Forc.toml new file mode 100644 index 000000000..b8627b951 --- /dev/null +++ b/Forc.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "packages/e2e-contract-tests/contracts/custom_asset" +] diff --git a/biome.json b/biome.json index f04cc5028..8de5bcd2a 100644 --- a/biome.json +++ b/biome.json @@ -48,7 +48,7 @@ "**/build", "**/dist", "**/dist-crx", - "**/contracts", + "**/contracts/**", "pnpm-lock.yaml" ] } diff --git a/docker/.env.test b/docker/.env.test deleted file mode 100644 index 8c8b4d428..000000000 --- a/docker/.env.test +++ /dev/null @@ -1,6 +0,0 @@ -PROJECT=fuels-wallet-test -MIN_GAS_PRICE=1 -WALLET_SECRET=0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 -DISPENSE_AMOUNT=500000000 -FUEL_CORE_PORT=4001 -FUEL_FAUCET_PORT=4041 diff --git a/docker/Makefile b/docker/Makefile index b4bd2a1f1..3f02f4e59 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -1,17 +1,8 @@ up: docker compose -p dev --env-file .env up -d --build -up-test: - docker compose -p test --env-file .env.test up -d --build - down: docker compose -p dev stop -down-test: - docker compose -p test stop - clean: docker compose -p dev down --rmi local -v --remove-orphans - -clean-test: - docker compose -p test down --rmi local -v --remove-orphans diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 49723b50e..f03f878a8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,7 +16,7 @@ services: volumes: - fuel-core-db:/mnt/db healthcheck: - test: curl --fail http://localhost:4000/health || exit 1 + test: curl --fail http://localhost:4000/v1/health || exit 1 interval: 1s timeout: 5s retries: 20 @@ -30,8 +30,8 @@ services: MIN_GAS_PRICE: ${MIN_GAS_PRICE} WALLET_SECRET_KEY: ${WALLET_SECRET} DISPENSE_AMOUNT: ${DISPENSE_AMOUNT} - FUEL_NODE_URL: http://${PROJECT:-fuel-node}_fuel-core:4000/graphql - image: ghcr.io/fuellabs/faucet:v0.6.2 + FUEL_NODE_URL: http://${PROJECT:-fuel-node}_fuel-core:4000/v1/graphql + image: ghcr.io/fuellabs/faucet:v0.7.2 ports: - '${FUEL_FAUCET_PORT:-4040}:3000' links: diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index 94310a552..f048630ec 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/fuellabs/fuel-core:v0.22.0 +FROM ghcr.io/fuellabs/fuel-core:v0.26.0 ENV MIN_GAS_PRICE="${MIN_GAS_PRICE}" ENV CONSENSUS_KEY="${CONSENSUS_KEY}" @@ -11,7 +11,7 @@ RUN apt install curl -y WORKDIR /root/ -COPY chainConfig.json . +COPY config ./config COPY run.sh . EXPOSE ${PORT} diff --git a/docker/fuel-core/chainConfig.json b/docker/fuel-core/chainConfig.json deleted file mode 100644 index d3b1d5268..000000000 --- a/docker/fuel-core/chainConfig.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "chain_name": "local_testnet", - "consensus": { - "PoA": { - "signing_key": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d" - } - }, - "parent_network": { - "type": "LocalTest" - }, - "block_gas_limit": 5000000000, - "consensus_parameters": { - "tx_params": { - "max_inputs": 255, - "max_outputs": 255, - "max_witnesses": 255, - "max_gas_per_tx": 10000000, - "max_size": 17825792 - }, - "predicate_params": { - "max_predicate_length": 1048576, - "max_predicate_data_length": 1048576, - "max_gas_per_predicate": 10000000, - "max_message_data_length": 1048576 - }, - "script_params": { - "max_script_length": 1048576, - "max_script_data_length": 1048576 - }, - "contract_params": { - "contract_max_size": 16777216, - "max_storage_slots": 255 - }, - "fee_params": { - "gas_price_factor": 92, - "gas_per_byte": 4 - } - }, - "gas_costs": { - "add": 1, - "addi": 1, - "aloc": 1, - "and": 1, - "andi": 1, - "bal": 13, - "bhei": 1, - "bhsh": 1, - "burn": 132, - "cb": 1, - "cfei": 1, - "cfsi": 1, - "croo": 16, - "div": 1, - "divi": 1, - "ecr1": 3000, - "eck1": 951, - "ed19": 3000, - "eq": 1, - "exp": 1, - "expi": 1, - "flag": 1, - "gm": 1, - "gt": 1, - "gtf": 1, - "ji": 1, - "jmp": 1, - "jne": 1, - "jnei": 1, - "jnzi": 1, - "jmpf": 1, - "jmpb": 1, - "jnzf": 1, - "jnzb": 1, - "jnef": 1, - "jneb": 1, - "k256": 11, - "lb": 1, - "log": 9, - "lt": 1, - "lw": 1, - "mcpi": 33, - "mint": 135, - "mlog": 1, - "modOp": 1, - "modi": 1, - "moveOp": 1, - "movi": 1, - "mroo": 2, - "mul": 1, - "muli": 1, - "mldv": 1, - "noop": 1, - "not": 1, - "or": 1, - "ori": 1, - "ret": 13, - "rvrt": 13, - "s256": 2, - "sb": 1, - "scwq": 13, - "sll": 1, - "slli": 1, - "srl": 1, - "srli": 1, - "srw": 12, - "sub": 1, - "subi": 1, - "sw": 1, - "sww": 43, - "swwq": 44, - "time": 1, - "tr": 105, - "tro": 60, - "wdcm": 1, - "wqcm": 1, - "wdop": 1, - "wqop": 1, - "wdml": 1, - "wqml": 1, - "wddv": 1, - "wqdv": 2, - "wdmd": 3, - "wqmd": 4, - "wdam": 2, - "wqam": 3, - "wdmm": 3, - "wqmm": 3, - "xor": 1, - "xori": 1, - "call": { - "base": 144, - "dep_per_unit": 214 - }, - "ccp": { - "base": 15, - "dep_per_unit": 103 - }, - "csiz": { - "base": 17, - "dep_per_unit": 790 - }, - "ldc": { - "base": 15, - "dep_per_unit": 272 - }, - "logd": { - "base": 26, - "dep_per_unit": 64 - }, - "mcl": { - "base": 1, - "dep_per_unit": 3333 - }, - "mcli": { - "base": 1, - "dep_per_unit": 3333 - }, - "mcp": { - "base": 1, - "dep_per_unit": 2000 - }, - "meq": { - "base": 1, - "dep_per_unit": 2500 - }, - "retd": { - "base": 29, - "dep_per_unit": 62 - }, - "smo": { - "base": 209, - "dep_per_unit": 55 - }, - "srwq": { - "base": 47, - "dep_per_unit": 5 - } - }, - "wallet": { - "address": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", - "privateKey": "0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298" - }, - "initial_state": { - "coins": [ - { - "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", - "amount": "0xFFFFFFFFFFFFFFFF", - "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", - "amount": "0xFFFFFFFFFFFFFFFF", - "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "owner": "0x6684dd7bb61364e1ac458e9241f969178c4a2e302bbc23e143c49d8b6dba701a", - "amount": "0x00000000FFFFFFFF", - "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "owner": "0xc8e615a4089466174459ef19cfd257d2e17adfabff3b8f219dbb5fb4eca87c50", - "amount": "0x00000000FFFFFFFF", - "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "owner": "0x92dffc873b56f219329ed03bb69bebe8c3d8b041088574882f7a6404f02e2f28", - "amount": "0x00000000FFFFFFFF", - "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ] - } -} diff --git a/docker/fuel-core/config/chainConfig.json b/docker/fuel-core/config/chainConfig.json new file mode 100644 index 000000000..81083c094 --- /dev/null +++ b/docker/fuel-core/config/chainConfig.json @@ -0,0 +1,271 @@ +{ + "chain_name": "local_testnet", + "consensus_parameters": { + "V1": { + "tx_params": { + "V1": { + "max_inputs": 255, + "max_outputs": 255, + "max_witnesses": 255, + "max_gas_per_tx": 30000000, + "max_size": 112640, + "max_bytecode_subsections": 256 + } + }, + "predicate_params": { + "V1": { + "max_predicate_length": 102400, + "max_predicate_data_length": 102400, + "max_message_data_length": 102400, + "max_gas_per_predicate": 30000000 + } + }, + "script_params": { + "V1": { + "max_script_length": 102400, + "max_script_data_length": 102400 + } + }, + "contract_params": { + "V1": { + "contract_max_size": 102400, + "max_storage_slots": 1760 + } + }, + "fee_params": { + "V1": { + "gas_price_factor": 92, + "gas_per_byte": 63 + } + }, + "chain_id": 0, + "gas_costs": { + "V1": { + "add": 2, + "addi": 2, + "aloc": 1, + "and": 2, + "andi": 2, + "bal": 366, + "bhei": 2, + "bhsh": 2, + "burn": 33949, + "cb": 2, + "cfei": 2, + "cfsi": 2, + "div": 2, + "divi": 2, + "eck1": 3347, + "ecr1": 46165, + "ed19": 4210, + "eq": 2, + "exp": 2, + "expi": 2, + "flag": 1, + "gm": 2, + "gt": 2, + "gtf": 16, + "ji": 2, + "jmp": 2, + "jne": 2, + "jnei": 2, + "jnzi": 2, + "jmpf": 2, + "jmpb": 2, + "jnzf": 2, + "jnzb": 2, + "jnef": 2, + "jneb": 2, + "lb": 2, + "log": 754, + "lt": 2, + "lw": 2, + "mint": 35718, + "mlog": 2, + "mod": 2, + "modi": 2, + "move": 2, + "movi": 2, + "mroo": 5, + "mul": 2, + "muli": 2, + "mldv": 4, + "noop": 1, + "not": 2, + "or": 2, + "ori": 2, + "poph": 3, + "popl": 3, + "pshh": 4, + "pshl": 4, + "ret_contract": 733, + "rvrt_contract": 722, + "sb": 2, + "sll": 2, + "slli": 2, + "srl": 2, + "srli": 2, + "srw": 253, + "sub": 2, + "subi": 2, + "sw": 2, + "sww": 29053, + "time": 79, + "tr": 46242, + "tro": 33251, + "wdcm": 3, + "wqcm": 3, + "wdop": 3, + "wqop": 3, + "wdml": 3, + "wqml": 4, + "wddv": 5, + "wqdv": 7, + "wdmd": 11, + "wqmd": 18, + "wdam": 9, + "wqam": 12, + "wdmm": 11, + "wqmm": 11, + "xor": 2, + "xori": 2, + "call": { + "LightOperation": { + "base": 21687, + "units_per_gas": 4 + } + }, + "ccp": { + "LightOperation": { + "base": 59, + "units_per_gas": 20 + } + }, + "croo": { + "LightOperation": { + "base": 1, + "units_per_gas": 1 + } + }, + "csiz": { + "LightOperation": { + "base": 59, + "units_per_gas": 195 + } + }, + "k256": { + "LightOperation": { + "base": 282, + "units_per_gas": 3 + } + }, + "ldc": { + "LightOperation": { + "base": 45, + "units_per_gas": 65 + } + }, + "logd": { + "LightOperation": { + "base": 1134, + "units_per_gas": 2 + } + }, + "mcl": { + "LightOperation": { + "base": 3, + "units_per_gas": 523 + } + }, + "mcli": { + "LightOperation": { + "base": 3, + "units_per_gas": 526 + } + }, + "mcp": { + "LightOperation": { + "base": 3, + "units_per_gas": 448 + } + }, + "mcpi": { + "LightOperation": { + "base": 7, + "units_per_gas": 585 + } + }, + "meq": { + "LightOperation": { + "base": 11, + "units_per_gas": 1097 + } + }, + "retd_contract": { + "LightOperation": { + "base": 1086, + "units_per_gas": 2 + } + }, + "s256": { + "LightOperation": { + "base": 45, + "units_per_gas": 3 + } + }, + "scwq": { + "HeavyOperation": { + "base": 30375, + "gas_per_unit": 28628 + } + }, + "smo": { + "LightOperation": { + "base": 64196, + "units_per_gas": 1 + } + }, + "srwq": { + "HeavyOperation": { + "base": 262, + "gas_per_unit": 249 + } + }, + "swwq": { + "HeavyOperation": { + "base": 28484, + "gas_per_unit": 26613 + } + }, + "contract_root": { + "LightOperation": { + "base": 45, + "units_per_gas": 1 + } + }, + "state_root": { + "HeavyOperation": { + "base": 350, + "gas_per_unit": 176 + } + }, + "new_storage_per_byte": 63, + "vm_initialization": { + "LightOperation": { + "base": 1645, + "units_per_gas": 14 + } + } + } + }, + "base_asset_id": "0000000000000000000000000000000000000000000000000000000000000000", + "block_gas_limit": 30000000, + "privileged_address": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "consensus": { + "PoA": { + "signing_key": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d" + } + } +} diff --git a/docker/fuel-core/accounts.json b/docker/fuel-core/config/faucets.json similarity index 100% rename from docker/fuel-core/accounts.json rename to docker/fuel-core/config/faucets.json diff --git a/docker/fuel-core/config/metadata.json b/docker/fuel-core/config/metadata.json new file mode 100644 index 000000000..ae0a1bf75 --- /dev/null +++ b/docker/fuel-core/config/metadata.json @@ -0,0 +1,8 @@ +{ + "chain_config": "chainConfig.json", + "table_encoding": { + "Json": { + "filepath": "stateConfig.json" + } + } +} diff --git a/docker/fuel-core/config/stateConfig.json b/docker/fuel-core/config/stateConfig.json new file mode 100644 index 000000000..1025822d7 --- /dev/null +++ b/docker/fuel-core/config/stateConfig.json @@ -0,0 +1,53 @@ +{ + "coins": [ + { + "tx_id": "0x260eabfd50937e92939fd92687e9302a72e91c5065f64f853f2ccbe02396fe09d665", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", + "amount": 18446744073709551615, + "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "tx_id": "0x2a757c2317236f7883ac9bbbf7d402f034e0b725c544ef1c8725b1d2bd960f8c690f", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "0x6684dd7bb61364e1ac458e9241f969178c4a2e302bbc23e143c49d8b6dba701a", + "amount": 18446744073709551615, + "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "tx_id": "0x634ef6cda00bac63992bbde80c6d694d484d58025a5ca0c9c848f0d35a5a3eee74b2", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "0xc8e615a4089466174459ef19cfd257d2e17adfabff3b8f219dbb5fb4eca87c50", + "amount": 18446744073709551615, + "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "tx_id": "0xd3543bb1da137a7987a96a1bb71681fdd195ff25318c0d4a923aa30eb27ffa80bc7b", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "0x92dffc873b56f219329ed03bb69bebe8c3d8b041088574882f7a6404f02e2f28", + "amount": 18446744073709551615, + "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "tx_id": "0x00000000000000000000000000000000000000000000000000000000000000000001", + "output_index": 0, + "tx_pointer_block_height": 0, + "tx_pointer_tx_idx": 0, + "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", + "amount": 18446744073709551615, + "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ], + "messages": [], + "contracts": [], + "block_height": 0, + "da_block_height": 0 +} diff --git a/docker/fuel-core/config/state_transition_bytecode.wasm b/docker/fuel-core/config/state_transition_bytecode.wasm new file mode 100644 index 000000000..e69de29bb diff --git a/docker/fuel-core/run.sh b/docker/fuel-core/run.sh index 9dbb11922..9fcabf874 100644 --- a/docker/fuel-core/run.sh +++ b/docker/fuel-core/run.sh @@ -1,14 +1,14 @@ #!/bin/bash -# Change the network name -sed -i "s/local_testnet/$NETWORK_NAME/g" ./chainConfig.json - # Start the Fuel Core node /root/fuel-core run \ --ip 0.0.0.0 \ --port 4000 \ --db-path ./mnt/db/ \ - --utxo-validation \ - --min-gas-price ${MIN_GAS_PRICE} \ --consensus-key ${CONSENSUS_KEY} \ - --chain ./chainConfig.json + --snapshot config \ + --poa-instant true \ + --vm-backtrace \ + --min-gas-price ${MIN_GAS_PRICE} \ + --utxo-validation \ + --debug diff --git a/examples/cra-dapp/package.json b/examples/cra-dapp/package.json index db7291a95..44280c735 100644 --- a/examples/cra-dapp/package.json +++ b/examples/cra-dapp/package.json @@ -7,10 +7,10 @@ "start": "vite" }, "dependencies": { - "@fuels/connectors": "0.1.1", - "@fuels/react": "0.18.0", + "@fuels/connectors": "0.4.0", + "@fuels/react": "0.19.0", "@tanstack/react-query": "5.28.4", - "fuels": "0.79.0", + "fuels": "0.84.0", "react": "18.2.0", "react-dom": "18.2.0" }, diff --git a/examples/cra-dapp/src/App.tsx b/examples/cra-dapp/src/App.tsx index 68d6b36bc..d4367af2c 100644 --- a/examples/cra-dapp/src/App.tsx +++ b/examples/cra-dapp/src/App.tsx @@ -1,18 +1,25 @@ import { + useAccount, useAccounts, useConnectUI, useDisconnect, + useFuel, useIsConnected, + useWallet, } from '@fuels/react'; + +import { bn } from 'fuels'; import './App.css'; function App() { - const { connect, error, isError, theme, setTheme, isConnecting } = - useConnectUI(); + const { connect, error, isError, theme, isConnecting } = useConnectUI(); + + const { fuel } = useFuel(); const { disconnect } = useDisconnect(); const { isConnected } = useIsConnected(); + const { wallet } = useWallet(); + const { account } = useAccount(); const { accounts } = useAccounts(); - const lightTheme = theme === 'light'; return (
@@ -33,9 +40,27 @@ function App() { )}
{isError &&

{error?.message}

} diff --git a/examples/cra-dapp/src/main.tsx b/examples/cra-dapp/src/main.tsx index 358016961..4d543bb8a 100644 --- a/examples/cra-dapp/src/main.tsx +++ b/examples/cra-dapp/src/main.tsx @@ -17,6 +17,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( { return ( - + diff --git a/packages/app/jest.config.ts b/packages/app/jest.config.ts index ec25303ed..c225aaf8a 100644 --- a/packages/app/jest.config.ts +++ b/packages/app/jest.config.ts @@ -40,7 +40,7 @@ const config: JestConfigWithTsJest = { require.resolve('@fuel-ui/test-utils/setup'), './jest.setup.ts', ], - setupFiles: ['./load.envs.js', 'fake-indexeddb/auto'], + setupFiles: ['fake-indexeddb/auto'], extensionsToTreatAsEsm: ['.ts', '.tsx'], injectGlobals: true, moduleNameMapper: { diff --git a/packages/app/load.envs.js b/packages/app/load.envs.js index d91bac4e5..210621e09 100644 --- a/packages/app/load.envs.js +++ b/packages/app/load.envs.js @@ -18,9 +18,6 @@ function getEnvName() { if (process.env.NODE_ENV === 'production') { return '.env.production'; } - if (process.env.NODE_ENV === 'test') { - return '.env.test'; - } } // Load from more specific env file to generic -> @@ -41,13 +38,10 @@ function getPublicEnvs() { ); } -// Export the port to be used on vite server and -// make it accessible to the playwirght tests -process.env.PORT = process.env.NODE_ENV === 'test' ? 3001 : 3000; - // Export the version to be used on database // and application level const versions = getVersion(); +process.env.PORT = 3000; process.env.VITE_APP_VERSION = process.env.VITE_APP_VERSION || versions.version; process.env.VITE_DATABASE_VERSION = versions.database; diff --git a/packages/app/package.json b/packages/app/package.json index c5e510f69..f96950743 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -16,21 +16,19 @@ "test:ci": "pnpm ts:check && pnpm test", "test:watch": "jest --watch", "ts:check": "pnpm xstate:typegen && tsc --noEmit", - "xstate:typegen": "xstate typegen 'src/**/*.ts?(x)'", - "codegen": "graphql-codegen --config codegen.ts" + "xstate:typegen": "xstate typegen 'src/**/*.ts?(x)'" }, "dependencies": { "@fontsource/source-code-pro": "5.0.13", - "@fuel-ui/css": "0.23.2", - "@fuel-ui/icons": "0.23.2", - "@fuel-ui/react": "0.23.2", + "@fuel-ui/css": "0.23.3", + "@fuel-ui/icons": "0.23.3", + "@fuel-ui/react": "0.23.3", "@fuel-ui/test-utils": "0.17.0", "@fuel-wallet/connections": "workspace:*", - "@fuels/assets": "^0.17.0", - "@fuels/connectors": "0.1.1", - "@fuels/local-storage": "0.17.0", - "@fuels/react": "0.18.0", - "@fuels/react-xstore": "0.17.0", + "@fuels/assets": "0.19.0", + "@fuels/local-storage": "0.19.0", + "@fuels/react": "0.19.0", + "@fuels/react-xstore": "0.19.0", "@hookform/resolvers": "3.3.2", "@react-aria/utils": "3.21.0", "@sentry/browser": "7.73.0", @@ -47,12 +45,8 @@ "events": "3.3.0", "fake-indexeddb": "4.0.2", "framer-motion": "10.16.4", - "fuels": "0.79.0", - "graphql": "16.8.1", - "graphql-request": "6.1.0", - "graphql-tag": "2.12.6", + "fuels": "0.84.0", "json-rpc-2.0": "1.7.0", - "lodash.clonedeep": "4.5.0", "lodash.debounce": "4.0.8", "react": "18.2.0", "react-dom": "18.2.0", @@ -69,13 +63,7 @@ "devDependencies": { "@crxjs/vite-plugin": "1.0.14", "@fuel-wallet/types": "workspace:*", - "@graphql-codegen/cli": "5.0.0", - "@graphql-codegen/named-operations-object": "2.3.1", - "@graphql-codegen/near-operation-file-preset": "2.5.0", - "@graphql-codegen/typescript": "4.0.1", - "@graphql-codegen/typescript-graphql-request": "5.0.0", - "@graphql-codegen/typescript-operations": "4.0.1", - "@graphql-codegen/typescript-react-apollo": "3.3.7", + "@fuels/connectors": "0.4.0", "@playwright/test": "1.39.0", "@sentry/cli": "2.21.2", "@storybook/addon-a11y": "7.4.6", @@ -98,7 +86,6 @@ "@tanstack/react-query-devtools": "5.28.4", "@testing-library/react": "14.0.0", "@types/chrome": "0.0.246", - "@types/lodash.clonedeep": "4.5.7", "@types/lodash.debounce": "4.0.7", "@types/react": "18.2.28", "@types/react-custom-scroll": "5.0.1", @@ -106,7 +93,6 @@ "@types/react-helmet": "6.1.7", "@vitejs/plugin-react": "4.1.0", "@xstate/inspect": "0.8.0", - "get-graphql-schema": "2.1.2", "jszip": "3.10.1", "msw": "1.3.2", "msw-storybook-addon": "1.9.0", diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index 31ea56be6..ae45af844 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -3,13 +3,10 @@ import { join } from 'path'; import { defineConfig } from '@playwright/test'; import './load.envs'; -const distDirectory = join(__dirname, './dist'); - -const _IS_CI = !!process.env.CI; const PORT = process.env.PORT; export default defineConfig({ - workers: 4, + workers: 1, testMatch: join(__dirname, './playwright/**/*.test.ts'), testDir: join(__dirname, './playwright/'), outputDir: join(__dirname, './playwright-results/'), @@ -17,17 +14,16 @@ export default defineConfig({ ['list', { printSteps: true }], ['html', { outputFolder: join(__dirname, './playwright-html/') }], ], - // Retry tests on CI if they fail - retries: 4, webServer: { - command: `pnpm exec http-server -s -p ${PORT} ${distDirectory}`, + command: 'pnpm dev:crx', port: Number(PORT), reuseExistingServer: true, }, use: { baseURL: `http://localhost:${PORT}/`, permissions: ['clipboard-read', 'clipboard-write'], - headless: true, + headless: false, trace: 'on-first-retry', + actionTimeout: 5000, }, }); diff --git a/packages/app/playwright/commons/delay.ts b/packages/app/playwright/commons/delay.ts new file mode 100644 index 000000000..59216636a --- /dev/null +++ b/packages/app/playwright/commons/delay.ts @@ -0,0 +1,2 @@ +export const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/app/playwright/commons/gas.ts b/packages/app/playwright/commons/gas.ts deleted file mode 100644 index 7c57c1948..000000000 --- a/packages/app/playwright/commons/gas.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { BN, Provider } from 'fuels'; - -// TODO: remove this function when SDK make transactions with correct gas configs -export const getGasConfig = async (provider: Provider) => { - const chain = await provider.getChain(); - const nodeInfo = await provider.fetchNode(); - const gasLimit = chain.consensusParameters.maxGasPerTx.div(2); - const gasPrice = nodeInfo.minGasPrice; - - return { gasLimit, gasPrice }; -}; diff --git a/packages/app/playwright/commons/index.ts b/packages/app/playwright/commons/index.ts index a1fa241a0..e21964310 100644 --- a/packages/app/playwright/commons/index.ts +++ b/packages/app/playwright/commons/index.ts @@ -3,4 +3,4 @@ export * from './button'; export * from './visit'; export * from './text'; export * from './seedWallet'; -export * from './gas'; +export * from './delay'; diff --git a/packages/app/playwright/commons/seedWallet.ts b/packages/app/playwright/commons/seedWallet.ts index 302d8fc05..381ef7032 100644 --- a/packages/app/playwright/commons/seedWallet.ts +++ b/packages/app/playwright/commons/seedWallet.ts @@ -1,11 +1,9 @@ import type { Page } from '@playwright/test'; import type { BN } from 'fuels'; -import { Address, BaseAssetId, Provider, Wallet } from 'fuels'; +import { Address, Provider, Wallet } from 'fuels'; import { ALT_ASSET, getAccount } from '../mocks'; -import { getGasConfig } from './gas'; - const { GENESIS_SECRET, VITE_FUEL_PROVIDER_URL } = process.env; export async function seedCurrentAccount(page: Page, amount: BN) { @@ -16,19 +14,18 @@ export async function seedCurrentAccount(page: Page, amount: BN) { export async function seedWallet(address: string, amount: BN) { const provider = await Provider.create(VITE_FUEL_PROVIDER_URL); const genesisWallet = Wallet.fromPrivateKey(GENESIS_SECRET, provider); - const { gasPrice, gasLimit } = await getGasConfig(genesisWallet.provider); + const transfETH = await genesisWallet.transfer( Address.fromString(address), amount, - BaseAssetId, - { gasPrice, gasLimit } + provider.getBaseAssetId() ); await transfETH.wait(); + const transfAsset = await genesisWallet.transfer( Address.fromString(address), amount, - ALT_ASSET.assetId, - { gasPrice, gasLimit: 110_000 } + ALT_ASSET.assetId ); await transfAsset.wait(); } diff --git a/packages/app/playwright/crx/crx.test.ts b/packages/app/playwright/crx/crx.test.ts index bc60bd4a2..656d74141 100644 --- a/packages/app/playwright/crx/crx.test.ts +++ b/packages/app/playwright/crx/crx.test.ts @@ -1,18 +1,13 @@ +import type { Account as WalletAccount } from '@fuel-wallet/types'; import { type Locator, expect } from '@playwright/test'; -import { - type Account, - type Asset, - Provider, - Signer, - Wallet, - bn, - hashMessage, -} from 'fuels'; +import { type Asset, Provider, Signer, Wallet, bn, hashMessage } from 'fuels'; import { + delay, getButtonByText, getByAriaLabel, getElementByText, + hasAriaLabel, hasText, reload, seedWallet, @@ -37,22 +32,7 @@ import { const WALLET_PASSWORD = 'Qwe123456$'; -test.describe.configure({ mode: 'parallel' }); test.describe('FuelWallet Extension', () => { - test('On install sign-up page is open', async ({ context }) => { - // In development mode files are render dynamically - // making this first page to throw an error File not found. - if (process.env.NODE_ENV !== 'test') return; - - const page = await context.waitForEvent('page', { - predicate: (page) => { - return page.url().includes('sign-up'); - }, - }); - expect(page.url()).toContain('sign-up'); - await page.close(); - }); - test('If user opens popup it should force open a sign-up page', async ({ context, extensionId, @@ -76,26 +56,20 @@ test.describe('FuelWallet Extension', () => { // the page. await blankPage.goto(new URL('e2e.html', baseURL).href); - await blankPage.waitForFunction(() => document.readyState === 'complete'); - await test.step('Has window.fuel', async () => { const hasFuel = await blankPage.evaluate(async () => { + // wait for the script to load + await new Promise((resolve) => setTimeout(resolve, 1000)); return typeof window.fuel === 'object'; }); expect(hasFuel).toBeTruthy(); }); await test.step('Should return current version of Wallet', async () => { - await blankPage.waitForFunction(() => { - return window.fuel?.currentConnector(); + const version = await blankPage.evaluate(async () => { + return window.fuel.version(); }); - - const hasConnector = await blankPage.evaluate(() => - window.fuel.currentConnector() - ); - expect(hasConnector).toBeDefined(); - const version = blankPage.evaluate(() => window.fuel.version()); - expect(await version).toStrictEqual(process.env.VITE_APP_VERSION); + expect(version).toEqual(process.env.VITE_APP_VERSION); }); await test.step('Should reconnect if service worker stops', async () => { @@ -204,13 +178,21 @@ test.describe('FuelWallet Extension', () => { }); async function connectAccounts() { + await reload(blankPage); const connectionResponse = blankPage.evaluate(async () => { - return window.fuel.connect(); + const isConnected = await window.fuel.connect(); + if (!isConnected) { + // throw this error to avoid needing to wait for `fuel.connect` timeout + throw new Error('Connecting to Fuel Wallet did not work'); + } + + return true; }); const authorizeRequest = await context.waitForEvent('page', { predicate: (page) => page.url().includes(extensionId), }); + await hasText(authorizeRequest, /connect/i); // Add Account 3 to the DApp connection await getByAriaLabel(authorizeRequest, 'Toggle Account 3').click(); // Add Account 4 to the DApp connection @@ -224,6 +206,7 @@ test.describe('FuelWallet Extension', () => { }).rejects.toThrow(); await hasText(authorizeRequest, /connect/i); + await getButtonByText(authorizeRequest, /next/i).click(); await hasText(authorizeRequest, /accounts/i); await getButtonByText(authorizeRequest, /connect/i).click(); @@ -301,8 +284,10 @@ test.describe('FuelWallet Extension', () => { await test.step('window.fuel.currentAccount()', async () => { await test.step('Current authorized current Account', async () => { const authorizedAccount = await switchAccount(popupPage, 'Account 1'); - await getByAriaLabel(popupPage, 'Accounts').click({ delay: 1000 }); - await getByAriaLabel(popupPage, 'Close dialog').click(); + + // delay to avoid the page to get the wrong currentAccount + await delay(2000); + const currentAccountPromise = await blankPage.evaluate(async () => { return window.fuel.currentAccount(); }); @@ -311,6 +296,10 @@ test.describe('FuelWallet Extension', () => { await test.step('Throw on not Authorized Account', async () => { await switchAccount(popupPage, 'Account 2'); + + // delay to avoid the page to get the wrong currentAccount + await delay(2000); + const currentAccountPromise = blankPage.evaluate(async () => { return window.fuel.currentAccount(); }); @@ -332,8 +321,10 @@ test.describe('FuelWallet Extension', () => { ); } - async function approveMessageSignCheck(authorizedAccount: Account) { - const signedMessagePromise = signMessage(authorizedAccount.address); + async function approveMessageSignCheck(authorizedAccount: WalletAccount) { + const signedMessagePromise = signMessage( + authorizedAccount.address.toString() + ); const signMessageRequest = await context.waitForEvent('page', { predicate: (page) => page.url().includes(extensionId), }); @@ -401,16 +392,10 @@ test.describe('FuelWallet Extension', () => { senderAddress as string ); - // TODO: remove this gas config once SDK fixes and start with correct values - const chain = await wallet.provider.getChain(); - const nodeInfo = await wallet.provider.fetchNode(); - const gasLimit = chain.consensusParameters.maxGasPerTx.div(2); - const gasPrice = nodeInfo.minGasPrice; const response = await wallet.transfer( receiver, Number(amount), - undefined, - { gasPrice, gasLimit } + undefined ); const result = await response.waitForResult(); return result.status; @@ -419,7 +404,7 @@ test.describe('FuelWallet Extension', () => { ); } - async function approveTxCheck(senderAccount: Account) { + async function approveTxCheck(senderAccount: WalletAccount) { const provider = await Provider.create( process.env.VITE_FUEL_PROVIDER_URL ); @@ -449,7 +434,8 @@ test.describe('FuelWallet Extension', () => { approveTransactionPage, senderAccount.address.toString() ); - await hasText(approveTransactionPage, /Confirm before approving/i); + + await hasAriaLabel(approveTransactionPage, 'Confirm Transaction'); await getButtonByText(approveTransactionPage, /Approve/i).click(); await expect(transferStatus).resolves.toBe('success'); @@ -582,7 +568,7 @@ test.describe('FuelWallet Extension', () => { // Check if added network is selected let networkSelector = getByAriaLabel(popupPage, 'Selected Network'); - await expect(networkSelector).toHaveText(/Testnet Beta 5/); + await expect(networkSelector).toHaveText(/Ignition\-Dev/); // Remove added network await networkSelector.click(); @@ -608,14 +594,15 @@ test.describe('FuelWallet Extension', () => { // Check if re-added network is selected networkSelector = getByAriaLabel(popupPage, 'Selected Network'); - await expect(networkSelector).toHaveText(/Testnet Beta 5/); + await expect(networkSelector).toHaveText(/Ignition\-Dev/); }); await test.step('window.fuel.on("currentAccount") to a connected account', async () => { // Switch to account 2 await switchAccount(popupPage, 'Account 2'); - await getByAriaLabel(popupPage, 'Accounts').click({ delay: 1000 }); - await getByAriaLabel(popupPage, 'Close dialog').click(); + + // delay to avoid the page to listen the event from above swithAccount wrong event + await delay(1000); const onChangeAccountPromise = blankPage.evaluate(() => { return new Promise((resolve) => { @@ -656,14 +643,15 @@ test.describe('FuelWallet Extension', () => { }); await test.step('Auto lock fuel wallet', async () => { - await getByAriaLabel(popupPage, 'Accounts').click({ delay: 65000 }); + await getByAriaLabel(popupPage, 'Accounts').click(); + await popupPage.waitForTimeout(65000); await hasText(popupPage, 'Unlock your wallet to continue'); }); }); }); // Increase timeout for this test -// The timeout is set for 2 minutes +// The timeout is set for 3 minutes // because some tests like reconnect // can take up to 1 minute before it's reconnected -test.setTimeout(180_000); +test.setTimeout(240_000); diff --git a/packages/app/playwright/crx/utils/popup.ts b/packages/app/playwright/crx/utils/popup.ts index b56cbe6cd..31a132602 100644 --- a/packages/app/playwright/crx/utils/popup.ts +++ b/packages/app/playwright/crx/utils/popup.ts @@ -32,7 +32,7 @@ export async function switchAccount(popupPage: Page, name: string) { y: 10, }, }); - await waitAriaLabel(popupPage, name); + await waitAriaLabel(popupPage, `${name} selected`); // Return account to be used on tests account = await getAccountByName(popupPage, name); diff --git a/packages/app/playwright/crx/utils/test.ts b/packages/app/playwright/crx/utils/test.ts index ee2668b74..2b6db4443 100644 --- a/packages/app/playwright/crx/utils/test.ts +++ b/packages/app/playwright/crx/utils/test.ts @@ -2,6 +2,7 @@ import path from 'path'; import type { BrowserContext } from '@playwright/test'; import { test as base, chromium } from '@playwright/test'; +import { delay } from '../../commons'; const pathToExtension = path.join(__dirname, '../../../dist-crx'); @@ -20,6 +21,9 @@ export const test = base.extend<{ let context: BrowserContext; test.beforeAll(async () => { + // @TODO: remove delay. it was added to wait the extension to be available as playwright + // only waits for port 3000 to be available which is done before the dist-crx gets done + await delay(5000); context = await chromium.launchPersistentContext('', { headless: false, args: [ diff --git a/packages/app/playwright/e2e.ts b/packages/app/playwright/e2e.ts index fdb4f1b3d..38f3e8bfa 100644 --- a/packages/app/playwright/e2e.ts +++ b/packages/app/playwright/e2e.ts @@ -1,10 +1,10 @@ -import { defaultConnectors } from '@fuels/connectors'; -import { Address, Fuel, type StorageAbstract } from 'fuels'; +import { FuelWalletDevelopmentConnector } from '@fuels/connectors'; +import { Address, Fuel, Provider } from 'fuels'; localStorage.clear(); window.fuel = new Fuel({ - connectors: defaultConnectors(), + connectors: [new FuelWalletDevelopmentConnector()], }); window.createAddress = (address: string) => Address.fromString(address); diff --git a/packages/app/playwright/e2e/HomeWallet.test.ts b/packages/app/playwright/e2e/HomeWallet.test.ts index 5a945830d..2e2bd369b 100644 --- a/packages/app/playwright/e2e/HomeWallet.test.ts +++ b/packages/app/playwright/e2e/HomeWallet.test.ts @@ -4,6 +4,7 @@ import test, { chromium } from '@playwright/test'; import { getButtonByText, getByAriaLabel, + getInputByName, hasText, reload, visit, @@ -21,6 +22,31 @@ test.describe('HomeWallet', () => { await mockData(page); }); + // @TODO: re-enable when interacting with faucet popup + test.skip('should change balance when select a new network', async ({ + context, + }) => { + await visit(page, '/wallet'); + await getButtonByText(page, 'Faucet').click(); + + // @TODO: detect faucetPage here + const pages = context.pages(); + const faucetPage = pages.find((page) => { + page.url().includes('address'); + }); + await getInputByName(faucetPage, 'agreement1').click(); + await getInputByName(faucetPage, 'agreement2').click(); + await getInputByName(faucetPage, 'agreement3').click(); + await getButtonByText(faucetPage, 'Give me ETH').click(); + await hasText(page, /Ethereum/i); + await hasText(page, /ETH.0\.5/i); + + /** Select a new network */ + await getByAriaLabel(page, 'Selected Network').click(); + await getByAriaLabel(page, 'fuel_network-item-2').click(); + await hasText(page, "You don't have any assets"); + }); + test('should open the side bar and close it', async () => { await visit(page, '/wallet'); await getByAriaLabel(page, 'Menu').click(); diff --git a/packages/app/playwright/e2e/Networks.test.ts b/packages/app/playwright/e2e/Networks.test.ts index 2ba498a25..abb59f935 100644 --- a/packages/app/playwright/e2e/Networks.test.ts +++ b/packages/app/playwright/e2e/Networks.test.ts @@ -88,13 +88,13 @@ test.describe('Networks', () => { await expect(buttonCreate).toBeDisabled(); const urlInput = getInputByName(page, 'url'); await expect(urlInput).toBeFocused(); - await urlInput.fill('https://beta-5.fuel.network/graphql'); - await hasText(page, /Testnet/i, 0, 15000); + await urlInput.fill('https://devnet.fuel.network/v1/graphql'); + await hasText(page, /Ignition\-Dev/i, 0, 15000); await expect(buttonCreate).toBeEnabled(); await buttonCreate.click(); // Wait for save and close popup; await page.waitForTimeout(2000); await reload(page); - await hasText(page, /Testnet/i); + await hasText(page, /Ignition\-Dev/i); }); }); diff --git a/packages/app/playwright/e2e/SendTransaction.test.ts b/packages/app/playwright/e2e/SendTransaction.test.ts index cb7284933..b25055ef6 100644 --- a/packages/app/playwright/e2e/SendTransaction.test.ts +++ b/packages/app/playwright/e2e/SendTransaction.test.ts @@ -53,7 +53,7 @@ test.describe('SendTransaction', () => { await getInputByName(page, 'amount').fill('0.001'); // Submit transaction - await getButtonByText(page, 'Confirm').click(); + await getButtonByText(page, 'Review').click(); await getButtonByText(page, 'Approve').click(); await hasText(page, '0.001 ETH'); @@ -79,7 +79,7 @@ test.describe('SendTransaction', () => { await getInputByName(page, 'amount').fill('0.001'); // Submit transaction - await getButtonByText(page, 'Confirm').click(); + await getButtonByText(page, 'Review').click(); // Approve transaction await hasText(page, '0.001 ETH'); @@ -113,7 +113,7 @@ test.describe('SendTransaction', () => { await hasAriaLabel(page, 'Balance: 1,000,000.00'); // Submit transaction - await getButtonByText(page, 'Confirm').click(); + await getButtonByText(page, 'Review').click(); // Approve transaction await hasText(page, `0.01 ${ALT_ASSET.symbol}`); @@ -147,8 +147,10 @@ test.describe('SendTransaction', () => { // Get calculated fee await hasText(page, /(.*)ETH/); - const el = await getElementByText(page, /(.*)ETH/); - const feeAmountText = (await el.textContent()).replace(' ETH', '').trim(); + const regularFee = await getByAriaLabel(page, 'Fee Value').first(); + const feeAmountText = (await regularFee.textContent()) + .replace(' ETH', '') + .trim(); const feeAmount = bn.parseUnits(feeAmountText); // Max amount after calculating fee @@ -159,7 +161,7 @@ test.describe('SendTransaction', () => { ); // Submit transaction - await getButtonByText(page, 'Confirm').click(); + await getButtonByText(page, 'Review').click(); // Approve transaction await hasText(page, `${maxAmountAfterFee} ETH`); diff --git a/packages/app/playwright/mocks/database.ts b/packages/app/playwright/mocks/database.ts index 0cef725f1..d0db07480 100644 --- a/packages/app/playwright/mocks/database.ts +++ b/packages/app/playwright/mocks/database.ts @@ -1,10 +1,11 @@ -import type { AssetData, Connection, NetworkData } from '@fuel-wallet/types'; -import type { Page } from '@playwright/test'; import type { - Account, - Asset, - WalletManagerAccount as WalletAccount, -} from 'fuels'; + AssetData, + Connection, + NetworkData, + Account as WalletAccount, +} from '@fuel-wallet/types'; +import type { Page } from '@playwright/test'; +import type { Asset, WalletManagerAccount } from 'fuels'; import { Address, Mnemonic, WalletManager, encrypt } from 'fuels'; import { getByAriaLabel } from '../commons/locator'; @@ -92,8 +93,7 @@ export const ALT_ASSET = { }; export const FUEL_NETWORK = { - name: 'Fuel Testnet', - url: 'https://beta-5.fuel.network/graphql', + url: 'https://devnet.fuel.network/v1/graphql', }; export async function getAccount(page: Page) { @@ -116,7 +116,7 @@ export async function createManager(mnemonic: string) { return walletManager; } -export function createAccount(wallet: WalletAccount, index = 0) { +export function createAccount(wallet: WalletManagerAccount, index = 0) { return { address: wallet.address.toAddress(), balance: '0', @@ -189,7 +189,7 @@ export async function mockData( await page.evaluate( ([accounts, networks, connections, assets, vault, password]: [ - Array, + Array, Array, Array, Array, diff --git a/packages/app/src/generated/graphql.ts b/packages/app/src/generated/graphql.ts deleted file mode 100644 index e12b8865f..000000000 --- a/packages/app/src/generated/graphql.ts +++ /dev/null @@ -1,1378 +0,0 @@ -import type { GraphQLClient } from 'graphql-request'; -import type { GraphQLClientRequestHeaders } from 'graphql-request/build/cjs/types'; -import gql from 'graphql-tag'; -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { - [K in keyof T]: T[K]; -}; -export type MakeOptional = Omit & { - [SubKey in K]?: Maybe; -}; -export type MakeMaybe = Omit & { - [SubKey in K]: Maybe; -}; -export type MakeEmpty< - T extends { [key: string]: unknown }, - K extends keyof T, -> = { [_ in K]?: never }; -export type Incremental = - | T - | { - [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; - }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string }; - String: { input: string; output: string }; - Boolean: { input: boolean; output: boolean }; - Int: { input: number; output: number }; - Float: { input: number; output: number }; - Address: { input: string; output: string }; - AssetId: { input: string; output: string }; - BlockId: { input: string; output: string }; - Bytes32: { input: string; output: string }; - ContractId: { input: string; output: string }; - HexString: { input: string; output: string }; - Nonce: { input: string; output: string }; - Salt: { input: string; output: string }; - Signature: { input: string; output: string }; - Tai64Timestamp: { input: string; output: string }; - TransactionId: { input: string; output: string }; - TxPointer: { input: string; output: string }; - U8: { input: string; output: string }; - U32: { input: string; output: string }; - U64: { input: string; output: string }; - UtxoId: { input: string; output: string }; -}; - -export type IBalance = { - __typename?: 'Balance'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - owner: Scalars['Address']['output']; -}; - -export type IBalanceConnection = { - __typename?: 'BalanceConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type IBalanceEdge = { - __typename?: 'BalanceEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: IBalance; -}; - -export type IBalanceFilterInput = { - /** Filter coins based on the `owner` field */ - owner: Scalars['Address']['input']; -}; - -export type IBlock = { - __typename?: 'Block'; - consensus: IConsensus; - header: IHeader; - id: Scalars['BlockId']['output']; - transactions: Array; -}; - -export type IBlockConnection = { - __typename?: 'BlockConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type IBlockEdge = { - __typename?: 'BlockEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: IBlock; -}; - -export type IBreakpoint = { - contract: Scalars['ContractId']['input']; - pc: Scalars['U64']['input']; -}; - -export type IChainInfo = { - __typename?: 'ChainInfo'; - consensusParameters: IConsensusParameters; - daHeight: Scalars['U64']['output']; - gasCosts: IGasCosts; - latestBlock: IBlock; - name: Scalars['String']['output']; -}; - -export type IChangeOutput = { - __typename?: 'ChangeOutput'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - to: Scalars['Address']['output']; -}; - -export type ICoin = { - __typename?: 'Coin'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - /** TxPointer - the height of the block this coin was created in */ - blockCreated: Scalars['U32']['output']; - maturity: Scalars['U32']['output']; - owner: Scalars['Address']['output']; - /** TxPointer - the index of the transaction that created this coin */ - txCreatedIdx: Scalars['U64']['output']; - utxoId: Scalars['UtxoId']['output']; -}; - -export type ICoinConnection = { - __typename?: 'CoinConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type ICoinEdge = { - __typename?: 'CoinEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: ICoin; -}; - -export type ICoinFilterInput = { - /** Returns coins only with `asset_id`. */ - assetId: InputMaybe; - /** Returns coins owned by the `owner`. */ - owner: Scalars['Address']['input']; -}; - -export type ICoinOutput = { - __typename?: 'CoinOutput'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - to: Scalars['Address']['output']; -}; - -/** The schema analog of the [`coins::CoinType`]. */ -export type ICoinType = ICoin | IMessageCoin; - -export type IConsensus = IGenesis | IPoAConsensus; - -export type IConsensusParameters = { - __typename?: 'ConsensusParameters'; - baseAssetId: Scalars['AssetId']['output']; - chainId: Scalars['U64']['output']; - contractParams: IContractParameters; - feeParams: IFeeParameters; - gasCosts: IGasCosts; - predicateParams: IPredicateParameters; - scriptParams: IScriptParameters; - txParams: ITxParameters; -}; - -export type IContract = { - __typename?: 'Contract'; - bytecode: Scalars['HexString']['output']; - id: Scalars['ContractId']['output']; - salt: Scalars['Salt']['output']; -}; - -export type IContractBalance = { - __typename?: 'ContractBalance'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - contract: Scalars['ContractId']['output']; -}; - -export type IContractBalanceConnection = { - __typename?: 'ContractBalanceConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type IContractBalanceEdge = { - __typename?: 'ContractBalanceEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: IContractBalance; -}; - -export type IContractBalanceFilterInput = { - /** Filter assets based on the `contractId` field */ - contract: Scalars['ContractId']['input']; -}; - -export type IContractCreated = { - __typename?: 'ContractCreated'; - contract: IContract; - stateRoot: Scalars['Bytes32']['output']; -}; - -export type IContractOutput = { - __typename?: 'ContractOutput'; - balanceRoot: Scalars['Bytes32']['output']; - inputIndex: Scalars['Int']['output']; - stateRoot: Scalars['Bytes32']['output']; -}; - -export type IContractParameters = { - __typename?: 'ContractParameters'; - contractMaxSize: Scalars['U64']['output']; - maxStorageSlots: Scalars['U64']['output']; -}; - -export type IDependentCost = IHeavyOperation | ILightOperation; - -export type IExcludeInput = { - /** Messages to exclude from the selection. */ - messages: Array; - /** Utxos to exclude from the selection. */ - utxos: Array; -}; - -export type IFailureStatus = { - __typename?: 'FailureStatus'; - block: IBlock; - programState: Maybe; - reason: Scalars['String']['output']; - receipts: Array; - time: Scalars['Tai64Timestamp']['output']; - transactionId: Scalars['TransactionId']['output']; -}; - -export type IFeeParameters = { - __typename?: 'FeeParameters'; - gasPerByte: Scalars['U64']['output']; - gasPriceFactor: Scalars['U64']['output']; -}; - -export type IGasCosts = { - __typename?: 'GasCosts'; - add: Scalars['U64']['output']; - addi: Scalars['U64']['output']; - aloc: Scalars['U64']['output']; - and: Scalars['U64']['output']; - andi: Scalars['U64']['output']; - bal: Scalars['U64']['output']; - bhei: Scalars['U64']['output']; - bhsh: Scalars['U64']['output']; - burn: Scalars['U64']['output']; - call: IDependentCost; - cb: Scalars['U64']['output']; - ccp: IDependentCost; - cfei: Scalars['U64']['output']; - cfsi: Scalars['U64']['output']; - contractRoot: IDependentCost; - croo: Scalars['U64']['output']; - csiz: IDependentCost; - div: Scalars['U64']['output']; - divi: Scalars['U64']['output']; - eck1: Scalars['U64']['output']; - ecr1: Scalars['U64']['output']; - ed19: Scalars['U64']['output']; - eq: Scalars['U64']['output']; - exp: Scalars['U64']['output']; - expi: Scalars['U64']['output']; - flag: Scalars['U64']['output']; - gm: Scalars['U64']['output']; - gt: Scalars['U64']['output']; - gtf: Scalars['U64']['output']; - ji: Scalars['U64']['output']; - jmp: Scalars['U64']['output']; - jmpb: Scalars['U64']['output']; - jmpf: Scalars['U64']['output']; - jne: Scalars['U64']['output']; - jneb: Scalars['U64']['output']; - jnef: Scalars['U64']['output']; - jnei: Scalars['U64']['output']; - jnzb: Scalars['U64']['output']; - jnzf: Scalars['U64']['output']; - jnzi: Scalars['U64']['output']; - k256: IDependentCost; - lb: Scalars['U64']['output']; - ldc: IDependentCost; - log: Scalars['U64']['output']; - logd: IDependentCost; - lt: Scalars['U64']['output']; - lw: Scalars['U64']['output']; - mcl: IDependentCost; - mcli: IDependentCost; - mcp: IDependentCost; - mcpi: IDependentCost; - meq: IDependentCost; - mint: Scalars['U64']['output']; - mldv: Scalars['U64']['output']; - mlog: Scalars['U64']['output']; - modOp: Scalars['U64']['output']; - modi: Scalars['U64']['output']; - moveOp: Scalars['U64']['output']; - movi: Scalars['U64']['output']; - mroo: Scalars['U64']['output']; - mul: Scalars['U64']['output']; - muli: Scalars['U64']['output']; - newStoragePerByte: Scalars['U64']['output']; - noop: Scalars['U64']['output']; - not: Scalars['U64']['output']; - or: Scalars['U64']['output']; - ori: Scalars['U64']['output']; - poph: Scalars['U64']['output']; - popl: Scalars['U64']['output']; - pshh: Scalars['U64']['output']; - pshl: Scalars['U64']['output']; - ret: Scalars['U64']['output']; - retd: IDependentCost; - rvrt: Scalars['U64']['output']; - s256: IDependentCost; - sb: Scalars['U64']['output']; - scwq: IDependentCost; - sll: Scalars['U64']['output']; - slli: Scalars['U64']['output']; - smo: IDependentCost; - srl: Scalars['U64']['output']; - srli: Scalars['U64']['output']; - srw: Scalars['U64']['output']; - srwq: IDependentCost; - stateRoot: IDependentCost; - sub: Scalars['U64']['output']; - subi: Scalars['U64']['output']; - sw: Scalars['U64']['output']; - sww: Scalars['U64']['output']; - swwq: IDependentCost; - time: Scalars['U64']['output']; - tr: Scalars['U64']['output']; - tro: Scalars['U64']['output']; - vmInitialization: IDependentCost; - wdam: Scalars['U64']['output']; - wdcm: Scalars['U64']['output']; - wddv: Scalars['U64']['output']; - wdmd: Scalars['U64']['output']; - wdml: Scalars['U64']['output']; - wdmm: Scalars['U64']['output']; - wdop: Scalars['U64']['output']; - wqam: Scalars['U64']['output']; - wqcm: Scalars['U64']['output']; - wqdv: Scalars['U64']['output']; - wqmd: Scalars['U64']['output']; - wqml: Scalars['U64']['output']; - wqmm: Scalars['U64']['output']; - wqop: Scalars['U64']['output']; - xor: Scalars['U64']['output']; - xori: Scalars['U64']['output']; -}; - -export type IGenesis = { - __typename?: 'Genesis'; - /** - * The chain configs define what consensus type to use, what settlement layer to use, - * rules of block validity, etc. - */ - chainConfigHash: Scalars['Bytes32']['output']; - /** The Binary Merkle Tree root of all genesis coins. */ - coinsRoot: Scalars['Bytes32']['output']; - /** The Binary Merkle Tree root of state, balances, contracts code hash of each contract. */ - contractsRoot: Scalars['Bytes32']['output']; - /** The Binary Merkle Tree root of all genesis messages. */ - messagesRoot: Scalars['Bytes32']['output']; -}; - -export type IHeader = { - __typename?: 'Header'; - /** Hash of the application header. */ - applicationHash: Scalars['Bytes32']['output']; - /** The layer 1 height of messages and events to include since the last layer 1 block number. */ - daHeight: Scalars['U64']['output']; - /** Fuel block height. */ - height: Scalars['U32']['output']; - /** Hash of the header */ - id: Scalars['BlockId']['output']; - /** Number of message receipts in this block. */ - messageReceiptCount: Scalars['U64']['output']; - /** Merkle root of message receipts in this block. */ - messageReceiptRoot: Scalars['Bytes32']['output']; - /** Merkle root of all previous block header hashes. */ - prevRoot: Scalars['Bytes32']['output']; - /** The block producer time. */ - time: Scalars['Tai64Timestamp']['output']; - /** Number of transactions in this block. */ - transactionsCount: Scalars['U64']['output']; - /** Merkle root of transactions. */ - transactionsRoot: Scalars['Bytes32']['output']; -}; - -export type IHeavyOperation = { - __typename?: 'HeavyOperation'; - base: Scalars['U64']['output']; - gasPerUnit: Scalars['U64']['output']; -}; - -export type IInput = IInputCoin | IInputContract | IInputMessage; - -export type IInputCoin = { - __typename?: 'InputCoin'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - maturity: Scalars['U32']['output']; - owner: Scalars['Address']['output']; - predicate: Scalars['HexString']['output']; - predicateData: Scalars['HexString']['output']; - predicateGasUsed: Scalars['U64']['output']; - txPointer: Scalars['TxPointer']['output']; - utxoId: Scalars['UtxoId']['output']; - witnessIndex: Scalars['Int']['output']; -}; - -export type IInputContract = { - __typename?: 'InputContract'; - balanceRoot: Scalars['Bytes32']['output']; - contract: IContract; - stateRoot: Scalars['Bytes32']['output']; - txPointer: Scalars['TxPointer']['output']; - utxoId: Scalars['UtxoId']['output']; -}; - -export type IInputMessage = { - __typename?: 'InputMessage'; - amount: Scalars['U64']['output']; - data: Scalars['HexString']['output']; - nonce: Scalars['Nonce']['output']; - predicate: Scalars['HexString']['output']; - predicateData: Scalars['HexString']['output']; - predicateGasUsed: Scalars['U64']['output']; - recipient: Scalars['Address']['output']; - sender: Scalars['Address']['output']; - witnessIndex: Scalars['Int']['output']; -}; - -export type ILightOperation = { - __typename?: 'LightOperation'; - base: Scalars['U64']['output']; - unitsPerGas: Scalars['U64']['output']; -}; - -export type IMerkleProof = { - __typename?: 'MerkleProof'; - proofIndex: Scalars['U64']['output']; - proofSet: Array; -}; - -export type IMessage = { - __typename?: 'Message'; - amount: Scalars['U64']['output']; - daHeight: Scalars['U64']['output']; - data: Scalars['HexString']['output']; - nonce: Scalars['Nonce']['output']; - recipient: Scalars['Address']['output']; - sender: Scalars['Address']['output']; -}; - -export type IMessageCoin = { - __typename?: 'MessageCoin'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - daHeight: Scalars['U64']['output']; - nonce: Scalars['Nonce']['output']; - recipient: Scalars['Address']['output']; - sender: Scalars['Address']['output']; -}; - -export type IMessageConnection = { - __typename?: 'MessageConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type IMessageEdge = { - __typename?: 'MessageEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: IMessage; -}; - -export type IMessageProof = { - __typename?: 'MessageProof'; - amount: Scalars['U64']['output']; - blockProof: IMerkleProof; - commitBlockHeader: IHeader; - data: Scalars['HexString']['output']; - messageBlockHeader: IHeader; - messageProof: IMerkleProof; - nonce: Scalars['Nonce']['output']; - recipient: Scalars['Address']['output']; - sender: Scalars['Address']['output']; -}; - -export enum IMessageState { - NotFound = 'NOT_FOUND', - Spent = 'SPENT', - Unspent = 'UNSPENT', -} - -export type IMessageStatus = { - __typename?: 'MessageStatus'; - state: IMessageState; -}; - -export type IMutation = { - __typename?: 'Mutation'; - continueTx: IRunResult; - /** Execute a dry-run of the transaction using a fork of current state, no changes are committed. */ - dryRun: Array; - endSession: Scalars['Boolean']['output']; - execute: Scalars['Boolean']['output']; - /** - * Sequentially produces `blocks_to_produce` blocks. The first block starts with - * `start_timestamp`. If the block production in the [`crate::service::Config`] is - * `Trigger::Interval { block_time }`, produces blocks with `block_time ` intervals between - * them. The `start_timestamp` is the timestamp in seconds. - */ - produceBlocks: Scalars['U32']['output']; - reset: Scalars['Boolean']['output']; - setBreakpoint: Scalars['Boolean']['output']; - setSingleStepping: Scalars['Boolean']['output']; - startSession: Scalars['ID']['output']; - startTx: IRunResult; - /** - * Submits transaction to the `TxPool`. - * - * Returns submitted transaction if the transaction is included in the `TxPool` without problems. - */ - submit: ITransaction; -}; - -export type IMutationContinueTxArgs = { - id: Scalars['ID']['input']; -}; - -export type IMutationDryRunArgs = { - tx: Scalars['HexString']['input']; - utxoValidation: InputMaybe; -}; - -export type IMutationEndSessionArgs = { - id: Scalars['ID']['input']; -}; - -export type IMutationExecuteArgs = { - id: Scalars['ID']['input']; - op: Scalars['String']['input']; -}; - -export type IMutationProduceBlocksArgs = { - blocksToProduce: Scalars['U32']['input']; - startTimestamp: InputMaybe; -}; - -export type IMutationResetArgs = { - id: Scalars['ID']['input']; -}; - -export type IMutationSetBreakpointArgs = { - breakpoint: IBreakpoint; - id: Scalars['ID']['input']; -}; - -export type IMutationSetSingleSteppingArgs = { - enable: Scalars['Boolean']['input']; - id: Scalars['ID']['input']; -}; - -export type IMutationStartTxArgs = { - id: Scalars['ID']['input']; - txJson: Scalars['String']['input']; -}; - -export type IMutationSubmitArgs = { - tx: Scalars['HexString']['input']; -}; - -export type INodeInfo = { - __typename?: 'NodeInfo'; - maxDepth: Scalars['U64']['output']; - maxTx: Scalars['U64']['output']; - minGasPrice: Scalars['U64']['output']; - nodeVersion: Scalars['String']['output']; - peers: Array; - utxoValidation: Scalars['Boolean']['output']; - vmBacktrace: Scalars['Boolean']['output']; -}; - -export type IOutput = - | IChangeOutput - | ICoinOutput - | IContractCreated - | IContractOutput - | IVariableOutput; - -/** - * A separate `Breakpoint` type to be used as an output, as a single - * type cannot act as both input and output type in async-graphql - */ -export type IOutputBreakpoint = { - __typename?: 'OutputBreakpoint'; - contract: Scalars['ContractId']['output']; - pc: Scalars['U64']['output']; -}; - -/** Information about pagination in a connection */ -export type IPageInfo = { - __typename?: 'PageInfo'; - /** When paginating forwards, the cursor to continue. */ - endCursor: Maybe; - /** When paginating forwards, are there more items? */ - hasNextPage: Scalars['Boolean']['output']; - /** When paginating backwards, are there more items? */ - hasPreviousPage: Scalars['Boolean']['output']; - /** When paginating backwards, the cursor to continue. */ - startCursor: Maybe; -}; - -export type IPeerInfo = { - __typename?: 'PeerInfo'; - /** The advertised multi-addrs that can be used to connect to this peer */ - addresses: Array; - /** The internal fuel p2p reputation of this peer */ - appScore: Scalars['Float']['output']; - /** The last reported height of the peer */ - blockHeight: Maybe; - /** The self-reported version of the client the peer is using */ - clientVersion: Maybe; - /** The libp2p peer id */ - id: Scalars['String']['output']; - /** The last heartbeat from this peer in unix epoch time ms */ - lastHeartbeatMs: Scalars['U64']['output']; -}; - -export type IPoAConsensus = { - __typename?: 'PoAConsensus'; - /** Gets the signature of the block produced by `PoA` consensus. */ - signature: Scalars['Signature']['output']; -}; - -export type IPolicies = { - __typename?: 'Policies'; - gasPrice: Maybe; - maturity: Maybe; - maxFee: Maybe; - witnessLimit: Maybe; -}; - -export type IPredicateParameters = { - __typename?: 'PredicateParameters'; - maxGasPerPredicate: Scalars['U64']['output']; - maxMessageDataLength: Scalars['U64']['output']; - maxPredicateDataLength: Scalars['U64']['output']; - maxPredicateLength: Scalars['U64']['output']; -}; - -export type IProgramState = { - __typename?: 'ProgramState'; - data: Scalars['HexString']['output']; - returnType: IReturnType; -}; - -export type IQuery = { - __typename?: 'Query'; - balance: IBalance; - balances: IBalanceConnection; - block: Maybe; - blocks: IBlockConnection; - chain: IChainInfo; - /** Gets the coin by `utxo_id`. */ - coin: Maybe; - /** Gets all unspent coins of some `owner` maybe filtered with by `asset_id` per page. */ - coins: ICoinConnection; - /** - * For each `query_per_asset`, get some spendable coins(of asset specified by the query) owned by - * `owner` that add up at least the query amount. The returned coins can be spent. - * The number of coins is optimized to prevent dust accumulation. - * - * The query supports excluding and maximum the number of coins. - * - * Returns: - * The list of spendable coins per asset from the query. The length of the result is - * the same as the length of `query_per_asset`. The ordering of assets and `query_per_asset` - * is the same. - */ - coinsToSpend: Array>; - contract: Maybe; - contractBalance: IContractBalance; - contractBalances: IContractBalanceConnection; - /** Estimate the predicate gas for the provided transaction */ - estimatePredicates: ITransaction; - /** Returns true when the GraphQL API is serving requests. */ - health: Scalars['Boolean']['output']; - memory: Scalars['String']['output']; - messageProof: Maybe; - messageStatus: IMessageStatus; - messages: IMessageConnection; - nodeInfo: INodeInfo; - register: Scalars['U64']['output']; - transaction: Maybe; - transactions: ITransactionConnection; - transactionsByOwner: ITransactionConnection; -}; - -export type IQueryBalanceArgs = { - assetId: Scalars['AssetId']['input']; - owner: Scalars['Address']['input']; -}; - -export type IQueryBalancesArgs = { - after: InputMaybe; - before: InputMaybe; - filter: IBalanceFilterInput; - first: InputMaybe; - last: InputMaybe; -}; - -export type IQueryBlockArgs = { - height: InputMaybe; - id: InputMaybe; -}; - -export type IQueryBlocksArgs = { - after: InputMaybe; - before: InputMaybe; - first: InputMaybe; - last: InputMaybe; -}; - -export type IQueryCoinArgs = { - utxoId: Scalars['UtxoId']['input']; -}; - -export type IQueryCoinsArgs = { - after: InputMaybe; - before: InputMaybe; - filter: ICoinFilterInput; - first: InputMaybe; - last: InputMaybe; -}; - -export type IQueryCoinsToSpendArgs = { - excludedIds: InputMaybe; - owner: Scalars['Address']['input']; - queryPerAsset: Array; -}; - -export type IQueryContractArgs = { - id: Scalars['ContractId']['input']; -}; - -export type IQueryContractBalanceArgs = { - asset: Scalars['AssetId']['input']; - contract: Scalars['ContractId']['input']; -}; - -export type IQueryContractBalancesArgs = { - after: InputMaybe; - before: InputMaybe; - filter: IContractBalanceFilterInput; - first: InputMaybe; - last: InputMaybe; -}; - -export type IQueryEstimatePredicatesArgs = { - tx: Scalars['HexString']['input']; -}; - -export type IQueryMemoryArgs = { - id: Scalars['ID']['input']; - size: Scalars['U32']['input']; - start: Scalars['U32']['input']; -}; - -export type IQueryMessageProofArgs = { - commitBlockHeight: InputMaybe; - commitBlockId: InputMaybe; - nonce: Scalars['Nonce']['input']; - transactionId: Scalars['TransactionId']['input']; -}; - -export type IQueryMessageStatusArgs = { - nonce: Scalars['Nonce']['input']; -}; - -export type IQueryMessagesArgs = { - after: InputMaybe; - before: InputMaybe; - first: InputMaybe; - last: InputMaybe; - owner: InputMaybe; -}; - -export type IQueryRegisterArgs = { - id: Scalars['ID']['input']; - register: Scalars['U32']['input']; -}; - -export type IQueryTransactionArgs = { - id: Scalars['TransactionId']['input']; -}; - -export type IQueryTransactionsArgs = { - after: InputMaybe; - before: InputMaybe; - first: InputMaybe; - last: InputMaybe; -}; - -export type IQueryTransactionsByOwnerArgs = { - after: InputMaybe; - before: InputMaybe; - first: InputMaybe; - last: InputMaybe; - owner: Scalars['Address']['input']; -}; - -export type IReceipt = { - __typename?: 'Receipt'; - amount: Maybe; - assetId: Maybe; - contract: Maybe; - contractId: Maybe; - data: Maybe; - digest: Maybe; - gas: Maybe; - gasUsed: Maybe; - is: Maybe; - len: Maybe; - nonce: Maybe; - param1: Maybe; - param2: Maybe; - pc: Maybe; - ptr: Maybe; - ra: Maybe; - rb: Maybe; - rc: Maybe; - rd: Maybe; - reason: Maybe; - receiptType: IReceiptType; - recipient: Maybe; - result: Maybe; - sender: Maybe; - subId: Maybe; - to: Maybe; - toAddress: Maybe; - val: Maybe; -}; - -export enum IReceiptType { - Burn = 'BURN', - Call = 'CALL', - Log = 'LOG', - LogData = 'LOG_DATA', - MessageOut = 'MESSAGE_OUT', - Mint = 'MINT', - Panic = 'PANIC', - Return = 'RETURN', - ReturnData = 'RETURN_DATA', - Revert = 'REVERT', - ScriptResult = 'SCRIPT_RESULT', - Transfer = 'TRANSFER', - TransferOut = 'TRANSFER_OUT', -} - -export enum IReturnType { - Return = 'RETURN', - ReturnData = 'RETURN_DATA', - Revert = 'REVERT', -} - -export type IRunResult = { - __typename?: 'RunResult'; - breakpoint: Maybe; - jsonReceipts: Array; - state: IRunState; -}; - -export enum IRunState { - /** Stopped on a breakpoint */ - Breakpoint = 'BREAKPOINT', - /** All breakpoints have been processed, and the program has terminated */ - Completed = 'COMPLETED', -} - -export type IScriptParameters = { - __typename?: 'ScriptParameters'; - maxScriptDataLength: Scalars['U64']['output']; - maxScriptLength: Scalars['U64']['output']; -}; - -export type ISpendQueryElementInput = { - /** Target amount for the query. */ - amount: Scalars['U64']['input']; - /** Identifier of the asset to spend. */ - assetId: Scalars['AssetId']['input']; - /** The maximum number of currencies for selection. */ - max: InputMaybe; -}; - -export type ISqueezedOutStatus = { - __typename?: 'SqueezedOutStatus'; - reason: Scalars['String']['output']; -}; - -export type ISubmittedStatus = { - __typename?: 'SubmittedStatus'; - time: Scalars['Tai64Timestamp']['output']; -}; - -export type ISubscription = { - __typename?: 'Subscription'; - /** - * Returns a stream of status updates for the given transaction id. - * If the current status is [`TransactionStatus::Success`], [`TransactionStatus::SqueezedOut`] - * or [`TransactionStatus::Failed`] the stream will return that and end immediately. - * If the current status is [`TransactionStatus::Submitted`] this will be returned - * and the stream will wait for a future update. - * - * This stream will wait forever so it's advised to use within a timeout. - * - * It is possible for the stream to miss an update if it is polled slower - * then the updates arrive. In such a case the stream will close without - * a status. If this occurs the stream can simply be restarted to return - * the latest status. - */ - statusChange: ITransactionStatus; - /** Submits transaction to the `TxPool` and await either confirmation or failure. */ - submitAndAwait: ITransactionStatus; -}; - -export type ISubscriptionStatusChangeArgs = { - id: Scalars['TransactionId']['input']; -}; - -export type ISubscriptionSubmitAndAwaitArgs = { - tx: Scalars['HexString']['input']; -}; - -export type ISuccessStatus = { - __typename?: 'SuccessStatus'; - block: IBlock; - programState: Maybe; - receipts: Array; - time: Scalars['Tai64Timestamp']['output']; - transactionId: Scalars['TransactionId']['output']; -}; - -export type ITransaction = { - __typename?: 'Transaction'; - bytecodeLength: Maybe; - bytecodeWitnessIndex: Maybe; - gasPrice: Maybe; - id: Scalars['TransactionId']['output']; - inputAssetIds: Maybe>; - inputContract: Maybe; - inputContracts: Maybe>; - inputs: Maybe>; - isCreate: Scalars['Boolean']['output']; - isMint: Scalars['Boolean']['output']; - isScript: Scalars['Boolean']['output']; - maturity: Maybe; - mintAmount: Maybe; - mintAssetId: Maybe; - outputContract: Maybe; - outputs: Array; - policies: Maybe; - /** Return the transaction bytes using canonical encoding */ - rawPayload: Scalars['HexString']['output']; - receipts: Maybe>; - receiptsRoot: Maybe; - salt: Maybe; - script: Maybe; - scriptData: Maybe; - scriptGasLimit: Maybe; - status: Maybe; - storageSlots: Maybe>; - txPointer: Maybe; - witnesses: Maybe>; -}; - -export type ITransactionConnection = { - __typename?: 'TransactionConnection'; - /** A list of edges. */ - edges: Array; - /** A list of nodes. */ - nodes: Array; - /** Information to aid in pagination. */ - pageInfo: IPageInfo; -}; - -/** An edge in a connection. */ -export type ITransactionEdge = { - __typename?: 'TransactionEdge'; - /** A cursor for use in pagination */ - cursor: Scalars['String']['output']; - /** The item at the end of the edge */ - node: ITransaction; -}; - -export type ITransactionStatus = - | IFailureStatus - | ISqueezedOutStatus - | ISubmittedStatus - | ISuccessStatus; - -export type ITxParameters = { - __typename?: 'TxParameters'; - maxGasPerTx: Scalars['U64']['output']; - maxInputs: Scalars['U8']['output']; - maxOutputs: Scalars['U8']['output']; - maxSize: Scalars['U64']['output']; - maxWitnesses: Scalars['U32']['output']; -}; - -export type IVariableOutput = { - __typename?: 'VariableOutput'; - amount: Scalars['U64']['output']; - assetId: Scalars['AssetId']['output']; - to: Scalars['Address']['output']; -}; - -export type IAddressTransactionsQueryVariables = Exact<{ - first: InputMaybe; - owner: Scalars['Address']['input']; -}>; - -export type IAddressTransactionsQuery = { - __typename?: 'Query'; - transactionsByOwner: { - __typename?: 'TransactionConnection'; - edges: Array<{ - __typename?: 'TransactionEdge'; - node: { - __typename?: 'Transaction'; - id: string; - rawPayload: string; - gasPrice: string | null; - receipts: Array<{ - __typename?: 'Receipt'; - pc: string | null; - is: string | null; - toAddress: string | null; - amount: string | null; - assetId: string | null; - gas: string | null; - param1: string | null; - param2: string | null; - val: string | null; - ptr: string | null; - digest: string | null; - reason: string | null; - ra: string | null; - rb: string | null; - rc: string | null; - rd: string | null; - len: string | null; - receiptType: IReceiptType; - result: string | null; - gasUsed: string | null; - data: string | null; - sender: string | null; - recipient: string | null; - nonce: string | null; - contractId: string | null; - subId: string | null; - contract: { - __typename?: 'Contract'; - id: string; - bytecode: string; - } | null; - to: { __typename?: 'Contract'; id: string; bytecode: string } | null; - }> | null; - status: - | { - __typename?: 'FailureStatus'; - time: string; - reason: string; - type: 'FailureStatus'; - block: { __typename?: 'Block'; id: string }; - } - | { __typename?: 'SqueezedOutStatus'; type: 'SqueezedOutStatus' } - | { - __typename?: 'SubmittedStatus'; - time: string; - type: 'SubmittedStatus'; - } - | { - __typename?: 'SuccessStatus'; - time: string; - type: 'SuccessStatus'; - block: { __typename?: 'Block'; id: string }; - programState: { - __typename?: 'ProgramState'; - returnType: IReturnType; - data: string; - } | null; - } - | null; - }; - }>; - }; -}; - -export type ITransactionFragment = { - __typename?: 'Transaction'; - id: string; - rawPayload: string; - gasPrice: string | null; - receipts: Array<{ - __typename?: 'Receipt'; - pc: string | null; - is: string | null; - toAddress: string | null; - amount: string | null; - assetId: string | null; - gas: string | null; - param1: string | null; - param2: string | null; - val: string | null; - ptr: string | null; - digest: string | null; - reason: string | null; - ra: string | null; - rb: string | null; - rc: string | null; - rd: string | null; - len: string | null; - receiptType: IReceiptType; - result: string | null; - gasUsed: string | null; - data: string | null; - sender: string | null; - recipient: string | null; - nonce: string | null; - contractId: string | null; - subId: string | null; - contract: { __typename?: 'Contract'; id: string; bytecode: string } | null; - to: { __typename?: 'Contract'; id: string; bytecode: string } | null; - }> | null; - status: - | { - __typename?: 'FailureStatus'; - time: string; - reason: string; - type: 'FailureStatus'; - block: { __typename?: 'Block'; id: string }; - } - | { __typename?: 'SqueezedOutStatus'; type: 'SqueezedOutStatus' } - | { __typename?: 'SubmittedStatus'; time: string; type: 'SubmittedStatus' } - | { - __typename?: 'SuccessStatus'; - time: string; - type: 'SuccessStatus'; - block: { __typename?: 'Block'; id: string }; - programState: { - __typename?: 'ProgramState'; - returnType: IReturnType; - data: string; - } | null; - } - | null; -}; - -export type IContractFragmentFragment = { - __typename?: 'Contract'; - id: string; - bytecode: string; -}; - -export type IReceiptFragment = { - __typename?: 'Receipt'; - pc: string | null; - is: string | null; - toAddress: string | null; - amount: string | null; - assetId: string | null; - gas: string | null; - param1: string | null; - param2: string | null; - val: string | null; - ptr: string | null; - digest: string | null; - reason: string | null; - ra: string | null; - rb: string | null; - rc: string | null; - rd: string | null; - len: string | null; - receiptType: IReceiptType; - result: string | null; - gasUsed: string | null; - data: string | null; - sender: string | null; - recipient: string | null; - nonce: string | null; - contractId: string | null; - subId: string | null; - contract: { __typename?: 'Contract'; id: string; bytecode: string } | null; - to: { __typename?: 'Contract'; id: string; bytecode: string } | null; -}; - -export const gqlOperations = { - Query: { - AddressTransactions: 'AddressTransactions', - }, - Fragment: { - transaction: 'transaction', - contractFragment: 'contractFragment', - receipt: 'receipt', - }, -}; -export const ContractFragmentFragmentDoc = gql` - fragment contractFragment on Contract { - id - bytecode -} - `; -export const ReceiptFragmentDoc = gql` - fragment receipt on Receipt { - contract { - ...contractFragment - } - pc - is - to { - ...contractFragment - } - toAddress - amount - assetId - gas - param1 - param2 - val - ptr - digest - reason - ra - rb - rc - rd - len - receiptType - result - gasUsed - data - sender - recipient - nonce - contractId - subId -} - ${ContractFragmentFragmentDoc}`; -export const TransactionFragmentDoc = gql` - fragment transaction on Transaction { - id - rawPayload - gasPrice - receipts { - ...receipt - } - status { - type: __typename - ... on SubmittedStatus { - time - } - ... on SuccessStatus { - block { - id - } - time - programState { - returnType - data - } - } - ... on FailureStatus { - block { - id - } - time - reason - } - } -} - ${ReceiptFragmentDoc}`; -export const AddressTransactionsDocument = gql` - query AddressTransactions($first: Int, $owner: Address!) { - transactionsByOwner(first: $first, owner: $owner) { - edges { - node { - ...transaction - receipts { - ...receipt - } - } - } - } -} - ${TransactionFragmentDoc} -${ReceiptFragmentDoc}`; - -export type SdkFunctionWrapper = ( - action: (requestHeaders?: Record) => Promise, - operationName: string, - operationType?: string -) => Promise; - -const defaultWrapper: SdkFunctionWrapper = ( - action, - _operationName, - _operationType -) => action(); - -export function getSdk( - client: GraphQLClient, - withWrapper: SdkFunctionWrapper = defaultWrapper -) { - return { - AddressTransactions( - variables: IAddressTransactionsQueryVariables, - requestHeaders?: GraphQLClientRequestHeaders - ): Promise { - return withWrapper( - (wrappedRequestHeaders) => - client.request( - AddressTransactionsDocument, - variables, - { ...requestHeaders, ...wrappedRequestHeaders } - ), - 'AddressTransactions', - 'query' - ); - }, - }; -} -export type Sdk = ReturnType; diff --git a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx index 5332ecf26..b930905b1 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx @@ -54,7 +54,11 @@ export function BalanceWidget({ css={{ boxShadow: '$sm' }} /> - + {account.name} { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + graphql.query('getNodeInfo', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + graphql.query('getBalances', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), +]); + describe('accountsMachine', () => { let service: AccountsMachineService; let state: ReturnType; diff --git a/packages/app/src/systems/Account/machines/editAccountMachine.test.ts b/packages/app/src/systems/Account/machines/editAccountMachine.test.ts index 6b2990bf4..b627ff354 100644 --- a/packages/app/src/systems/Account/machines/editAccountMachine.test.ts +++ b/packages/app/src/systems/Account/machines/editAccountMachine.test.ts @@ -5,6 +5,9 @@ import { expectStateMatch } from '~/systems/Core/__tests__/utils'; import { MOCK_ACCOUNTS } from '../__mocks__'; import { AccountService } from '../services'; +import { graphql } from 'msw'; +import { mockServer } from '~/mocks/server'; +import { MOCK_TRANSACTION_WITH_RECEIPTS_GQL } from '~/systems/Transaction/__mocks__/transaction'; import type { EditAccountMachineService, EditAccountMachineEvents as MachineEvents, @@ -31,6 +34,18 @@ const machine = editAccountMachine.withContext({}).withConfig({ }, }); +mockServer([ + graphql.query('getChain', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + graphql.query('getNodeInfo', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + graphql.query('getBalances', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), +]); + describe('editAccountMachine', () => { let service: EditAccountMachineService; diff --git a/packages/app/src/systems/Account/services/account.test.ts b/packages/app/src/systems/Account/services/account.test.ts index 9f569b1af..6ff95d6ec 100644 --- a/packages/app/src/systems/Account/services/account.test.ts +++ b/packages/app/src/systems/Account/services/account.test.ts @@ -1,22 +1,37 @@ -import { BaseAssetId, bn } from 'fuels'; +import { Provider, bn } from 'fuels'; import { mockServer } from '~/mocks/server'; -import { mockBalancesOnGraphQL } from '~/systems/Asset/__mocks__/assets'; +import { + MOCK_BASE_ASSET_ID, + mockBalancesOnGraphQL, +} from '~/systems/Asset/__mocks__/assets'; import { MOCK_ACCOUNTS } from '../__mocks__'; +import { graphql } from 'msw'; +import { MOCK_TRANSACTION_WITH_RECEIPTS_GQL } from '~/systems/Transaction/__mocks__/transaction'; import { AccountService } from './account'; +const providerUrl = import.meta.env.VITE_FUEL_PROVIDER_URL; const MOCK_ACCOUNT = MOCK_ACCOUNTS[0]; + const MOCK_BALANCES = [ { node: { - assetId: BaseAssetId, + assetId: MOCK_BASE_ASSET_ID, amount: bn(1000), }, }, ]; -mockServer([mockBalancesOnGraphQL(MOCK_BALANCES)]); +mockServer([ + graphql.query('getChain', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + graphql.query('getNodeInfo', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), + mockBalancesOnGraphQL(MOCK_BALANCES), +]); describe('AccountService', () => { beforeEach(async () => { @@ -32,12 +47,13 @@ describe('AccountService', () => { it('should fetch balance from account', async () => { const account = await AccountService.addAccount({ data: MOCK_ACCOUNT }); - const providerUrl = import.meta.env.VITE_FUEL_PROVIDER_URL; if (!account) return; const result = await AccountService.fetchBalance({ account, providerUrl }); expect(result.balance).toBe(bn(1000).toString()); expect(result.address).toBe(MOCK_ACCOUNT.address); - expect(result.balances?.[0].assetId).toBe(BaseAssetId); + expect(result.balances?.[0].assetId).toBe( + (await Provider.create(providerUrl)).getBaseAssetId() + ); }); it('should convert an array of accounts into a map', async () => { diff --git a/packages/app/src/systems/Account/services/account.ts b/packages/app/src/systems/Account/services/account.ts index 864f0d7ab..26677eeca 100644 --- a/packages/app/src/systems/Account/services/account.ts +++ b/packages/app/src/systems/Account/services/account.ts @@ -1,6 +1,5 @@ import type { Account } from '@fuel-wallet/types'; import { Address, Provider, bn } from 'fuels'; -import { isEth } from '~/systems/Asset/utils/asset'; import type { Maybe } from '~/systems/Core/types'; import { db } from '~/systems/Core/utils/database'; import { getUniqueString } from '~/systems/Core/utils/string'; @@ -93,8 +92,13 @@ export class AccountService { const { account, providerUrl } = input; try { - const balances = await getBalances(providerUrl, account.publicKey); - const ethAsset = balances.find(isEth); + const provider = await Provider.create(providerUrl!); + const balances = await getBalances(provider, account.publicKey); + const baseAssetId = provider.getBaseAssetId(); + + const ethAsset = balances.find( + (balance) => balance.assetId === baseAssetId.toString() + ); const ethBalance = ethAsset?.amount; const nextAccount = await AccountService.setBalance({ data: { @@ -200,8 +204,7 @@ export class AccountService { // Private methods // ---------------------------------------------------------------------------- -async function getBalances(providerUrl: string, publicKey = '0x00') { - const provider = await Provider.create(providerUrl!); +async function getBalances(provider: Provider, publicKey = '0x00') { const address = Address.fromPublicKey(publicKey); const balances = await provider.getBalances(address); return balances; diff --git a/packages/app/src/systems/Asset/__mocks__/assets.tsx b/packages/app/src/systems/Asset/__mocks__/assets.tsx index fdf70cc4e..26f376833 100644 --- a/packages/app/src/systems/Asset/__mocks__/assets.tsx +++ b/packages/app/src/systems/Asset/__mocks__/assets.tsx @@ -1,13 +1,33 @@ -import type { BigNumberish } from 'fuels'; -import { BaseAssetId, bn } from 'fuels'; +import { type AssetFuel, type BigNumberish, assets, bn } from 'fuels'; import { graphql } from 'msw'; -import { fuelAssets } from '~/systems/Core'; +import { uniqueId } from 'xstate/lib/utils'; -export const MOCK_ASSETS = fuelAssets.map((item) => ({ +export const MOCK_NETWORK = { + id: uniqueId(), + name: 'Another', + url: 'https://devnet.fuel.network/v1/graphql', +}; + +export const MOCK_FUEL_ASSETS = assets.map((asset) => { + const fuelNetworkAsset = asset.networks.find( + (n) => n.type === 'fuel' + ) as AssetFuel; + return { + ...asset, + assetId: fuelNetworkAsset.assetId, + decimals: fuelNetworkAsset.decimals, + }; +}); + +export const MOCK_ASSETS = MOCK_FUEL_ASSETS.map((item) => ({ ...item, amount: bn(14563943834), })); +// BaseAssetId replacement +export const MOCK_BASE_ASSET_ID = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + export const MOCK_CUSTOM_ASSET = { assetId: '0x566012155ae253353c7df01f36c8f6249c94131a69a3484bdb0234e3822b5d90', name: 'New', @@ -18,7 +38,7 @@ export const MOCK_CUSTOM_ASSET = { }; export const MOCK_ASSETS_AMOUNTS = [ - ...fuelAssets.map((item, idx) => ({ + ...MOCK_FUEL_ASSETS.map((item, idx) => ({ ...item, amount: bn(idx % 2 === 0 ? 14563943834 : -14563943834), })), @@ -28,7 +48,7 @@ export const MOCK_ASSETS_AMOUNTS = [ export const MOCK_ASSETS_NODE = [ { node: { - assetId: BaseAssetId, + assetId: MOCK_BASE_ASSET_ID, amount: bn(30000000000), }, }, diff --git a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.stories.tsx b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.stories.tsx index 7a82c3af9..5483f44c7 100644 --- a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.stories.tsx +++ b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.stories.tsx @@ -1,7 +1,7 @@ import { Box } from '@fuel-ui/react'; import { bn } from 'fuels'; -import { fuelAssets } from '~/systems/Core'; +import { MOCK_FUEL_ASSETS } from '../../__mocks__/assets'; import type { AssetItemProps } from './AssetItem'; import { AssetItem } from './AssetItem'; @@ -15,7 +15,7 @@ export const Usage = (args: AssetItemProps) => ( diff --git a/packages/app/src/systems/Asset/components/AssetSelect/AssetSelect.tsx b/packages/app/src/systems/Asset/components/AssetSelect/AssetSelect.tsx index 11317d27a..932fd5e57 100644 --- a/packages/app/src/systems/Asset/components/AssetSelect/AssetSelect.tsx +++ b/packages/app/src/systems/Asset/components/AssetSelect/AssetSelect.tsx @@ -1,5 +1,4 @@ import { cssObj, cx } from '@fuel-ui/css'; -import type { DropdownProps } from '@fuel-ui/react'; import { Avatar, Box, @@ -10,24 +9,19 @@ import { Text, } from '@fuel-ui/react'; import type { AssetAmount } from '@fuel-wallet/types'; -import { useState } from 'react'; +import { memo, useState } from 'react'; import type { Maybe } from '~/systems/Core'; import { shortAddress } from '~/systems/Core'; export type AssetSelectInput = AssetAmount; -export type AssetSelectProps = DropdownProps & { +export type AssetSelectProps = { items?: Maybe; selected?: Maybe; onSelect: (asset?: string | null) => void; }; -export function AssetSelect({ - items, - selected, - onSelect, - ...props -}: AssetSelectProps) { +function AssetSelectBase({ items, selected, onSelect }: AssetSelectProps) { const [isOpen, setIsOpen] = useState(false); const assetAmount = items?.find((i) => i.assetId === selected); @@ -37,7 +31,6 @@ export function AssetSelect({ return ( + error?: InsufficientInputAmountError | any; +}; + export type AssetsAmountProps = { amounts: AssetAmount[]; title?: string; diff --git a/packages/app/src/systems/Asset/events.tsx b/packages/app/src/systems/Asset/events.tsx index 9b16eac61..76d7e2310 100644 --- a/packages/app/src/systems/Asset/events.tsx +++ b/packages/app/src/systems/Asset/events.tsx @@ -24,5 +24,10 @@ export function assetEvents(store: StoreClass) { input, }); }, + reloadListedAssets() { + store.send(Services.assets, { + type: 'RELOAD_LISTED_ASSETS', + }); + }, }; } diff --git a/packages/app/src/systems/Asset/index.tsx b/packages/app/src/systems/Asset/index.tsx index 0943c7e6f..02b5c714b 100644 --- a/packages/app/src/systems/Asset/index.tsx +++ b/packages/app/src/systems/Asset/index.tsx @@ -3,6 +3,5 @@ export * from './hooks'; export * from './machines'; export * from './pages'; export * from './services'; -export * from './utils'; export * from './events'; export * from './routes'; diff --git a/packages/app/src/systems/Asset/machines/assetsMachine.test.tsx b/packages/app/src/systems/Asset/machines/assetsMachine.test.tsx index b1a77ee73..b8138821f 100644 --- a/packages/app/src/systems/Asset/machines/assetsMachine.test.tsx +++ b/packages/app/src/systems/Asset/machines/assetsMachine.test.tsx @@ -1,11 +1,11 @@ import { interpret } from 'xstate'; import { expectStateMatch } from '~/systems/Core/__tests__/utils'; -import { MOCK_CUSTOM_ASSET } from '../__mocks__/assets'; +import { MOCK_CUSTOM_ASSET, MOCK_NETWORK } from '../__mocks__/assets'; +import { NetworkService } from '../../Network/services'; import type { AssetsMachineService } from './assetsMachine'; import { assetsMachine } from './assetsMachine'; - const machine = assetsMachine.withContext({}).withConfig({ actions: { navigateBack() {}, @@ -17,6 +17,8 @@ describe('assetsMachine', () => { let state: ReturnType; beforeEach(async () => { + await NetworkService.clearNetworks(); + await NetworkService.addNetwork({ data: MOCK_NETWORK }); service = interpret(machine).start(); state = service.getSnapshot(); }); diff --git a/packages/app/src/systems/Asset/machines/assetsMachine.tsx b/packages/app/src/systems/Asset/machines/assetsMachine.tsx index 2d5f96831..1f60f6af9 100644 --- a/packages/app/src/systems/Asset/machines/assetsMachine.tsx +++ b/packages/app/src/systems/Asset/machines/assetsMachine.tsx @@ -2,7 +2,7 @@ import { toast } from '@fuel-ui/react'; import type { AssetData } from '@fuel-wallet/types'; import type { InterpreterFrom, StateFrom } from 'xstate'; import { assign, createMachine } from 'xstate'; -import { FetchMachine, fuelAssets } from '~/systems/Core'; +import { FetchMachine } from '~/systems/Core'; import type { AssetInputs } from '../services'; import { AssetService } from '../services'; @@ -41,6 +41,7 @@ type MachineEvents = | { type: 'ADD_ASSET'; input: AssetInputs['addAsset'] } | { type: 'UPDATE_ASSET'; input: AssetInputs['updateAsset'] } | { type: 'REMOVE_ASSET'; input: AssetInputs['removeAsset'] } + | { type: 'RELOAD_LISTED_ASSETS'; input?: never } | { type: 'CANCEL'; input?: null }; export const assetsMachine = createMachine( @@ -94,6 +95,9 @@ export const assetsMachine = createMachine( REMOVE_ASSET: { target: 'removing', }, + RELOAD_LISTED_ASSETS: { + target: 'settingListedAssets', + }, }, }, updating: { @@ -174,17 +178,7 @@ export const assetsMachine = createMachine( setListedAssets: FetchMachine.create({ showError: true, async fetch() { - await Promise.all( - fuelAssets.map((asset) => - AssetService.upsertAsset({ - data: { - ...asset, - isCustom: false, - imageUrl: asset.icon, - }, - }) - ) - ); + await AssetService.setListedAssets(); }, }), fetchAssets: FetchMachine.create< diff --git a/packages/app/src/systems/Asset/services/assets.ts b/packages/app/src/systems/Asset/services/assets.ts index 8fcea09bd..8b9fb1849 100644 --- a/packages/app/src/systems/Asset/services/assets.ts +++ b/packages/app/src/systems/Asset/services/assets.ts @@ -1,7 +1,9 @@ import type { AssetData } from '@fuel-wallet/types'; -import { isB256 } from 'fuels'; +import initialAssets from '@fuels/assets'; +import { type NetworkFuel, Provider, isB256 } from 'fuels'; import { db } from '~/systems/Core/utils/database'; import { getUniqueString } from '~/systems/Core/utils/string'; +import { NetworkService } from '~/systems/Network/services/network'; export type AssetInputs = { upsertAsset: { @@ -31,18 +33,69 @@ export type AssetInputs = { export class AssetService { static async upsertAsset(input: AssetInputs['upsertAsset']) { return db.transaction('rw!', db.assets, async () => { - const { assetId, ...updateData } = input.data; - const asset = await db.assets.get({ assetId }); - if (asset) { - await db.assets.update(assetId, updateData); - } else { - await db.assets.add(input.data); + const assets = await AssetService.getAssetsByFilter((a) => { + return a.name === input.data.name; + }); + if (assets?.[0]) { + await db.assets.delete(assets[0].assetId); } + await db.assets.add(input.data); - return db.assets.get({ assetId }); + return db.assets.get({ assetId: input.data.assetId }); }); } + static async setListedAssets() { + // @TODO: Remove when SDK provide correct asset id for the network + const legacyFuelBaseAssetId = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + const currentNetwork = await NetworkService.getSelectedNetwork(); + const provider = await Provider.create(currentNetwork?.url || ''); + const networkBaseAssetId = provider.getBaseAssetId(); + + const assetsPromises = initialAssets.map((asset) => { + const fuelNetworkAssets = asset.networks.filter( + (n) => n.type === 'fuel' + ) as Array; + + const fuelNetworkAsset = asset.networks.find( + (n) => n.type === 'fuel' && n.chainId === provider.getChainId() + ) as NetworkFuel; + + const networks = fuelNetworkAssets.map((network) => { + if ( + network.chainId === provider.getChainId() && + network.assetId === legacyFuelBaseAssetId + ) { + return { + ...network, + assetId: networkBaseAssetId, + }; + } + return network; + }); + + const newAsset = { + ...asset, + networks, + assetId: + fuelNetworkAsset.assetId === legacyFuelBaseAssetId + ? networkBaseAssetId + : fuelNetworkAsset.assetId, + decimals: fuelNetworkAsset.decimals, + }; + return AssetService.upsertAsset({ + data: { + ...newAsset, + isCustom: false, + imageUrl: newAsset.icon, + }, + }); + }); + + await Promise.all(assetsPromises); + } + static updateAsset(input: AssetInputs['updateAsset']) { if (!input.data) { throw new Error('Asset.data undefined'); @@ -99,7 +152,10 @@ export class AssetService { static async getAssetsByFilter(filterFn: (asset: AssetData) => boolean) { return db.transaction('r', db.assets, async () => { - const assets = db.assets.filter(filterFn).toArray(); + const assets: Array = []; + await db.assets.filter(filterFn).each((data) => { + assets.push({ ...data }); + }); return assets; }); } diff --git a/packages/app/src/systems/Asset/utils/asset.tsx b/packages/app/src/systems/Asset/utils/asset.tsx deleted file mode 100644 index a856b8653..000000000 --- a/packages/app/src/systems/Asset/utils/asset.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { BytesLike, CoinQuantity } from 'fuels'; -import { BaseAssetId, hexlify } from 'fuels'; - -type CoinLike = { - assetId?: BytesLike; -}; - -export function isEth(asset: BytesLike | CoinLike) { - const assetId = - typeof asset === 'string' ? asset : (asset as CoinQuantity).assetId; - return BaseAssetId === hexlify(assetId); -} diff --git a/packages/app/src/systems/Asset/utils/index.tsx b/packages/app/src/systems/Asset/utils/index.tsx deleted file mode 100644 index ea2719dd8..000000000 --- a/packages/app/src/systems/Asset/utils/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './asset'; diff --git a/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx b/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx new file mode 100644 index 000000000..e4d966154 --- /dev/null +++ b/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx @@ -0,0 +1,299 @@ +/** + * This file was a direct copy from "@fuel-ui/react" package. + * That library is legacy and not maintained anymore. + * To avoid overhead of versions we will use the component directly inside the wallet + **/ + +import { cssObj } from '@fuel-ui/css'; +import type { BN } from 'fuels'; +import { DEFAULT_DECIMAL_UNITS, bn, format } from 'fuels'; +import { useEffect, useState } from 'react'; +import type { FC } from 'react'; + +import { + Avatar, + Box, + Button, + Flex, + Icon, + Image, + Input, + InputAmountLoader, + type InputNumberProps, + type InputProps, + Text, + Tooltip, +} from '@fuel-ui/react'; + +export const DECIMAL_UNITS = DEFAULT_DECIMAL_UNITS; + +export function formatAmountLeadingZeros(text: string): string { + const valueWithoutLeadingZeros = text + .replace(/^0\d/, (substring) => substring.replace(/^0+(?=[\d])/, '')) + .replace(/^0+(\d\.)/, '$1'); + const startsWithPoint = valueWithoutLeadingZeros.startsWith('.'); + + if (!startsWithPoint) { + return valueWithoutLeadingZeros; + } + if (valueWithoutLeadingZeros.length < 3) { + return `0${valueWithoutLeadingZeros}`; + } + return text; +} + +export function createAmount(text: string, units: number = DECIMAL_UNITS) { + const textAmountFixed = formatAmountLeadingZeros(text); + return { + text: textAmountFixed, + amount: bn.parseUnits(text.replaceAll(',', ''), units), + }; +} + +export type InputAmountProps = Omit & { + name?: string; + label?: string; + balance?: BN; + units?: number; + balancePrecision?: number; + asset?: { name?: string; imageUrl?: string; address?: string }; + assetTooltip?: string; + hiddenMaxButton?: boolean; + hiddenBalance?: boolean; + value?: BN | null; + onChange?: (val: BN | null) => void; + onClickMax?: () => void; + // biome-ignore lint/suspicious/noExplicitAny: allow any + onClickAsset?: (e: any) => void; + /* Input props */ + inputProps?: InputNumberProps; +}; + +type InputAmountComponent = FC & { + Loader: typeof InputAmountLoader; +}; + +export const InputAmount: InputAmountComponent = ({ + name, + label, + balance: initialBalance, + balancePrecision = 3, + value, + units = DECIMAL_UNITS, + hiddenBalance, + hiddenMaxButton, + onChange, + onClickMax, + inputProps, + asset, + assetTooltip, + onClickAsset, + ...props +}) => { + const formatOpts = { units, precision: units }; + const [assetAmount, setAssetAmount] = useState( + !value || value.eq(0) ? '' : value.format(formatOpts) + ); + + const balance = initialBalance ?? bn(initialBalance); + const formattedBalance = balance.format({ + ...formatOpts, + precision: balance.eq(0) ? 1 : balancePrecision, + }); + + // biome-ignore lint/correctness/useExhaustiveDependencies: allow any + useEffect(() => { + handleAmountChange(value ? value.format(formatOpts) : ''); + }, [value?.toString()]); + + const handleAmountChange = (text: string) => { + const { text: newText, amount } = createAmount(text, formatOpts.units); + const { amount: currentAmount } = createAmount( + assetAmount, + formatOpts.units + ); + if (!currentAmount.eq(amount)) { + onChange?.(amount); + setAssetAmount(newText); + } + }; + + const getAssetImage = () => { + if (asset?.imageUrl) { + return ( + {`${asset.name} + ); + } + + return ( + + ); + }; + + return ( + + + {label} + + + { + handleAmountChange(e.target.value); + }} + decimalScale={units} + {...inputProps} + /> + {initialBalance && ( + + + {!hiddenMaxButton && ( + + )} + {asset && ( + + + + )} + + + )} + + + {!hiddenBalance && ( + + + Balance: {formattedBalance} + + + )} + + + ); +}; + +InputAmount.Loader = InputAmountLoader; + +const styles = { + input: cssObj({ + py: '$2', + px: '$3', + display: 'flex', + flexDirection: 'column', + height: 'auto', + gap: '$0', + + input: { + is: ['display'], + width: '100%', + boxSizing: 'border-box', + fontSize: '$md', + fontFamily: '$mono', + }, + + 'input, .fuel_input-element--right': { + px: '$0', + }, + }), + heading: cssObj({ + color: '$intentsBase9', + fontSize: '$sm', + lineHeight: '$tight', + }), + secondRow: cssObj({ + alignItems: 'center', + width: '100%', + }), + elementRight: cssObj({ + pr: '$0', + + '[aria-disabled="true"]': { + opacity: 'unset', + backgroundColor: 'unset', + color: 'unset', + }, + }), + balanceActions: cssObj({ + display: 'flex', + justifyContent: 'end', + }), + maxButton: cssObj({ + px: '$3', + width: '$8', + height: '$5', + borderRadius: '$default', + fontSize: '$sm', + fontFamily: '$mono', + }), + assetButton: cssObj({ + padding: '$1 $2', + height: 'auto', + gap: '$1', + + '[data-dropdown="true"]': { + padding: '$1 $1 $1 $2', + }, + }), + balanceContainer: cssObj({ + gap: '$1', + alignItems: 'center', + whiteSpace: 'nowrap', + lineHeight: '$tight', + fontSize: '$sm', + fontWeight: '$normal', + }), + balanceLabel: cssObj({ + color: '$intentsBase9', + }), + balanceValue: cssObj({ + fontFamily: '$mono', + color: '$intentsBase9', + }), + image: cssObj({ + borderRadius: '$full', + width: '$5', + height: '$5', + }), +}; diff --git a/packages/app/src/systems/Core/utils/animations.ts b/packages/app/src/systems/Core/utils/animations.ts index b83480c2c..e6f3762c3 100644 --- a/packages/app/src/systems/Core/utils/animations.ts +++ b/packages/app/src/systems/Core/utils/animations.ts @@ -15,9 +15,9 @@ export const animations = { factor = 30, transition = defaultTransition, }: Opts = defaultTransition) => ({ - initial: { opacity: 0.4, y: -factor }, + initial: { opacity: 0, y: -factor }, animate: { opacity: 1, y: 0 }, - exit: { opacity: 0.4, y: -factor }, + exit: { opacity: 0, y: -factor }, transition: { default: transition } as Transition, }), slideInRight: ({ diff --git a/packages/app/src/systems/Core/utils/assets.tsx b/packages/app/src/systems/Core/utils/assets.tsx deleted file mode 100644 index a93dc721a..000000000 --- a/packages/app/src/systems/Core/utils/assets.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import assets from '@fuels/assets'; -import type { AssetFuel } from 'fuels'; - -export const fuelAssets = assets.map((asset) => { - const fuelNetworkAsset = asset.networks.find( - (n) => n.type === 'fuel' - ) as AssetFuel; - return { - ...asset, - assetId: fuelNetworkAsset.assetId, - decimals: fuelNetworkAsset.decimals, - }; -}); diff --git a/packages/app/src/systems/Core/utils/database.ts b/packages/app/src/systems/Core/utils/database.ts index 92e4b1d03..7ee1e6b40 100644 --- a/packages/app/src/systems/Core/utils/database.ts +++ b/packages/app/src/systems/Core/utils/database.ts @@ -39,7 +39,7 @@ export class FuelDB extends Dexie { networks: '&id, &url, &name', connections: 'origin', transactions: '&id', - assets: '&assetId, &name, $symbol', + assets: '&assetId, &name, &symbol', abis: '&contractId', errors: '&id', }) diff --git a/packages/app/src/systems/Core/utils/graphql.tsx b/packages/app/src/systems/Core/utils/graphql.tsx deleted file mode 100644 index 096a8fe47..000000000 --- a/packages/app/src/systems/Core/utils/graphql.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { GraphQLClient } from 'graphql-request'; -import { getSdk } from '~/generated/graphql'; - -export function getGraphqlClient(providerUrl: string) { - const graphqlClient = new GraphQLClient(providerUrl); - return getSdk(graphqlClient); -} diff --git a/packages/app/src/systems/Core/utils/index.tsx b/packages/app/src/systems/Core/utils/index.tsx index fff3ebe60..5bfb71261 100644 --- a/packages/app/src/systems/Core/utils/index.tsx +++ b/packages/app/src/systems/Core/utils/index.tsx @@ -11,4 +11,3 @@ export * from './storage'; export * from './storybook'; export * from './wallet'; export * from './delay'; -export * from './assets'; diff --git a/packages/app/src/systems/DApp/__mocks__/dapp-transaction.ts b/packages/app/src/systems/DApp/__mocks__/dapp-transaction.ts index d9ed23379..53a866c24 100644 --- a/packages/app/src/systems/DApp/__mocks__/dapp-transaction.ts +++ b/packages/app/src/systems/DApp/__mocks__/dapp-transaction.ts @@ -1,4 +1,4 @@ -import { Address, BaseAssetId, Provider, Wallet, bn } from 'fuels'; +import { Address, Provider, bn } from 'fuels'; import { TxService } from '~/systems/Transaction/services'; function getAddressFromString(address: string) { @@ -8,23 +8,18 @@ function getAddressFromString(address: string) { } export const getMockedTransaction = async ( - owner: string, destiny: string, providerUrl: string ) => { - const ownerAddress = getAddressFromString(owner); const destinyAddress = getAddressFromString(destiny); const provider = await Provider.create(providerUrl); - const ownerWallet = Wallet.fromAddress(ownerAddress, provider); const transactionRequest = await TxService.createTransfer({ to: destinyAddress.toString(), amount: bn.parseUnits('0.1'), - assetId: BaseAssetId, - provider: ownerWallet.provider, + assetId: provider.getBaseAssetId(), + tip: bn(0), + gasLimit: bn(1_000_000), }); - return TxService.fundTransaction({ - wallet: ownerWallet, - transactionRequest, - }); + return transactionRequest; }; diff --git a/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx b/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx index 7129525c6..6f5993a6c 100644 --- a/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx +++ b/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx @@ -3,7 +3,6 @@ import { TransactionStatus } from 'fuels'; import { useCallback } from 'react'; import { Services, store } from '~/store'; import { useOverlay } from '~/systems/Overlay'; -import { getFilteredErrors } from '~/systems/Transaction'; import type { TxInputs } from '~/systems/Transaction/services'; import { TxRequestStatus } from '../machines/transactionRequestMachine'; @@ -16,11 +15,11 @@ const selectors = { account(state: TransactionRequestState) { return state.context.input.account; }, - txResult(state: TransactionRequestState) { - return state.context.response?.txResult; + txSummarySimulated(state: TransactionRequestState) { + return state.context.response?.txSummarySimulated; }, - approvedTx(state: TransactionRequestState) { - return state.context.response?.approvedTx; + txSummaryExecuted(state: TransactionRequestState) { + return state.context.response?.txSummaryExecuted; }, isLoadingAccounts(state: TransactionRequestState) { return state.matches('fetchingAccount'); @@ -30,12 +29,12 @@ const selectors = { }, errors(state: TransactionRequestState) { if (!state.context.errors) return {}; - const grouped = state.context.errors?.txDryRunGroupedErrors; - const general = getFilteredErrors(grouped, ['InsufficientInputAmount']); - const hasGeneral = Boolean(Object.keys(general || {}).length); - const unlockError = state.context.errors?.unlockError; + const simulateTxErrors = state.context.errors?.simulateTxErrors; + const hasSimulateTxErrors = Boolean( + Object.keys(simulateTxErrors || {}).length + ); const txApproveError = state.context.errors?.txApproveError; - return { txApproveError, unlockError, grouped, general, hasGeneral }; + return { txApproveError, simulateTxErrors, hasSimulateTxErrors }; }, status(externalLoading?: boolean) { return useCallback( @@ -102,17 +101,24 @@ export function useTransactionRequest(opts: UseTransactionRequestOpts = {}) { const txStatusSelector = selectors.status(isLoadingAccounts); const txStatus = useSelector(service, txStatusSelector); const title = useSelector(service, selectors.title); - const txResult = useSelector(service, selectors.txResult); - const approvedTx = useSelector(service, selectors.approvedTx); + const txSummarySimulated = useSelector(service, selectors.txSummarySimulated); + const txSummaryExecuted = useSelector(service, selectors.txSummaryExecuted); const origin = useSelector(service, selectors.origin); const originTitle = useSelector(service, selectors.originTitle); const favIconUrl = useSelector(service, selectors.favIconUrl); const isSendingTx = useSelector(service, selectors.sendingTx); const isPreLoading = useSelector(service, selectors.isPreLoading); const isLoading = status('loading'); - const showActions = !status('failed') && !status('success'); - const shouldShowTx = (status('waitingApproval') || isSendingTx) && !!txResult; - const shouldShowLoader = isPreLoading || !txResult; + const shouldShowActions = !status('success'); + const shouldShowTxExecuted = + !!txSummaryExecuted && (status('success') || status('failed')); + const shouldShowTxSimulated = + !shouldShowTxExecuted && + (status('waitingApproval') || isSendingTx) && + !!txSummarySimulated; + const shouldDisableApproveBtn = + shouldShowTxSimulated && errors.hasSimulateTxErrors; + const shouldShowLoader = isPreLoading || !txSummarySimulated; function closeDialog() { reset(); @@ -123,10 +129,10 @@ export function useTransactionRequest(opts: UseTransactionRequestOpts = {}) { return txStatus === status; } - function approveStatus() { + function executedStatus() { if (status('success')) return TransactionStatus.success; if (status('failed')) return TransactionStatus.failure; - return txResult?.status; + return txSummarySimulated?.status; } function approve() { @@ -151,21 +157,23 @@ export function useTransactionRequest(opts: UseTransactionRequestOpts = {}) { return { ...ctx, account, - approveStatus, + executedStatus, errors, isLoading, providerUrl, - showActions, status, origin, originTitle, favIconUrl, title, - txResult, + txSummarySimulated, txStatus, - approvedTx, + txSummaryExecuted, isSendingTx, - shouldShowTx, + shouldShowActions, + shouldDisableApproveBtn, + shouldShowTxSimulated, + shouldShowTxExecuted, shouldShowLoader, handlers: { request, diff --git a/packages/app/src/systems/DApp/machines/transactionRequestMachine.test.ts b/packages/app/src/systems/DApp/machines/transactionRequestMachine.test.ts index ae57727ec..11ca6b92f 100644 --- a/packages/app/src/systems/DApp/machines/transactionRequestMachine.test.ts +++ b/packages/app/src/systems/DApp/machines/transactionRequestMachine.test.ts @@ -11,20 +11,20 @@ import { transactionRequestMachine } from './transactionRequestMachine'; describe('txApproveMachine', () => { let service: TransactionRequestService; - let transactionRequest: TransactionRequest; + let transactionRequest: TransactionRequest | undefined; let data: MockVaultData; const openDialog = jest.fn(); beforeAll(async () => { data = await mockVault(); - transactionRequest = await getMockedTransaction( - data.account?.address.toLocaleLowerCase() || '', + const mocked = await getMockedTransaction( '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077', data.network?.url || '' ); + transactionRequest = mocked?.transactionRequest; }); - beforeEach(async () => { + beforeEach(() => { service = interpret( transactionRequestMachine .withContext({ input: {}, response: {} }) @@ -36,15 +36,15 @@ describe('txApproveMachine', () => { service.stop(); }); - it.only('should approve/send transaction', async () => { + it('should approve/send transaction', async () => { await expectStateMatch(service, 'idle'); service.send('START', { input: { address: data.account?.address, + origin: 'foo.com', transactionRequest, providerUrl: data.network?.url, - origin: 'foo.com', }, }); diff --git a/packages/app/src/systems/DApp/machines/transactionRequestMachine.tsx b/packages/app/src/systems/DApp/machines/transactionRequestMachine.tsx index e25319a94..acf22ce0b 100644 --- a/packages/app/src/systems/DApp/machines/transactionRequestMachine.tsx +++ b/packages/app/src/systems/DApp/machines/transactionRequestMachine.tsx @@ -38,23 +38,23 @@ type MachineContext = { account?: Account; }; response?: { - txResult?: TransactionSummary; - approvedTx?: TransactionResponse; + txSummarySimulated?: TransactionSummary; + txSummaryExecuted?: TransactionSummary; }; errors?: { - unlockError?: string; txApproveError?: VMApiError; - txDryRunGroupedErrors?: GroupedErrors; + simulateTxErrors?: GroupedErrors; }; }; type MachineServices = { send: { - data: TransactionResponse; + data: TransactionSummary; }; simulateTransaction: { data: { - txResult: TransactionSummary; + txSummary: TransactionSummary; + simulateTxErrors?: GroupedErrors; }; }; fetchGasPrice: { @@ -76,7 +76,6 @@ type MachineEvents = export const transactionRequestMachine = createMachine( { predictableActionArguments: true, - tsTypes: {} as import('./transactionRequestMachine.typegen').Typegen0, schema: { context: {} as MachineContext, @@ -115,20 +114,7 @@ export const transactionRequestMachine = createMachine( }, { actions: ['assignAccount'], - target: 'settingGasPrice', - }, - ], - }, - }, - settingGasPrice: { - tags: ['loading', 'preLoading'], - invoke: { - src: 'fetchGasPrice', - data: ({ input }: MachineContext) => ({ input }), - onDone: [ - { target: 'simulatingTransaction', - actions: ['assignGasPrice'], }, ], }, @@ -139,14 +125,9 @@ export const transactionRequestMachine = createMachine( src: 'simulateTransaction', data: ({ input }: MachineContext) => ({ input }), onDone: [ - { - actions: ['assignTxDryRunError'], - target: 'failed', - cond: FetchMachine.hasError, - }, { target: 'waitingApproval', - actions: ['assignTxResult'], + actions: ['assignTxSummarySimulated', 'assignSimulateTxErrors'], }, ], }, @@ -257,34 +238,25 @@ export const transactionRequestMachine = createMachine( }; }, }), - assignGasPrice: assign((ctx, ev) => { - if (!ctx.input.transactionRequest) { - throw new Error('Transaction is required'); - } - ctx.input.transactionRequest.gasPrice = ev.data; - return ctx; - }), assignApprovedTx: assign({ - response: (ctx, ev) => ({ ...ctx.response, approvedTx: ev.data }), + response: (ctx, ev) => ({ + ...ctx.response, + txSummaryExecuted: ev.data, + }), }), - assignTxResult: assign({ + assignTxSummarySimulated: assign({ response: (ctx, ev) => ({ ...ctx.response, - txResult: ev.data.txResult, + txSummarySimulated: ev.data.txSummary, }), }), - assignTxDryRunError: assign((ctx, ev) => { - const txDryRunGroupedErrors = getGroupedErrors( - // biome-ignore lint/suspicious/noExplicitAny: - (ev.data as any)?.error?.response?.errors - ); + assignSimulateTxErrors: assign((ctx, ev) => { return { ...ctx, errors: { ...ctx.errors, - txDryRunGroupedErrors, + simulateTxErrors: ev.data.simulateTxErrors, }, - error: JSON.stringify(txDryRunGroupedErrors), }; }), assignTxApproveError: assign((ctx, ev) => { @@ -301,16 +273,6 @@ export const transactionRequestMachine = createMachine( }), }, services: { - fetchGasPrice: FetchMachine.create({ - showError: false, - async fetch({ input }) { - if (!input?.providerUrl) { - throw new Error('providerUrl is required'); - } - const { minGasPrice } = await NetworkService.getNodeInfo(input); - return minGasPrice; - }, - }), simulateTransaction: FetchMachine.create< TxInputs['simulateTransaction'], MachineServices['simulateTransaction']['data'] @@ -324,12 +286,9 @@ export const transactionRequestMachine = createMachine( // this creates a better experience for the user as the // screen doesn't flash between states await delay(600); - const { txResult } = await TxService.simulateTransaction(input); - if (txResult.isStatusFailure) { - // TODO: add reason for error failure if the sdk supports it - throw new Error('The transaction will fail to run.'); - } - return { txResult }; + const txSummary = await TxService.simulateTransaction(input); + + return txSummary; }, }), send: FetchMachine.create< @@ -343,7 +302,10 @@ export const transactionRequestMachine = createMachine( if (!input?.address || !input?.transactionRequest) { throw new Error('Invalid approveTx input'); } - return TxService.send(input); + const txResponse = await TxService.send(input); + const txSummary = await txResponse.getTransactionSummary(); + + return txSummary; }, }), fetchAccount: FetchMachine.create< diff --git a/packages/app/src/systems/DApp/methods.ts b/packages/app/src/systems/DApp/methods.ts index 83ddbc905..a53bce22e 100644 --- a/packages/app/src/systems/DApp/methods.ts +++ b/packages/app/src/systems/DApp/methods.ts @@ -58,7 +58,7 @@ export class RequestMethods extends ExtensionPageConnection { ...WAIT_FOR_CONFIG, done: 'txSuccess', }); - return state.context.response?.approvedTx?.id; + return state.context.response?.txSummaryExecuted?.id; } async addAssets(input: MessageInputs['addAssets']) { diff --git a/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.stories.tsx b/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.stories.tsx index 8dbb41572..43829860a 100644 --- a/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.stories.tsx +++ b/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.stories.tsx @@ -10,12 +10,11 @@ import { getMockedTransaction } from '../../__mocks__/dapp-transaction'; import { TransactionRequest } from './TransactionRequest'; async function loader() { - const { account, password } = await createMockAccount(); + const { password } = await createMockAccount(); await NetworkService.clearNetworks(); const network = await NetworkService.addDefaultNetworks(); const wallet = new Signer(Signer.generatePrivateKey()); const transactionRequest = await getMockedTransaction( - account?.publicKey || '', wallet.publicKey, network?.url! ); diff --git a/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.tsx b/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.tsx index 1766d3310..1a2588def 100644 --- a/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.tsx +++ b/packages/app/src/systems/DApp/pages/TransactionRequest/TransactionRequest.tsx @@ -10,71 +10,43 @@ import { useTransactionRequest } from '../../hooks/useTransactionRequest'; export function TransactionRequest() { const txRequest = useTransactionRequest({ isOriginRequired: true }); - const { handlers, status, isSendingTx, txResult, ...ctx } = txRequest; - const { assets } = useAssets(); + const { + handlers, + status, + isSendingTx, + txSummarySimulated, + txSummaryExecuted, + ...ctx + } = txRequest; + const { assets, isLoading: isLoadingAssets } = useAssets(); if (!ctx.account) return null; - const shouldShowTx = status('waitingApproval') || isSendingTx; - - const Header = ( - <> - - {txResult?.status === TransactionStatus.failure ? ( - - - Simulating your transaction resulted in an error - - - {/* TODO: add a reason for the transaction failing if the sdk ever supports it */} - The transaction will fail to run. - - - ) : ( - - Confirm before approving - - Carefully check if all the details in your transaction are correct - - - )} - - ); + const isLoading = status('loading') || status('sending') || isLoadingAssets; return ( <> - {ctx.isLoading && !txResult && } - {shouldShowTx && ( + {ctx.shouldShowLoader && } + {ctx.shouldShowTxSimulated && ( )} - {(status('success') || status('failed')) && ( + {ctx.shouldShowTxExecuted && ( - } + providerUrl={ctx.providerUrl} footer={ status('failed') && ( diff --git a/packages/app/src/systems/Network/__mocks__/chainInfo.ts b/packages/app/src/systems/Network/__mocks__/chainInfo.ts index 3e960c64e..9f14cb797 100644 --- a/packages/app/src/systems/Network/__mocks__/chainInfo.ts +++ b/packages/app/src/systems/Network/__mocks__/chainInfo.ts @@ -2,162 +2,250 @@ export const MOCK_CHAIN_INFO = { chain: { name: 'local_testnet', latestBlock: { - id: '0x60c7cebe85db60d07d7735f3d4979443057a5739894ecbb77be982e52fddbafe', - header: { height: '45', time: '4611686020129440447' }, + id: '0x4a59174fe5a17015e3a1444e290409b6009db261455df4787f7e4dfe1ead33eb', + height: '15', + header: { + time: '4611686020142482819', + }, transactions: [ { - id: '0x34aa7a0c34a0e959601aac9ba4192a6b279b98dbb7a19a4e9d5ac5188d612ee6', + id: '0x7c23856da804f76e038197ebb491b4db7db38c8857c69b74f97a04149fce892e', }, { - id: '0x7de4c037da8d062483b8be1bb25dbd1e104431c38f0c0ac9a62379ccf2a30fa8', + id: '0x046ef1b81bd0d4b57e96127feef1d48a34b2fb30c22e2ef5cef20b9460777d04', }, ], }, daHeight: '0', - peerCount: 0, consensusParameters: { + version: 'V1', txParams: { + version: 'V1', maxInputs: '255', maxOutputs: '255', maxWitnesses: '255', - maxGasPerTx: '10000000', - maxSize: '17825792', + maxGasPerTx: '30000000', + maxSize: '112640', + maxBytecodeSubsections: '256', }, predicateParams: { - maxPredicateLength: '1048576', - maxPredicateDataLength: '1048576', - maxGasPerPredicate: '10000000', - maxMessageDataLength: '1048576', + version: 'V1', + maxPredicateLength: '102400', + maxPredicateDataLength: '102400', + maxGasPerPredicate: '30000000', + maxMessageDataLength: '102400', }, scriptParams: { - maxScriptLength: '1048576', - maxScriptDataLength: '1048576', + version: 'V1', + maxScriptLength: '102400', + maxScriptDataLength: '102400', + }, + contractParams: { + version: 'V1', + contractMaxSize: '102400', + maxStorageSlots: '1760', + }, + feeParams: { + version: 'V1', + gasPriceFactor: '92', + gasPerByte: '63', }, - contractParams: { contractMaxSize: '16777216', maxStorageSlots: '255' }, - feeParams: { gasPriceFactor: '92', gasPerByte: '4' }, gasCosts: { - add: '1', - addi: '1', + version: 'V1', + add: '2', + addi: '2', aloc: '1', - and: '1', - andi: '1', - bal: '13', - bhei: '1', - bhsh: '1', - burn: '132', - cb: '1', - cfei: '1', - cfsi: '1', - croo: '16', - div: '1', - divi: '1', - ecr1: '3000', - eck1: '951', - ed19: '3000', - eq: '1', - exp: '1', - expi: '1', + and: '2', + andi: '2', + bal: '366', + bhei: '2', + bhsh: '2', + burn: '33949', + cb: '2', + cfei: '2', + cfsi: '2', + div: '2', + divi: '2', + ecr1: '46165', + eck1: '3347', + ed19: '4210', + eq: '2', + exp: '2', + expi: '2', flag: '1', - gm: '1', - gt: '1', - gtf: '1', - ji: '1', - jmp: '1', - jne: '1', - jnei: '1', - jnzi: '1', - jmpf: '1', - jmpb: '1', - jnzf: '1', - jnzb: '1', - jnef: '1', - jneb: '1', - lb: '1', - log: '9', - lt: '1', - lw: '1', - mint: '135', - mlog: '1', - modOp: '1', - modi: '1', - moveOp: '1', - movi: '1', - mroo: '2', - mul: '1', - muli: '1', - mldv: '1', + gm: '2', + gt: '2', + gtf: '16', + ji: '2', + jmp: '2', + jne: '2', + jnei: '2', + jnzi: '2', + jmpf: '2', + jmpb: '2', + jnzf: '2', + jnzb: '2', + jnef: '2', + jneb: '2', + lb: '2', + log: '754', + lt: '2', + lw: '2', + mint: '35718', + mlog: '2', + modOp: '2', + modi: '2', + moveOp: '2', + movi: '2', + mroo: '5', + mul: '2', + muli: '2', + mldv: '4', noop: '1', - not: '1', - or: '1', - ori: '1', - poph: '2', - popl: '2', - pshh: '2', - pshl: '2', - ret: '13', - rvrt: '13', - sb: '1', - sll: '1', - slli: '1', - srl: '1', - srli: '1', - srw: '12', - sub: '1', - subi: '1', - sw: '1', - sww: '67', - time: '1', - tr: '105', - tro: '60', - wdcm: '1', - wqcm: '1', - wdop: '1', - wqop: '1', - wdml: '1', - wqml: '1', - wddv: '1', - wqdv: '2', - wdmd: '3', - wqmd: '4', - wdam: '2', - wqam: '3', - wdmm: '3', - wqmm: '3', - xor: '1', - xori: '1', - call: { __typename: 'LightOperation', base: '144', unitsPerGas: '214' }, - ccp: { __typename: 'LightOperation', base: '15', unitsPerGas: '103' }, - csiz: { __typename: 'LightOperation', base: '17', unitsPerGas: '790' }, - k256: { __typename: 'LightOperation', base: '11', unitsPerGas: '214' }, - ldc: { __typename: 'LightOperation', base: '15', unitsPerGas: '272' }, - logd: { __typename: 'LightOperation', base: '26', unitsPerGas: '64' }, - mcl: { __typename: 'LightOperation', base: '1', unitsPerGas: '3333' }, - mcli: { __typename: 'LightOperation', base: '1', unitsPerGas: '3333' }, - mcp: { __typename: 'LightOperation', base: '1', unitsPerGas: '2000' }, - mcpi: { __typename: 'LightOperation', base: '3', unitsPerGas: '2000' }, - meq: { __typename: 'LightOperation', base: '1', unitsPerGas: '2500' }, - retd: { __typename: 'LightOperation', base: '29', unitsPerGas: '62' }, - s256: { __typename: 'LightOperation', base: '2', unitsPerGas: '214' }, - scwq: { __typename: 'LightOperation', base: '13', unitsPerGas: '5' }, - smo: { __typename: 'LightOperation', base: '209', unitsPerGas: '55' }, - srwq: { __typename: 'LightOperation', base: '47', unitsPerGas: '5' }, - swwq: { __typename: 'LightOperation', base: '44', unitsPerGas: '5' }, - contractRoot: { + not: '2', + or: '2', + ori: '2', + poph: '3', + popl: '3', + pshh: '4', + pshl: '4', + ret: '733', + rvrt: '722', + sb: '2', + sll: '2', + slli: '2', + srl: '2', + srli: '2', + srw: '253', + sub: '2', + subi: '2', + sw: '2', + sww: '29053', + time: '79', + tr: '46242', + tro: '33251', + wdcm: '3', + wqcm: '3', + wdop: '3', + wqop: '3', + wdml: '3', + wqml: '4', + wddv: '5', + wqdv: '7', + wdmd: '11', + wqmd: '18', + wdam: '9', + wqam: '12', + wdmm: '11', + wqmm: '11', + xor: '2', + xori: '2', + call: { + __typename: 'LightOperation', + base: '21687', + unitsPerGas: '4', + }, + ccp: { __typename: 'LightOperation', - base: '75', + base: '59', + unitsPerGas: '20', + }, + croo: { + __typename: 'LightOperation', + base: '1', unitsPerGas: '1', }, - stateRoot: { + csiz: { + __typename: 'LightOperation', + base: '59', + unitsPerGas: '195', + }, + k256: { + __typename: 'LightOperation', + base: '282', + unitsPerGas: '3', + }, + ldc: { + __typename: 'LightOperation', + base: '45', + unitsPerGas: '65', + }, + logd: { + __typename: 'LightOperation', + base: '1134', + unitsPerGas: '2', + }, + mcl: { + __typename: 'LightOperation', + base: '3', + unitsPerGas: '523', + }, + mcli: { + __typename: 'LightOperation', + base: '3', + unitsPerGas: '526', + }, + mcp: { + __typename: 'LightOperation', + base: '3', + unitsPerGas: '448', + }, + mcpi: { __typename: 'LightOperation', - base: '412', + base: '7', + unitsPerGas: '585', + }, + meq: { + __typename: 'LightOperation', + base: '11', + unitsPerGas: '1097', + }, + retd: { + __typename: 'LightOperation', + base: '1086', + unitsPerGas: '2', + }, + s256: { + __typename: 'LightOperation', + base: '45', + unitsPerGas: '3', + }, + scwq: { + __typename: 'HeavyOperation', + base: '30375', + gasPerUnit: '28628', + }, + smo: { + __typename: 'LightOperation', + base: '64196', unitsPerGas: '1', }, - vmInitialization: { + srwq: { __typename: 'HeavyOperation', - base: '2000', - gasPerUnit: '0', + base: '262', + gasPerUnit: '249', + }, + swwq: { + __typename: 'HeavyOperation', + base: '28484', + gasPerUnit: '26613', + }, + contractRoot: { + __typename: 'LightOperation', + base: '45', + unitsPerGas: '1', + }, + stateRoot: { + __typename: 'HeavyOperation', + base: '350', + gasPerUnit: '176', + }, + vmInitialization: { + __typename: 'LightOperation', + base: '1645', + unitsPerGas: '14', }, - newStoragePerByte: '1', + newStoragePerByte: '63', }, baseAssetId: '0x0000000000000000000000000000000000000000000000000000000000000000', diff --git a/packages/app/src/systems/Network/__mocks__/networks.ts b/packages/app/src/systems/Network/__mocks__/networks.ts index 49ed35156..485fb6fe9 100644 --- a/packages/app/src/systems/Network/__mocks__/networks.ts +++ b/packages/app/src/systems/Network/__mocks__/networks.ts @@ -10,6 +10,6 @@ export const MOCK_NETWORKS = [ { id: uniqueId(), name: 'Another', - url: 'https://beta-5.fuel.network/graphql', + url: 'https://devnet.fuel.network/v1/graphql', }, ]; diff --git a/packages/app/src/systems/Network/machines/networksMachine.test.ts b/packages/app/src/systems/Network/machines/networksMachine.test.ts index c8543be4b..87b4b29c6 100644 --- a/packages/app/src/systems/Network/machines/networksMachine.test.ts +++ b/packages/app/src/systems/Network/machines/networksMachine.test.ts @@ -64,21 +64,6 @@ describe('networksMachine', () => { }, }; - it('should be able to add a new network', async () => { - state = await expectStateMatch(service, 'idle'); - - const nextState = service.nextState(addEv); - expect(nextState.value).toBe('addingNetwork'); - expect(nextState.hasTag('loading')).toBeTruthy(); - - service.send(addEv); - state = await expectStateMatch(service, 'idle'); - const networks = state.context.networks || []; - expect(networks?.length).toBe(2); - const networkId = networks?.[1].id; - await NetworkService.removeNetwork({ id: networkId as string }); - }); - it('should be able to remove a network', async () => { await expectStateMatch(service, 'idle'); @@ -100,6 +85,21 @@ describe('networksMachine', () => { expect(state.context.networks?.length).toBe(1); }); + it('should be able to add a new network', async () => { + state = await expectStateMatch(service, 'idle'); + + const nextState = service.nextState(addEv); + expect(nextState.value).toBe('addingNetwork'); + expect(nextState.hasTag('loading')).toBeTruthy(); + + service.send(addEv); + state = await expectStateMatch(service, 'idle'); + const networks = state.context.networks || []; + expect(networks?.length).toBe(2); + const networkId = networks?.[1].id; + await NetworkService.removeNetwork({ id: networkId as string }); + }); + it('should be able to select a new network', async () => { await expectStateMatch(service, 'idle'); diff --git a/packages/app/src/systems/Network/machines/networksMachine.ts b/packages/app/src/systems/Network/machines/networksMachine.ts index 8ad993ebd..81d5b4964 100644 --- a/packages/app/src/systems/Network/machines/networksMachine.ts +++ b/packages/app/src/systems/Network/machines/networksMachine.ts @@ -182,8 +182,11 @@ export const networksMachine = createMachine( assignNetworkId: assign({ networkId: (_, ev) => ev.input.id, }), - assignNetworks: assign({ - networks: (_, ev) => ev.data, + assignNetworks: assign((_, event) => { + store.reloadListedAssets(); + return { + networks: event.data, + }; }), assignNetwork: assign({ network: (ctx, ev) => diff --git a/packages/app/src/systems/Network/utils/url.test.tsx b/packages/app/src/systems/Network/utils/url.test.tsx index f2f22cd6f..15a300e95 100644 --- a/packages/app/src/systems/Network/utils/url.test.tsx +++ b/packages/app/src/systems/Network/utils/url.test.tsx @@ -3,6 +3,9 @@ import { isValidNetworkUrl } from './url'; describe('isValidNetworkUrl()', () => { it('should return true for valid network urls', () => { expect(isValidNetworkUrl('http://localhost:4000/graphql')).toBe(true); + expect(isValidNetworkUrl('https://devnet.fuel.network/v1/graphql')).toBe( + true + ); expect(isValidNetworkUrl('https://beta-5.fuel.network/graphql')).toBe(true); }); @@ -11,5 +14,6 @@ describe('isValidNetworkUrl()', () => { expect(isValidNetworkUrl('localhost:4000/graphql')).toBe(false); expect(isValidNetworkUrl('https://beta-4.fuel.network')).toBe(false); expect(isValidNetworkUrl('beta3-5-devv.swayswap.io/graphql')).toBe(false); + expect(isValidNetworkUrl('https://beta-5.fuel.network/')).toBe(false); }); }); diff --git a/packages/app/src/systems/Send/__mocks__/send.tsx b/packages/app/src/systems/Send/__mocks__/send.tsx index f0c1cd727..308618a26 100644 --- a/packages/app/src/systems/Send/__mocks__/send.tsx +++ b/packages/app/src/systems/Send/__mocks__/send.tsx @@ -10,10 +10,10 @@ export function sendLoader() { await NetworkService.clearNetworks(); const network = await NetworkService.addDefaultNetworks(); const transactionRequest = await getMockedTransaction( - acc1?.publicKey || '', signer.publicKey, network?.url! ); + return { acc1, network, diff --git a/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx b/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx index 4e74f0e46..d3b1a0a4b 100644 --- a/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx +++ b/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx @@ -1,49 +1,80 @@ import { cssObj } from '@fuel-ui/css'; -import { Box, Input, InputAmount, Text } from '@fuel-ui/react'; +import { Box, Form, Input, Text } from '@fuel-ui/react'; import { motion } from 'framer-motion'; -import { BaseAssetId, DECIMAL_UNITS, bn } from 'fuels'; -import { useEffect, useMemo } from 'react'; +import { type BN, DECIMAL_FUEL, bn } from 'fuels'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { AssetSelect } from '~/systems/Asset'; -import { ControlledField, Layout, animations } from '~/systems/Core'; -import { TxDetails } from '~/systems/Transaction'; +import { + ControlledField, + Layout, + MotionStack, + animations, +} from '~/systems/Core'; +import { useController, useWatch } from 'react-hook-form'; +import { InputAmount } from '~/systems/Core/components/InputAmount/InputAmount'; +import { TxFeeOptions } from '~/systems/Transaction/components/TxFeeOptions/TxFeeOptions'; import type { UseSendReturn } from '../../hooks'; const MotionContent = motion(Layout.Content); + type SendSelectProps = UseSendReturn; export function SendSelect({ form, balanceAssets, - handlers, balanceAssetSelected, - status, - fee, + baseFee = bn(0), + baseGasLimit = bn(0), + tip, + regularTip, + fastTip, + errorMessage, }: SendSelectProps) { - const assetId = form.watch('asset', ''); - // biome-ignore lint/correctness/useExhaustiveDependencies: + const [watchMax, setWatchMax] = useState(false); + const isAmountFocused = useRef(false); + const baseFeeRef = useRef(baseFee); + const tipRef = useRef(tip); + + const { field: amount, fieldState: amountFieldState } = useController({ + control: form.control, + name: 'amount', + }); + + const assetId = useWatch({ + control: form.control, + name: 'asset', + }); + const decimals = useMemo(() => { const selectedAsset = balanceAssets?.find((a) => a.assetId === assetId); - return selectedAsset?.decimals || DECIMAL_UNITS; - }, [assetId]); - const isLoadingTx = status('loadingTx'); + return selectedAsset?.decimals || DECIMAL_FUEL; + }, [assetId, balanceAssets]); - // If max balance is set on the input assume the user wants to send the max - // and change the amount to the max balance minus the fee. - // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - const amount = form.getValues('amount'); - if (assetId === BaseAssetId && balanceAssetSelected.eq(amount) && fee) { - form.setValue('amount', balanceAssetSelected.sub(fee).toString()); + if ( + watchMax && + (!baseFeeRef.current?.eq(baseFee) || !tipRef.current.eq(tip)) + ) { + baseFeeRef.current = baseFee; + tipRef.current = tip; + + // Adding 2 magical units to match the fake unit that is added on TS SDK (.add(1)) + // and then removed on the "transaction" service (.sub(1)) + const maxFee = baseFee.add(tip).add(2); + + form.setValue('amount', balanceAssetSelected.sub(maxFee), { + shouldValidate: true, + }); } - }, [fee, balanceAssetSelected]); + }, [watchMax, balanceAssetSelected, baseFee, tip, form.setValue]); return ( - Send + Asset { - form.setValue('asset', assetId || '', { - shouldValidate: true, - }); - }} + onSelect={field.onChange} /> )} /> @@ -87,30 +114,62 @@ export function SendSelect({ - + Amount - ( - { - const amountValue = value || undefined; - form.setValue('amount', amountValue?.toString() || ''); - handlers.handleValidateAmount(bn(amountValue)); - }} - /> + isInvalid={Boolean(errorMessage || amountFieldState.error)} + > + { + if (isAmountFocused.current) { + setWatchMax(false); + amount.onChange(val); + } + }} + onClickMax={() => { + baseFeeRef.current = null; // Workaround just to trigger the watcher when max is clicked and base fee is stable + setWatchMax(true); + }} + inputProps={{ + onFocus: () => { + isAmountFocused.current = true; + }, + onBlur: () => { + isAmountFocused.current = false; + }, + }} + /> + {(errorMessage || amountFieldState.error) && ( + + {errorMessage || amountFieldState.error?.message} + )} - /> + - {fee && (isLoadingTx ? : )} + + {amount.value.gt(0) && + assetId && + baseFee.gt(0) && + regularTip && + fastTip && ( + + + Fee (network) + + + + )} ); @@ -136,11 +195,8 @@ const styles = { title: cssObj({ pt: '$2', color: '$intentsBase12', - fontSize: '$xl', - fontWeight: '$normal', - }), - amountTitle: cssObj({ fontSize: '$md', + fontWeight: '$normal', }), addressRow: cssObj({ flex: 1, diff --git a/packages/app/src/systems/Send/hooks/useSend.tsx b/packages/app/src/systems/Send/hooks/useSend.tsx index 29251c688..329acea9f 100644 --- a/packages/app/src/systems/Send/hooks/useSend.tsx +++ b/packages/app/src/systems/Send/hooks/useSend.tsx @@ -1,9 +1,9 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useInterpret, useSelector } from '@xstate/react'; -import type { BigNumberish } from 'fuels'; +import type { BN, BNInput } from 'fuels'; import { bn, isBech32 } from 'fuels'; -import { useCallback, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import * as yup from 'yup'; import { useAccounts } from '~/systems/Account'; @@ -23,8 +23,20 @@ export enum SendStatus { } const selectors = { - fee(state: SendMachineState) { - return state.context.fee; + baseGasLimit(state: SendMachineState) { + return state.context.baseGasLimit; + }, + maxGasPerTx(state: SendMachineState) { + return state.context.maxGasPerTx; + }, + baseFee(state: SendMachineState) { + return state.context.baseFee; + }, + regularTip(state: SendMachineState) { + return state.context.regularTip; + }, + fastTip(state: SendMachineState) { + return state.context.fastTip; }, readyToSend(state: SendMachineState) { return state.matches('readyToSend'); @@ -37,6 +49,7 @@ const selectors = { (state: SendMachineState) => { const isLoadingTx = state.matches('creatingTx') || + state.matches('changingInput') || txStatus === TxRequestStatus.loading || txStatus === TxRequestStatus.sending; if (isLoadingTx) return SendStatus.loadingTx; @@ -45,22 +58,50 @@ const selectors = { [txStatus] ); }, - title(txStatus?: TxRequestStatus) { - return useCallback( - (state: SendMachineState) => { - if (state.matches('creatingTx') || txStatus === TxRequestStatus.loading) - return 'Creating transaction'; - return 'Send'; - }, - [txStatus] - ); - }, +}; + +type SchemaOptions = { + accountBalanceAssets: Array<{ + assetId: string; + amount?: BNInput; + }>; + baseFee: BN | undefined; + maxGasPerTx: BN | undefined; }; const schema = yup .object({ asset: yup.string().required('Asset is required'), - amount: yup.string().required('Amount is required'), + amount: yup + .mixed() + .test('positive', 'Amount must be greater than 0', (value) => { + return value?.gt(0); + }) + .test('balance', 'Insufficient funds', (value, ctx) => { + const { asset, fees } = ctx.parent as SendFormValues; + const { accountBalanceAssets, baseFee } = ctx.options + .context as SchemaOptions; + + const balanceAssetSelected = accountBalanceAssets?.find( + ({ assetId }) => assetId === asset + ); + if (!balanceAssetSelected?.amount || !value) { + return false; + } + + if (value.gt(balanceAssetSelected.amount)) { + return false; + } + + // It means "baseFee" is being calculated + if (!baseFee) { + return true; + } + + const totalAmount = value.add(baseFee.add(fees.tip)); + return totalAmount.lte(bn(balanceAssetSelected.amount)); + }) + .required('Amount is required'), address: yup .string() .required('Address is required') @@ -71,26 +112,74 @@ const schema = yup return false; } }), + fees: yup + .object({ + tip: yup + .mixed() + .test( + 'integer', + 'Tip must be greater than or equal to 0', + (value) => { + return value?.gte(0); + } + ) + .required('Tip is required'), + gasLimit: yup + .mixed() + .test( + 'integer', + 'Gas limit must be greater or equal to 0', + (value) => { + return value?.gte(0); + } + ) + .test({ + name: 'max', + test: (value, ctx) => { + const { maxGasPerTx } = ctx.options.context as SchemaOptions; + if (!maxGasPerTx) return false; + + if (value?.lte(maxGasPerTx)) { + return true; + } + + return ctx.createError({ + message: `Gas limit must be less or equal to ${maxGasPerTx.toString()}`, + }); + }, + }) + .required('Gas limit is required'), + }) + .required('Fees are required'), }) .required(); +export type SendFormValues = { + asset: string; + address: string; + amount: BN; + fees: { + tip: BN; + gasLimit: BN; + }; +}; + +const DEFAULT_VALUES: SendFormValues = { + asset: '', + amount: bn(0), + address: '', + fees: { + tip: bn(0), + gasLimit: bn(0), + }, +}; + export function useSend() { const navigate = useNavigate(); const txRequest = useTransactionRequest(); const { account, balanceAssets: accountBalanceAssets } = useAccounts(); const { assets } = useAssets(); - const form = useForm({ - resolver: yupResolver(schema), - reValidateMode: 'onChange', - mode: 'onChange', - defaultValues: { - asset: '', - amount: '', - address: '', - }, - }); - const service = useInterpret(() => sendMachine.withConfig({ actions: { @@ -102,6 +191,7 @@ export function useSend() { if (!providerUrl || !transactionRequest || !address) { throw new Error('Params are required'); } + txRequest.handlers.request({ providerUrl, transactionRequest, @@ -112,54 +202,68 @@ export function useSend() { }) ); - const amount = form.watch('amount'); + const baseFee = useSelector(service, selectors.baseFee); + const baseGasLimit = useSelector(service, selectors.baseGasLimit); + const maxGasPerTx = useSelector(service, selectors.maxGasPerTx); const errorMessage = useSelector(service, selectors.error); - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (bn(amount).gt(0) && form.formState.isValid) { - const asset = assets.find( - ({ assetId }) => assetId === form.getValues('asset') - ); - const amount = bn(form.getValues('amount')); - const address = form.getValues('address'); - const input = { - account, - asset, - amount, - address, - } as TxInputs['isValidTransaction']; - service.send('SET_DATA', { input }); - } - }, [amount, form.formState.isValid]); + const form = useForm({ + resolver: yupResolver(schema), + reValidateMode: 'onChange', + mode: 'onChange', + defaultValues: DEFAULT_VALUES, + context: { + accountBalanceAssets, + baseFee, + maxGasPerTx, + }, + }); - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (errorMessage) { - form.setError('amount', { - type: 'pattern', - message: errorMessage.split(':')[0], - }); - } - }, [errorMessage]); + const tip = useWatch({ + control: form.control, + name: 'fees.tip', + }); + + const gasLimit = useWatch({ + control: form.control, + name: 'fees.gasLimit', + }); + + const { isValid } = form.formState; - const fee = useSelector(service, selectors.fee); + const amount = useWatch({ + control: form.control, + name: 'amount', + }); + const address = useWatch({ + control: form.control, + name: 'address', + }); + const assetIdSelected = useWatch({ + control: form.control, + name: 'asset', + }); + + const regularTip = useSelector(service, selectors.regularTip); + const fastTip = useSelector(service, selectors.fastTip); const sendStatusSelector = selectors.status(txRequest.txStatus); const sendStatus = useSelector(service, sendStatusSelector); const readyToSend = useSelector(service, selectors.readyToSend); - const titleSelector = selectors.title(txRequest.txStatus); - const title = useSelector(service, titleSelector); + const balanceAssets = useMemo(() => { + return accountBalanceAssets?.filter(({ assetId }) => + assets.find((asset) => asset.assetId === assetId) + ); + }, [assets, accountBalanceAssets]); - const balanceAssets = accountBalanceAssets?.filter(({ assetId }) => - assets.find((asset) => asset.assetId === assetId) - ); + const balanceAssetSelected = useMemo(() => { + const asset = balanceAssets?.find( + ({ assetId }) => assetId === assetIdSelected + ); + if (!asset) return bn(0); - const assetIdSelected = form.getValues('asset'); - const balanceAssetSelected = - bn( - balanceAssets?.find(({ assetId }) => assetId === assetIdSelected)?.amount - ) || bn(0); + return bn(asset.amount); + }, [balanceAssets, assetIdSelected]); function status(status: keyof typeof SendStatus) { return sendStatus === status; @@ -168,50 +272,40 @@ export function useSend() { function cancel() { service.send('BACK'); } + function submit() { - const asset = assets.find( - ({ assetId }) => assetId === form.getValues('asset') - ); - const amount = bn(form.getValues('amount')); - const address = form.getValues('address'); - const input = { - account, - asset, - amount, - address, - } as TxInputs['isValidTransaction']; - service.send('CONFIRM', { input }); + service.send('CONFIRM'); } + function goHome() { navigate(Pages.index()); } + function tryAgain() { txRequest.handlers.tryAgain(); } - function handleValidateAmount(amount?: BigNumberish) { - if (bn(amount).lte(0)) { - form.setError('amount', { - type: 'pattern', - message: 'Amount is required', - }); - return; - } - if (bn(balanceAssetSelected).lt(amount!)) { - form.setError('amount', { - type: 'pattern', - message: 'Insufficient funds', - }); - return; + useEffect(() => { + if (isValid && address && assetIdSelected) { + const input: TxInputs['createTransfer'] = { + to: address, + assetId: assetIdSelected, + amount, + tip, + gasLimit, + }; + + service.send('SET_INPUT', { input }); } - form.clearErrors('amount'); - form.trigger('amount'); - } + }, [isValid, service.send, address, assetIdSelected, amount, tip, gasLimit]); return { form, - fee, - title, + baseFee, + baseGasLimit, + tip, + regularTip, + fastTip, status, readyToSend, balanceAssets, @@ -219,12 +313,12 @@ export function useSend() { txRequest, assetIdSelected, balanceAssetSelected, + errorMessage, handlers: { cancel, submit, goHome, tryAgain, - handleValidateAmount, }, }; } diff --git a/packages/app/src/systems/Send/machines/sendMachine.test.ts b/packages/app/src/systems/Send/machines/sendMachine.test.ts index 120f43905..50b148855 100644 --- a/packages/app/src/systems/Send/machines/sendMachine.test.ts +++ b/packages/app/src/systems/Send/machines/sendMachine.test.ts @@ -1,17 +1,23 @@ import { Address, bn } from 'fuels'; import { interpret } from 'xstate'; import { waitFor } from 'xstate/lib/waitFor'; -import { MOCK_ASSETS } from '~/systems/Asset/__mocks__/assets'; +import { + MOCK_ASSETS, + MOCK_BASE_ASSET_ID, +} from '~/systems/Asset/__mocks__/assets'; import type { MockVaultData } from '~/systems/Core/__tests__'; import { expectStateMatch, mockVault } from '~/systems/Core/__tests__'; +import type { TxInputs } from '~/systems/Transaction/services'; import { sendMachine } from './sendMachine'; import type { SendMachineService } from './sendMachine'; -const MOCK_INPUTS = { - address: Address.fromRandom().toString(), - asset: MOCK_ASSETS[0], +const MOCK_INPUTS: TxInputs['createTransfer'] = { + to: Address.fromRandom().toString(), amount: bn(100), + assetId: MOCK_BASE_ASSET_ID, + tip: bn(0), + gasLimit: bn(1_000_000), }; describe('sendMachine', () => { @@ -47,7 +53,7 @@ describe('sendMachine', () => { it('should create a transaction and calculate the fee', async () => { await waitFor(service, (state) => state.matches('idle')); - service.send('SET_DATA', { input: MOCK_INPUTS }); + service.send('SET_INPUT', { input: MOCK_INPUTS }); await expectStateMatch(service, 'readyToSend'); service.send('CONFIRM'); expect(callTransactionRequest).toHaveBeenCalled(); diff --git a/packages/app/src/systems/Send/machines/sendMachine.ts b/packages/app/src/systems/Send/machines/sendMachine.ts index 1dc1bda6a..48926f307 100644 --- a/packages/app/src/systems/Send/machines/sendMachine.ts +++ b/packages/app/src/systems/Send/machines/sendMachine.ts @@ -1,13 +1,11 @@ -import { type BN, Provider, type TransactionRequest } from 'fuels'; +import type { BN, TransactionRequest } from 'fuels'; import { type InterpreterFrom, type StateFrom, assign, createMachine, } from 'xstate'; -import { AccountService } from '~/systems/Account'; import { FetchMachine, assignError } from '~/systems/Core'; -import { NetworkService } from '~/systems/Network'; import { type TxInputs, TxService } from '~/systems/Transaction/services'; export enum SendScreens { @@ -21,27 +19,48 @@ export type MachineContext = { transactionRequest?: TransactionRequest; providerUrl?: string; address?: string; - fee?: BN; + baseFee?: BN; + baseGasLimit?: BN; + regularTip?: BN; + fastTip?: BN; + maxGasPerTx?: BN; + input?: TxInputs['createTransfer']; error?: string; }; +type EstimateDefaultTipsReturn = { + regularTip: BN; + fastTip: BN; +}; + +type EstimateGasLimitReturn = { + baseGasLimit: BN; + maxGasPerTx: BN; +}; + type CreateTransactionReturn = { - transactionRequest: TransactionRequest; + baseFee?: BN; + transactionRequest?: TransactionRequest; providerUrl: string; address: string; - fee: BN; }; type MachineServices = { createTransactionRequest: { data: CreateTransactionReturn; }; + estimateDefaultTips: { + data: EstimateDefaultTipsReturn; + }; + estimateGasLimit: { + data: EstimateGasLimitReturn; + }; }; type MachineEvents = | { type: 'RESET'; input: null } | { type: 'BACK'; input: null } - | { type: 'SET_DATA'; input: TxInputs['isValidTransaction'] } + | { type: 'SET_INPUT'; input: TxInputs['createTransfer'] } | { type: 'CONFIRM'; input: null }; const IDLE_STATE = { @@ -63,13 +82,47 @@ export const sendMachine = createMachine( services: {} as MachineServices, }, id: '(machine)', - initial: 'idle', + initial: 'estimatingInitialTips', states: { + estimatingInitialTips: { + invoke: { + src: 'estimateDefaultTips', + onDone: [ + { + cond: FetchMachine.hasError, + target: 'idle', + actions: [assignError()], + }, + { + actions: ['assignDefaultTips'], + target: 'estimatingGasLimit', + }, + ], + }, + }, + estimatingGasLimit: { + invoke: { + src: 'estimateGasLimit', + onDone: [ + { + cond: FetchMachine.hasError, + target: 'idle', + actions: [assignError()], + }, + { + actions: ['assignGasLimit'], + target: 'idle', + }, + ], + }, + }, idle: IDLE_STATE, creatingTx: { invoke: { src: 'createTransactionRequest', - data: (_: MachineContext, { input }: MachineEvents) => ({ input }), + data: (ctx: MachineContext) => ({ + input: ctx.input, + }), onDone: [ { cond: FetchMachine.hasError, @@ -83,6 +136,19 @@ export const sendMachine = createMachine( ], }, }, + changingInput: { + on: { + SET_INPUT: { + actions: ['assignInput'], + target: 'changingInput', + }, + }, + after: { + 1000: { + target: 'creatingTx', + }, + }, + }, readyToSend: { on: { CONFIRM: { actions: ['callTransactionRequest'] }, @@ -91,62 +157,61 @@ export const sendMachine = createMachine( }, on: { BACK: { + actions: ['goToHome'], target: 'idle', }, - SET_DATA: { target: 'creatingTx' }, + SET_INPUT: { actions: ['assignInput'], target: 'changingInput' }, }, }, { actions: { - assignTransactionData: assign((_, ev) => ({ + assignDefaultTips: assign((_ctx, ev) => ({ + regularTip: ev.data.regularTip, + fastTip: ev.data.fastTip, + })), + assignGasLimit: assign((_ctx, ev) => ({ + baseGasLimit: ev.data.baseGasLimit, + maxGasPerTx: ev.data.maxGasPerTx, + })), + assignInput: assign((_ctx, ev) => ({ + input: ev.input, + })), + assignTransactionData: assign((_ctx, ev) => ({ transactionRequest: ev.data.transactionRequest, providerUrl: ev.data.providerUrl, address: ev.data.address, - fee: ev.data.fee, + baseFee: ev.data.baseFee, })), }, services: { + estimateDefaultTips: FetchMachine.create< + never, + EstimateDefaultTipsReturn + >({ + showError: false, + maxAttempts: 1, + async fetch() { + const defaultTips = await TxService.estimateDefaultTips(); + return defaultTips; + }, + }), + estimateGasLimit: FetchMachine.create({ + showError: false, + maxAttempts: 1, + async fetch() { + const gasLimit = await TxService.estimateGasLimit(); + return gasLimit; + }, + }), createTransactionRequest: FetchMachine.create< - TxInputs['isValidTransaction'], - CreateTransactionReturn | null + TxInputs['createTransfer'], + CreateTransactionReturn >({ showError: false, maxAttempts: 1, async fetch({ input }) { - const to = input?.address; - const assetId = input?.asset?.assetId; - const { amount } = input || {}; - const [network, account] = await Promise.all([ - NetworkService.getSelectedNetwork(), - AccountService.getCurrentAccount(), - ]); - - if (!to || !assetId || !amount || !network?.url || !account) { - throw new Error('Missing params for transaction request'); - } - - const provider = await Provider.create(network.url); - const transferRequest = await TxService.createTransfer({ - to, - amount, - assetId, - provider, - }); - const { fee, transactionRequest } = - await TxService.resolveTransferCosts({ - account, - transferRequest, - amount, - assetId, - provider, - }); - - return { - fee, - transactionRequest, - address: account.address, - providerUrl: network.url, - }; + const transfer = await TxService.createTransfer(input); + return transfer; }, }), }, diff --git a/packages/app/src/systems/Send/pages/SendPage/SendPage.tsx b/packages/app/src/systems/Send/pages/SendPage/SendPage.tsx index 5e036f800..0fb166dbd 100644 --- a/packages/app/src/systems/Send/pages/SendPage/SendPage.tsx +++ b/packages/app/src/systems/Send/pages/SendPage/SendPage.tsx @@ -2,41 +2,48 @@ import { cssObj } from '@fuel-ui/css'; import { Button } from '@fuel-ui/react'; import { Layout, MotionStack, animations } from '~/systems/Core'; +import { FormProvider } from 'react-hook-form'; import { Send } from '../../components'; import { useSend } from '../../hooks'; export function SendPage() { const send = useSend(); - const { handlers, txRequest, status, form, readyToSend, ...ctx } = send; + const { handlers, txRequest, status, form, readyToSend } = send; return ( -
- - - - - - {txRequest.showActions && ( - - - - - )} - -
+ +
+ + + + + + {txRequest.shouldShowActions && ( + + + + + )} + +
+
); } diff --git a/packages/app/src/systems/Transaction/__mocks__/operation.tsx b/packages/app/src/systems/Transaction/__mocks__/operation.tsx index 79db7a986..ea7897137 100644 --- a/packages/app/src/systems/Transaction/__mocks__/operation.tsx +++ b/packages/app/src/systems/Transaction/__mocks__/operation.tsx @@ -1,8 +1,28 @@ -import type { Operation } from 'fuels'; -import { BaseAssetId, OperationName, bn } from 'fuels'; +import { + type AssetFuel, + type Operation, + OperationName, + assets, + bn, +} from 'fuels'; import { MOCK_TX_RECIPIENT } from './tx-recipient'; +// BaseAssetId replacement +const MOCK_BASE_ASSET_ID = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + +export const MOCK_FUEL_ASSETS = assets.map((asset) => { + const fuelNetworkAsset = asset.networks.find( + (n) => n.type === 'fuel' + ) as AssetFuel; + return { + ...asset, + assetId: fuelNetworkAsset.assetId, + decimals: fuelNetworkAsset.decimals, + }; +}); + export const MOCK_OPERATION_CONTRACT_CALL: Operation = { name: OperationName.contractCall, from: MOCK_TX_RECIPIENT.account, @@ -10,7 +30,7 @@ export const MOCK_OPERATION_CONTRACT_CALL: Operation = { assetsSent: [ { amount: bn.parseUnits('0.10001'), - assetId: BaseAssetId, + assetId: MOCK_BASE_ASSET_ID, }, ], calls: [ @@ -42,7 +62,7 @@ export const MOCK_OPERATION_TRANSFER: Operation = { assetsSent: [ { amount: bn.parseUnits('0.52'), - assetId: BaseAssetId, + assetId: MOCK_BASE_ASSET_ID, }, ], }; diff --git a/packages/app/src/systems/Transaction/__mocks__/transaction.tsx b/packages/app/src/systems/Transaction/__mocks__/transaction.tsx index e5478ab0a..59ccbd38f 100644 --- a/packages/app/src/systems/Transaction/__mocks__/transaction.tsx +++ b/packages/app/src/systems/Transaction/__mocks__/transaction.tsx @@ -18,323 +18,107 @@ export const MOCK_TRANSACTION_CREATE = { export const MOCK_TRANSACTION_WITH_RECEIPTS_GQL = { // this response is got from a simple "Send" transaction created from the wallet UI. transaction: { - id: '0x1e1b1960c0f15534eb9b138101567e0dcbc31e394df106edd7abab1a7875614c', - __typename: 'Transaction', - gasPrice: '1', - maturity: '0', - txPointer: null, - isScript: true, - isCreate: false, - isMint: false, - witnesses: [ - '0x803f4fb0c4f575a8e15fa01b6e3ba9d116d3ec5ae1249d7fe7f1a6b1d77d22f3740d676d4d1840ea40df8b99d94b1ed8348f72df7b3e76ad6d8e8ca8bbe18236', - ], - receiptsRoot: - '0xe7f678a2e8df7da272cf303aff96023da2ab1968b74d86bb92f5b558d38ed6bd', - script: '0x24000000', - scriptData: '0x', - bytecodeWitnessIndex: null, - bytecodeLength: null, - salt: null, - storageSlots: null, + id: '0xaeb72ff8937553828b5cd9f05efa5a6e1bb21f628cd90f4c91101799d27d5ddb', rawPayload: - '0x00000000000000000000000000e4e1c0000000000000000400000000000000000000000000000001000000000000000100000000000000020000000000000001e7f678a2e8df7da272cf303aff96023da2ab1968b74d86bb92f5b558d38ed6bd2400000000000000000000000000000100000000000000007a8353e0c12f3893e99b8c50f9eaebb1278c85dad65adbbf8eb92b2b96207c3200000000000000013e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302000000000097cdec0000000000000000000000000000000000000000000000000000000000000000000000000019a27d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeb709945b9058c3d50f3922bd1b49f92ced2950a9ecaf810aa7829295550cd20000000000002710000000000000000000000000000000000000000000000000000000000000000000000000000000023e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302000000000097a36500000000000000000000000000000000000000000000000000000000000000000000000000000040803f4fb0c4f575a8e15fa01b6e3ba9d116d3ec5ae1249d7fe7f1a6b1d77d22f3740d676d4d1840ea40df8b99d94b1ed8348f72df7b3e76ad6d8e8ca8bbe18236', + '0x000000000000000000000000000002ddc27a4ce0f152478cf32027140179d8a65aa85bae900a0054c5afdeb69503c4f000000000000000040000000000000000000000000000000800000000000000010000000000000002000000000000000124000000000000000000000000000371000000000000000054d0cebcdcff05b42e7ecb8a702922e383a7654004a660730ff2aceb9ff580b00000000000000000bbe95b34554ff07244f32234f95e56d7695c4e987e150eb1878cf9e70648aedd00000000186fffaa0000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000613f61758566fbd145f3f834156f2c4249038565fdc2cfccec191adcb77b6fa700000000000186a000000000000000000000000000000000000000000000000000000000000000000000000000000002bbe95b34554ff07244f32234f95e56d7695c4e987e150eb1878cf9e70648aedd00000000186e759900000000000000000000000000000000000000000000000000000000000000000000000000000040334daf700179a760105112805dc5c0ac909cc63f8afdc5aaff2cf7bd84c8f81232ba3023501c9eb67645b64cf5bee19873e26fec63e6e8e507a80330ff420bef', status: { - __typename: 'SuccessStatus', - time: '4611686020132737762', + type: 'SuccessStatus', block: { - id: '0x0d477f50818d6597488309996758e922bc2e553832556e8de7d4747dc1d683ae', - header: { - id: '0x0d477f50818d6597488309996758e922bc2e553832556e8de7d4747dc1d683ae', - height: '2022716', - daHeight: '5092255', - applicationHash: - '0x047a9ba464f8f0e5c4392d3136f134a97ac5342c527d27444c9c51d399e4af36', - messageReceiptRoot: - '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - messageReceiptCount: '0', - time: '4611686020132737762', - }, + id: '0x4ed161f29c72b986a534cba8d48586d2d69f3ae7289bb5a14792173570baa1ba', }, + time: '4611686020142520776', programState: { + returnType: 'RETURN', data: '0x0000000000000000', }, + receipts: [ + { + id: null, + pc: '10368', + is: '10368', + to: null, + toAddress: null, + amount: null, + assetId: null, + gas: null, + param1: null, + param2: null, + val: '0', + ptr: null, + digest: null, + reason: null, + ra: null, + rb: null, + rc: null, + rd: null, + len: null, + receiptType: 'RETURN', + result: null, + gasUsed: null, + data: null, + sender: null, + recipient: null, + nonce: null, + contractId: null, + subId: null, + }, + { + id: null, + pc: null, + is: null, + to: null, + toAddress: null, + amount: null, + assetId: null, + gas: null, + param1: null, + param2: null, + val: null, + ptr: null, + digest: null, + reason: null, + ra: null, + rb: null, + rc: null, + rd: null, + len: null, + receiptType: 'SCRIPT_RESULT', + result: '0', + gasUsed: '733', + data: null, + sender: null, + recipient: null, + nonce: null, + contractId: null, + subId: null, + }, + ], + totalGas: '81050', + totalFee: '881', }, - inputAssetIds: [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - inputContracts: [], - inputs: [ - { - __typename: 'InputCoin', - amount: '9948652', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - owner: - '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - predicate: '0x', - predicateData: '0x', - txPointer: '0019a27d0000', - utxoId: - '0x7a8353e0c12f3893e99b8c50f9eaebb1278c85dad65adbbf8eb92b2b96207c3201', - witnessIndex: 0, - }, - ], - outputs: [ - { - __typename: 'CoinOutput', - to: '0xeeb709945b9058c3d50f3922bd1b49f92ced2950a9ecaf810aa7829295550cd2', - amount: '10000', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - { - __typename: 'ChangeOutput', - to: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - amount: '9937765', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - ], - receipts: [ - { - __typename: 'Receipt', - contract: null, - to: null, - pc: '10336', - is: '10336', - toAddress: null, - amount: null, - assetId: null, - gas: null, - param1: null, - param2: null, - val: '0', - ptr: null, - digest: null, - reason: null, - ra: null, - rb: null, - rc: null, - rd: null, - len: null, - receiptType: 'RETURN', - result: null, - gasUsed: null, - data: null, - sender: null, - recipient: null, - nonce: null, - contractId: null, - subId: null, - }, - { - __typename: 'Receipt', - contract: null, - to: null, - pc: null, - is: null, - toAddress: null, - amount: null, - assetId: null, - gas: null, - param1: null, - param2: null, - val: null, - ptr: null, - digest: null, - reason: null, - ra: null, - rb: null, - rc: null, - rd: null, - len: null, - receiptType: 'SCRIPT_RESULT', - result: '0', - gasUsed: '733', - data: null, - sender: null, - recipient: null, - nonce: null, - contractId: null, - subId: null, - }, - ], - time: { - fromNow: '9 days ago', - full: '15 Jan 2024 - 20:17:01 PM', - rawTai64: '4611686020132737762', - rawUnix: '1705349821', - }, - groupedInputs: [ - { - type: 'InputCoin', - totalAmount: '0x97cdec', - inputs: [ - { - __typename: 'InputCoin', - amount: '9948652', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - owner: - '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - predicate: '0x', - predicateData: '0x', - txPointer: '0019a27d0000', - utxoId: - '0x7a8353e0c12f3893e99b8c50f9eaebb1278c85dad65adbbf8eb92b2b96207c3201', - witnessIndex: 0, - }, - ], - contractId: null, - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - sender: null, - recipient: null, - data: null, - owner: - '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - }, - ], - groupedOutputs: [ - { - to: '0xeeb709945b9058c3d50f3922bd1b49f92ced2950a9ecaf810aa7829295550cd2', - type: 'CoinOutput', - totalAmount: '0x2710', - outputs: [ - { - __typename: 'CoinOutput', - to: '0xeeb709945b9058c3d50f3922bd1b49f92ced2950a9ecaf810aa7829295550cd2', - amount: '10000', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - ], - contract: null, - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - inputIndex: null, - recipient: null, - }, - { - to: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - type: 'ChangeOutput', - totalAmount: '0x97a365', - outputs: [ - { - __typename: 'ChangeOutput', - to: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - amount: '9937765', - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - ], - contract: null, - assetId: - '0x0000000000000000000000000000000000000000000000000000000000000000', - inputIndex: null, - recipient: null, - }, - ], - accountsInvolved: [ - { - id: '0x3e7ddda4d0d3f8307ae5f1aed87623992c1c4decefec684936960775181b2302', - type: 'Predicate', - }, - ], - isPredicate: false, - blockHeight: '2022716', - statusType: 'Success', - totalAccounts: 1, - totalAssets: 1, - totalOperations: 1, - gasUsed: '733', - title: 'Script', - fee: '0x377', - operations: [ - { - __typename: 'Operation', - type: 'FINAL_RESULT', - receipts: [ - { - item: { - __typename: 'Receipt', - contract: null, - to: null, - pc: '10336', - is: '10336', - toAddress: null, - amount: null, - assetId: null, - gas: null, - param1: null, - param2: null, - val: '0', - ptr: null, - digest: null, - reason: null, - ra: null, - rb: null, - rc: null, - rd: null, - len: null, - receiptType: 'RETURN', - result: null, - gasUsed: null, - data: null, - sender: null, - recipient: null, - nonce: null, - contractId: null, - subId: null, - }, - receipts: null, - }, - { - item: { - __typename: 'Receipt', - contract: null, - to: null, - pc: null, - is: null, - toAddress: null, - amount: null, - assetId: null, - gas: null, - param1: null, - param2: null, - val: null, - ptr: null, - digest: null, - reason: null, - ra: null, - rb: null, - rc: null, - rd: null, - len: null, - receiptType: 'SCRIPT_RESULT', - result: '0', - gasUsed: '733', - data: null, - sender: null, - recipient: null, - nonce: null, - contractId: null, - subId: null, - }, - receipts: null, - }, - ], - }, - ], }, chain: MOCK_CHAIN_INFO.chain, nodeInfo: { utxoValidation: true, - vmBacktrace: false, - minGasPrice: '1', + vmBacktrace: true, maxTx: '4064', maxDepth: '10', - nodeVersion: '0.22.0', + nodeVersion: '0.26.0', + }, + latestGasPrice: { + gasPrice: '1', + }, + balances: { + edges: [ + { + node: { + owner: + '0xbbe95b34554ff07244f32234f95e56d7695c4e987e150eb1878cf9e70648aedd', + amount: '499985267', + assetId: + '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + }, + ], }, }; diff --git a/packages/app/src/systems/Transaction/__mocks__/tx-request.tsx b/packages/app/src/systems/Transaction/__mocks__/tx-request.tsx index 58d75af5d..077f7d0c9 100644 --- a/packages/app/src/systems/Transaction/__mocks__/tx-request.tsx +++ b/packages/app/src/systems/Transaction/__mocks__/tx-request.tsx @@ -15,7 +15,6 @@ export const MOCK_TX_REQUEST = { amount: '0x989680', owner: '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e', - maturity: 0, txPointer: '0x00000000000000000000000000000000', witnessIndex: 0, predicate: '0x', diff --git a/packages/app/src/systems/Transaction/__mocks__/tx.ts b/packages/app/src/systems/Transaction/__mocks__/tx.ts index 6ef930c9b..9c3ec500a 100644 --- a/packages/app/src/systems/Transaction/__mocks__/tx.ts +++ b/packages/app/src/systems/Transaction/__mocks__/tx.ts @@ -89,10 +89,9 @@ export const MOCK_TRANSACTION_CONTRACT_CALL_PARTS: { '0x0000000000000000000000000000000000000000000000000000000000000000', txPointer: { blockHeight: 0, txIndex: 0 }, witnessIndex: 0, - maturity: 0, predicateGasUsed: bn(0), - predicateLength: 0, - predicateDataLength: 0, + predicateLength: bn(0), + predicateDataLength: bn(0), predicate: '0x', predicateData: '0x', }, @@ -190,11 +189,12 @@ export const MOCK_TRANSACTION_CONTRACT_CALL_PARTS: { const thirtyFourDaysAgo = dateToTai64( new Date(Date.now() - 1000 * 60 * 60 * 24 * 34) ); + export const MOCK_TRANSACTION_CONTRACT_CALL: MockTransaction = { transaction: { policies: [ { - type: PolicyType.GasPrice, + type: PolicyType.Tip, data: bn(1), }, ], @@ -216,8 +216,8 @@ export const MOCK_TRANSACTION_CONTRACT_CALL: MockTransaction = { '0x90000004470000000000000000000cd45dfcc00110fff3001a5c5000910005b861440006724002d0164114005b40100d360000006158000c61440001504175305f5d10a6504175305d4570a6504171385f5d1027504171385d417027134100007340001a9000001f1a445000910000085d43f0005f4500009000002b504171385d4170271341004073400024900000291a445000910000085d43f0015f4500009000002b360000001a44000050417528504175286041100850457528504170085041700860411008504170085d4100001341000073400037900000396144000c9000003b360000001a440000504174305f5d1086504174305d4570865d43f00210450440504174485f5d108961440001504175405f5d10a8504175405d4570a8504171405f5d1028504171405d417028134100007340004f900000541a445000910000085d43f0005f45000090000060504171405d41702813410040734000599000005e1a445000910000085d43f0015f45000090000060360000001a44000050417538504175386041100850457538504170005041700060411008504170005d410000134100007340006c9000006e6144000690000078504170005d410000134100407340007390000076360000001a44000090000078360000001a4400005d43f00220451400504173805f5d1070504174485d497089504173805d4170701a445000910000105f4520005f450001504175a8504175a8604110105d47f00326440000504470015041726050417260604110a026000000504070011a445000910000105f4500005f440001504174785041747860411010504173505f5c006a5d47f0025d43f00412451400504173005f5d1060504173505d45706a504173005d41706016411400734000a4900000b150496000504173505d41706a5545009010452440504170785041707860411090504170785d41000013410040734001249000031f504972601a445000910000a050411000604120a05041748850417488604110a026000000504070011a445000910000105f4500005f44000150417198504171986041101050517198505574885d454001504174085f5d10815d4540015d43f00310450440504173c85f5d10795d4140005d4d4001504573c85d457079154914c0734800d3900000e12644000050487001504573a85f5d207515453000734400da900000de504573a85d457075284504c0900000de504173a85d417075900000e15f510000504173c85d4170795f5100015d454000504174085d417081104504405d43f0032845540050557198505174785d41400113410000734000f1900000f35d4150019000011c5d455001504174105f5d10825d4550015d41400110450440504173d05f5d107a5d4150005d4d5001504573d05d45707a154914c073480102900001102644000050487001504573b05f5d207615453000734401099000010d504573b05d457076284504c09000010d504173b05d417076900001105f550000504173d05d41707a5f5500015d4940005d455000504174105d417082104504405d414001284524005d417082504171985d450000504171985d41000125450000504574885d43f003254500005041707850450008504171a8504171a860411088504171a850450028504171085041710860411018504171085d41000013410000734001339000013f504171085d450002504175485f5d10a9504175485d4970a91a445000910000185d43f0005f4500005f4520029000017b504171085d41000013410040734001449000016050417108504100085d450000504173e85f5d107d50417108504100085d450001504173785f5d106f504175a85d450000504173e85d41707d10450440504173785d41706f1a485000910000105f4910005f4900011a445000910000185d43f0015f45000050411008604120109000017b50417108504100085d450000504173f05f5d107e50417108504100085d450001504173905f5d1072504175a85d450000504173f05d41707e10450440504173905d4170721a485000910000105f4910005f4900011a445000910000185d43f0015f4500005041100860412010504173085041730860411018504171a850550000504171a85d51000450457308504171a8504d0040504170105041701060411018504170105d410000134100007340018d90000194504170105d450002504175505f5d10aa504175505d4570aa900001a8504170105d4100001341004073400199900001a150417010504100085d450000504174385f5d1087504174385d457087900001a850417010504100085d450000504174505f5d108a504174505d45708a504173205f5d1064504173205d4970641a4450009100003050411000604150205f4540045f45200550417230504172306041103050453000504170285041702860411010504170285d41000013410040734001be900001c5504170285d450001504171485f5d1029504171485d457029900001cd504170285d41000013410000734001ca900001cc1a440000900001cd1a440000504171505f5d102a50453010504170385041703860411028504170385d41000013410040734001d8900001df504170385045000850417358504173586041102050497358900001f1504170385d41000013410000734001e4900001eb1a485000910000205d47f00a104513005041200060411020900001f11a485000910000205d47f00a10451300504120006041102050417158504171586041202050453038504170605041706060411010504170605d41000013410040734001fd90000204504170605d450001504173405f5d1068504173405d4570689000020c504170605d41000013410000734002099000020b1a44a0009000020c1a44a000504173485f5d1069504d7230504171505d49702a50457158504173485d4170692d4d24501a44e000504170705f5d100e504170705d41700e134100007340021d900002281a44d000504175785f5d10af504175785d4570af1a485000910000185d43f0005f4900005f4910029000023d504170705d45700e504173885f5d10711a44d000504174585f5d108b504174585d49708b504173885d4170711a445000910000105f4520005f4500011a485000910000185d43f0015f490000504120086041101050417460504174606041201850457460504171205041712060411018504171205d410000134100007340024990000255504171205d450002504175a05f5d10b4504175a05d4970b41a445000910000185d43f0005f4500005f45200290000309504171205d410000134100407340025a900002b250417120504100085d450000504174285f5d108550417120504100085d450001504173985f5d1073504174285d497085504173985d4170731a445000910000105f4520005f45000150417178504171786041101050557478505171785d4140011341000073400275900002775d455001900002a15d455001504174185f5d10835d4550015d41400110450440504173d85f5d107b5d4150005d4d5001504573d85d45707b154914c073480286900002942644000050487001504573b85f5d2077154530007344028d90000291504573b85d457077284504c090000291504173b85d417077900002945f550000504173d85d41707b5f5500015d4940005d455000504174185d417083104504405d41400128452400504174185d457083504173f85f5d107f504173f85d45707f504173985d4170731a485000910000105f4910005f4900011a445000910000185d43f0015f45000050411008604120109000030950417120504100085d450000504174405f5d108850417120504100085d450001504173a05f5d1074504174405d497088504173a05d4170741a445000910000105f4520005f45000150417188504171886041101050557478505171885d41400113410000734002cd900002cf5d455001900002f95d455001504174205f5d10845d4550015d41400110450440504173e05f5d107c5d4150005d4d5001504573e05d45707c154914c0734802de900002ec2644000050487001504573c05f5d207815453000734402e5900002e9504573c05d457078284504c0900002e9504173c05d417078900002ec5f550000504173e05d41707c5f5500015d4940005d455000504174205d417084104504405d41400128452400504174205d457084504174005f5d1080504174005d457080504173a05d4170741a485000910000105f4910005f4900011a445000910000185d43f0015f4500005041100860412010504173285041732860411018504973281a445000910000205d43f0015f450000504110086041201850417558504175586041102050457260504173505d41706a5549002010491480504575585d43f009284914009000032e504175801a445000910000205d43f0005f450000504175806041102050457260504173505d41706a5549002010491480504575805d43f0092849140050417350504173505d41706a104014005f5d006a9000009d470000000000000000000000000000000000000100000000000002d000000000000000a00000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000cfc', scriptData: '0x00000000000000010a98320d39c03337401a4e46263972a9af6ce69ec2f35a5420b1bd35784c74b1000000002b85955300000000000000000000000000000000000000000000000300000000000000010000000005f5e1000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - scriptDataLength: 720, - scriptLength: 3372, + scriptDataLength: bn(720), + scriptLength: bn(3372), type: TransactionType.Script, }, id: '0x18617ccc580478214175c4daba11903df93a66a94aada773e80411ed06b6ade7', @@ -245,12 +245,15 @@ export const MOCK_TRANSACTION_CONTRACT_CALL: MockTransaction = { }, ], gasUsed: bn('0x28f90'), + tip: bn(0), fee: bn('0x1'), type: TransactionTypeName.Script, status: TransactionStatus.success, isTypeMint: false, isTypeCreate: false, isTypeScript: true, + isTypeUpgrade: false, + isTypeUpload: false, isStatusPending: false, isStatusSuccess: true, isStatusFailure: false, @@ -301,9 +304,8 @@ export const MOCK_TRANSACTION_CREATE_CONTRACT_PARTS: { '0x0000000000000000000000000000000000000000000000000000000000000000', txPointer: { blockHeight: 0, txIndex: 0 }, witnessIndex: 1, - maturity: 0, - predicateLength: 0, - predicateDataLength: 0, + predicateLength: bn(0), + predicateDataLength: bn(0), predicate: '0x', predicateData: '0x', predicateGasUsed: bn(0), @@ -329,14 +331,13 @@ export const MOCK_TRANSACTION_CREATE_CONTRACT: MockTransaction = { type: TransactionType.Create, policies: [ { - type: PolicyType.GasPrice, + type: PolicyType.Tip, data: bn(1), }, ], policyTypes: 1, - bytecodeLength: 65, bytecodeWitnessIndex: 0, - storageSlotsCount: 1, + storageSlotsCount: bn(1), inputsCount: 2, outputsCount: 2, witnessesCount: 2, @@ -384,9 +385,12 @@ export const MOCK_TRANSACTION_CREATE_CONTRACT: MockTransaction = { }, ], gasUsed: bn(1), + tip: bn(0), fee: bn(1), isTypeCreate: true, isTypeScript: false, + isTypeUpgrade: false, + isTypeUpload: false, isTypeMint: false, isStatusFailure: false, isStatusSuccess: true, @@ -437,9 +441,12 @@ export const MOCK_TRANSACTION_MINT: MockTransaction = { }, ], gasUsed: bn(0), + tip: bn(0), fee: bn(0), isTypeCreate: false, isTypeScript: false, + isTypeUpgrade: false, + isTypeUpload: false, isTypeMint: true, isStatusFailure: false, isStatusSuccess: true, @@ -469,9 +476,8 @@ export const MOCK_TRANSACTION_TRANSFER_PARTS: { '0x0000000000000000000000000000000000000000000000000000000000000000', txPointer: { blockHeight: 0, txIndex: 0 }, witnessIndex: 0, - maturity: 0, - predicateLength: 0, - predicateDataLength: 0, + predicateLength: bn(0), + predicateDataLength: bn(0), predicate: '0x', predicateData: '0x', predicateGasUsed: bn(0), @@ -483,8 +489,8 @@ export const MOCK_TRANSACTION_TRANSFER_PARTS: { nonce: bn(2).toString(), predicate: '0x', predicateData: '0x', - predicateDataLength: 0, - predicateLength: 0, + predicateDataLength: bn(0), + predicateLength: bn(0), predicateGasUsed: bn(0), recipient: '0x06300e686a5511c7ba0399fc68dcbe0ca2d8f54f7e6afea73c505dd3bcacf33b', @@ -526,13 +532,13 @@ export const MOCK_TRANSACTION_TRANSFER: MockTransaction = { type: 0, policies: [ { - type: PolicyType.GasPrice, + type: PolicyType.Tip, data: bn(1), }, ], policyTypes: 1, - scriptLength: 4, - scriptDataLength: 0, + scriptLength: bn(4), + scriptDataLength: bn(0), inputsCount: 2, outputsCount: 2, witnessesCount: 1, @@ -578,9 +584,12 @@ export const MOCK_TRANSACTION_TRANSFER: MockTransaction = { }, ], gasUsed: bn(1335), + tip: bn(0), fee: bn(1), isTypeCreate: false, isTypeScript: true, + isTypeUpgrade: false, + isTypeUpload: false, isTypeMint: false, isStatusFailure: false, isStatusSuccess: true, @@ -634,9 +643,8 @@ export const MOCK_TRANSACTION_WITHDRAW_FROM_FUEL_PARTS: { '0x0000000000000000000000000000000000000000000000000000000000000000', txPointer: { blockHeight: 0, txIndex: 0 }, witnessIndex: 0, - maturity: 0, - predicateLength: 0, - predicateDataLength: 0, + predicateLength: bn(0), + predicateDataLength: bn(0), predicate: '0x', predicateData: '0x', predicateGasUsed: bn(0), @@ -681,7 +689,7 @@ export const MOCK_TRANSACTION_WITHDRAW_FROM_FUEL: MockTransaction = { type: 0, policies: [ { - type: PolicyType.GasPrice, + type: PolicyType.Tip, data: bn(1), }, ], @@ -692,8 +700,8 @@ export const MOCK_TRANSACTION_WITHDRAW_FROM_FUEL: MockTransaction = { inputsCount: 1, outputsCount: 2, witnessesCount: 1, - scriptLength: 56, - scriptDataLength: 0, + scriptLength: bn(56), + scriptDataLength: bn(0), receiptsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', script: @@ -724,9 +732,12 @@ export const MOCK_TRANSACTION_WITHDRAW_FROM_FUEL: MockTransaction = { }, ], gasUsed: bn('0x93'), + tip: bn(0), fee: bn('0x0'), isTypeCreate: false, isTypeScript: true, + isTypeUpgrade: false, + isTypeUpload: false, isTypeMint: false, isStatusPending: false, isStatusSuccess: true, diff --git a/packages/app/src/systems/Transaction/components/TxContent/TxContent.stories.tsx b/packages/app/src/systems/Transaction/components/TxContent/TxContent.stories.tsx index 4d141eb01..497335112 100644 --- a/packages/app/src/systems/Transaction/components/TxContent/TxContent.stories.tsx +++ b/packages/app/src/systems/Transaction/components/TxContent/TxContent.stories.tsx @@ -1,7 +1,6 @@ import type { Meta } from '@storybook/react'; import { MOCK_TRANSACTION_MINT } from '../../__mocks__/tx'; -import { TxHeader } from '../TxHeader'; import { TxContent } from '.'; @@ -14,25 +13,12 @@ export default { }, } as Meta; -const PROVIDER_URL = 'http://localhost:4000'; const TX = MOCK_TRANSACTION_MINT; export const Info = () => { - return ( - - } - /> - ); + return ; }; export const Loading = () => { - return } />; + return ; }; diff --git a/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx b/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx index 1eea9e1d6..0595103a4 100644 --- a/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx +++ b/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx @@ -1,56 +1,134 @@ -import { Box } from '@fuel-ui/react'; +import { cssObj } from '@fuel-ui/css'; +import { Alert, Box, CardList, ContentLoader } from '@fuel-ui/react'; import type { AssetData } from '@fuel-wallet/types'; import type { TransactionStatus, TransactionSummary } from 'fuels'; -import type { ReactNode } from 'react'; +import { type ReactNode, useMemo } from 'react'; import type { Maybe } from '~/systems/Core'; import { MotionStack, animations } from '~/systems/Core'; -import { TxDetails, TxOperations } from '~/systems/Transaction'; +import { + type GroupedErrors, + TxFee, + TxHeader, + TxOperations, +} from '~/systems/Transaction'; -type TxContentLoaderProps = { - header?: ReactNode; +const ErrorHeader = ({ errors }: { errors?: GroupedErrors }) => { + const errorMessages = useMemo(() => { + const messages = []; + if (errors) { + if (errors.InsufficientInputAmount) messages.push('Not enough funds'); + + // biome-ignore lint: will not be a large array + Object.keys(errors).forEach((key: string) => { + if (key === 'InsufficientInputAmount') return; + + let errorMessage = `${key}: `; + try { + errorMessage += JSON.stringify(errors[key]); + } catch (_) { + errorMessage += errors[key]; + } + messages.push(errorMessage); + }); + } + + return messages; + }, [errors]); + + return ( + + + {errorMessages.map((message) => ( + {message} + ))} + + + ); }; -function TxContentLoader({ header }: TxContentLoaderProps) { +const ConfirmHeader = () => ( + + + Carefully check if all the details in your transaction are correct + + +); + +const LoaderHeader = () => ( + + + + + +); + +function TxContentLoader() { return ( - {header} + - + ); } type TxContentInfoProps = { - header?: ReactNode; footer?: ReactNode; tx?: Maybe; txStatus?: Maybe; showDetails?: boolean; assets?: Maybe; isLoading?: boolean; + isConfirm?: boolean; + errors?: GroupedErrors; + providerUrl?: string; }; function TxContentInfo({ tx, txStatus, - header, footer, showDetails, assets, isLoading, + isConfirm, + errors, + providerUrl, }: TxContentInfoProps) { - const status = tx?.status || txStatus; + const status = txStatus || tx?.status || txStatus; + const hasErrors = Boolean(Object.keys(errors || {}).length); + const isExecuted = !!tx?.id; + + function getHeader() { + if (hasErrors) return ; + if (isExecuted) + return ( + + ); + if (isConfirm) return ; + + return ; + } + return ( - {header} + {getHeader()} - {isLoading && !showDetails && } - {showDetails && } + {isLoading && !showDetails && } + {showDetails && } {footer} ); @@ -60,3 +138,17 @@ export const TxContent = { Loader: TxContentLoader, Info: TxContentInfo, }; + +const styles = { + alert: cssObj({ + '& .fuel_Alert-content': { + gap: '$1', + }, + ' & .fuel_Heading': { + fontSize: '$sm', + }, + '& .fuel_Icon': { + mt: '-2px', + }, + }), +}; diff --git a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.stories.tsx b/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.stories.tsx deleted file mode 100644 index 89c3d9b67..000000000 --- a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Box } from '@fuel-ui/react'; -import { bn } from 'fuels'; - -import type { TxDetailsProps } from './TxDetails'; -import { TxDetails } from './TxDetails'; - -export default { - component: TxDetails, - title: 'Transaction/Components/TxDetails', -}; - -export const Usage = (args: TxDetailsProps) => ( - - - -); - -export const Loader = () => ( - - - -); diff --git a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.tsx b/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.tsx deleted file mode 100644 index f4edade0f..000000000 --- a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Card, Text } from '@fuel-ui/react'; -import type { BN } from 'fuels'; -import { bn } from 'fuels'; -import type { FC } from 'react'; - -import { TxDetailsLoader } from './TxDetailsLoader'; -import { styles } from './styles'; - -export type TxDetailsProps = { - fee?: BN; -}; - -type TxDetailsComponent = FC & { - Loader: typeof TxDetailsLoader; -}; - -export const TxDetails: TxDetailsComponent = ({ - fee: initialFee, -}: TxDetailsProps) => { - const fee = bn(initialFee); - - return ( - - - Fee (network) - - - {fee?.format()} ETH - - - ); -}; - -TxDetails.Loader = TxDetailsLoader; diff --git a/packages/app/src/systems/Transaction/components/TxDetails/index.tsx b/packages/app/src/systems/Transaction/components/TxDetails/index.tsx deleted file mode 100644 index 870f71ea5..000000000 --- a/packages/app/src/systems/Transaction/components/TxDetails/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './TxDetails'; diff --git a/packages/app/src/systems/Transaction/components/TxDetails/styles.tsx b/packages/app/src/systems/Transaction/components/TxDetails/styles.tsx deleted file mode 100644 index a4dd82351..000000000 --- a/packages/app/src/systems/Transaction/components/TxDetails/styles.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cssObj } from '@fuel-ui/css'; - -export const styles = { - detailItem: cssObj({ - padding: '$3 $4', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - display: 'flex', - }), - text: cssObj({ - fontSize: '$sm', - fontWeight: '$normal', - }), -}; diff --git a/packages/app/src/systems/Transaction/components/TxFee/TxFee.stories.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFee.stories.tsx new file mode 100644 index 000000000..c1afa158c --- /dev/null +++ b/packages/app/src/systems/Transaction/components/TxFee/TxFee.stories.tsx @@ -0,0 +1,24 @@ +import { Box } from '@fuel-ui/react'; +import { bn } from 'fuels'; + +import type { TxFeeProps } from './TxFee'; +import { TxFee } from './TxFee'; + +export default { + component: TxFee, + title: 'Transaction/Components/TxFee', +}; + +export const Usage = (args: TxFeeProps) => ( + + + +); + +export const Loader = () => ( + + + +); diff --git a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.test.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx similarity index 74% rename from packages/app/src/systems/Transaction/components/TxDetails/TxDetails.test.tsx rename to packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx index 9c4d52d2c..5766a5fc0 100644 --- a/packages/app/src/systems/Transaction/components/TxDetails/TxDetails.test.tsx +++ b/packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx @@ -1,16 +1,16 @@ import { render, screen, testA11y } from '@fuel-ui/test-utils'; import { bn } from 'fuels'; -import { TxDetails } from './TxDetails'; +import { TxFee } from './TxFee'; -describe('TxDetails', () => { +describe('TxFee', () => { it('a11y', async () => { - await testA11y(); + await testA11y(); }); it('should be able to show the transaction fee', async () => { const feeCost = bn(6); - render(); + render(); expect(await screen.findByText(/fee \(network\)/i)).toBeInTheDocument(); const valFee = screen.getByLabelText(/Fee value/i); expect(valFee).toBeInTheDocument(); diff --git a/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx new file mode 100644 index 000000000..dbb1250d8 --- /dev/null +++ b/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx @@ -0,0 +1,40 @@ +import { Card, Text } from '@fuel-ui/react'; +import type { BN } from 'fuels'; +import type { FC } from 'react'; + +import { TxFeeLoader } from './TxFeeLoader'; +import { styles } from './styles'; + +export type TxFeeProps = { + fee?: BN; + checked?: boolean; + onChecked?: (checked: boolean) => void; + title?: string; +}; + +type TxFeeComponent = FC & { + Loader: typeof TxFeeLoader; +}; + +export const TxFee: TxFeeComponent = ({ + fee, + checked, + onChecked, + title, +}: TxFeeProps) => { + return ( + onChecked?.(true)} + > + + {title || 'Fee (network)'} + + + {fee ? `${fee.format()} ETH` : '--'} + + + ); +}; + +TxFee.Loader = TxFeeLoader; diff --git a/packages/app/src/systems/Transaction/components/TxDetails/TxDetailsLoader.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFeeLoader.tsx similarity index 71% rename from packages/app/src/systems/Transaction/components/TxDetails/TxDetailsLoader.tsx rename to packages/app/src/systems/Transaction/components/TxFee/TxFeeLoader.tsx index cbe065976..553260d53 100644 --- a/packages/app/src/systems/Transaction/components/TxDetails/TxDetailsLoader.tsx +++ b/packages/app/src/systems/Transaction/components/TxFee/TxFeeLoader.tsx @@ -3,9 +3,9 @@ import { Card, ContentLoader, Text } from '@fuel-ui/react'; import { styles } from './styles'; -export const TxDetailsLoader = (props: ContentLoaderProps) => ( - - +export const TxFeeLoader = (props: ContentLoaderProps) => ( + + Fee (network) diff --git a/packages/app/src/systems/Transaction/components/TxFee/index.tsx b/packages/app/src/systems/Transaction/components/TxFee/index.tsx new file mode 100644 index 000000000..221083d4c --- /dev/null +++ b/packages/app/src/systems/Transaction/components/TxFee/index.tsx @@ -0,0 +1 @@ +export * from './TxFee'; diff --git a/packages/app/src/systems/Transaction/components/TxFee/styles.tsx b/packages/app/src/systems/Transaction/components/TxFee/styles.tsx new file mode 100644 index 000000000..a157ea0c0 --- /dev/null +++ b/packages/app/src/systems/Transaction/components/TxFee/styles.tsx @@ -0,0 +1,32 @@ +import { cssObj } from '@fuel-ui/css'; + +export const styles = { + detailItem: (active = false, pointer = false) => + cssObj({ + padding: '$3 $4', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + display: 'flex', + position: 'relative', + cursor: pointer ? 'pointer' : 'auto', + + ...(active && { + '&::after': { + position: 'absolute', + display: 'block', + content: '""', + top: 0, + left: 0, + width: '3px', + height: '100%', + background: '$intentsPrimary11', + borderRadius: '$md 0 0 $md', + }, + }), + }), + text: cssObj({ + fontSize: '$sm', + fontWeight: '$normal', + }), +}; diff --git a/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx b/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx new file mode 100644 index 000000000..0ed0add6a --- /dev/null +++ b/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx @@ -0,0 +1,163 @@ +import { Box, Button, Input, Text, VStack } from '@fuel-ui/react'; +import { AnimatePresence } from 'framer-motion'; +import { type BN, DEFAULT_DECIMAL_UNITS, bn } from 'fuels'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; +import { MotionFlex, MotionStack, animations } from '~/systems/Core'; +import type { SendFormValues } from '~/systems/Send/hooks'; +import { TxFee } from '../TxFee'; + +type TxFeeOptionsProps = { + baseFee: BN; + baseGasLimit: BN; + regularTip: BN; + fastTip: BN; +}; + +const DECIMAL_UNITS = DEFAULT_DECIMAL_UNITS; + +export const TxFeeOptions = ({ + baseFee, + baseGasLimit, + regularTip, + fastTip, +}: TxFeeOptionsProps) => { + const [isAdvanced, setIsAdvanced] = useState(false); + const { control, setValue, trigger } = useFormContext(); + const previousDefaultTip = useRef(regularTip); + const previousGasLimit = useRef(baseGasLimit); + + const { field: tip } = useController({ + control, + name: 'fees.tip', + }); + + // @TODO: Enable this when the SDK gets to work with custom gas limits + const { field: _gasLimit, fieldState: _gasLimitState } = useController({ + control, + name: 'fees.gasLimit', + }); + + const options = useMemo(() => { + return [ + { name: 'Regular', fee: baseFee.add(regularTip), tip: regularTip }, + { name: 'Fast', fee: baseFee.add(fastTip), tip: fastTip }, + ]; + }, [baseFee, regularTip, fastTip]); + + const tipFormatted = useMemo(() => { + return tip.value.format({ + units: DECIMAL_UNITS, + minPrecision: 0, + }); + }, [tip.value]); + + const toggle = () => { + setIsAdvanced((curr) => !curr); + }; + + /** + * Resetting fees if hiding advanced options (or initializing them) + */ + useEffect(() => { + if (!isAdvanced) { + setValue('fees.tip', previousDefaultTip.current); + setValue('fees.gasLimit', previousGasLimit.current); + } + }, [isAdvanced, setValue]); + + return ( + + + {isAdvanced ? ( + + + Tip + + { + const text = e.target.value; + const val = text.replaceAll(',', ''); + const units = bn.parseUnits(val, DECIMAL_UNITS); + + tip.onChange(units); + trigger('amount'); + }} + /> + + + {/* @TODO: Remove this when the SDK gets to work with custom gas limits */} + {/* + Gas limit + + + { + const ignore = /[.,\-+]/g; + const val = (e.target.value || '').replaceAll(ignore, ''); + gasLimit.onChange(bn(val)); + }} + /> + + {gasLimitState.error && ( + + {gasLimitState.error.message} + + )} + + */} + + ) : ( + + {options.map((option) => ( + { + previousDefaultTip.current = option.tip; + setValue('fees.tip', option.tip); + trigger('amount'); + }} + /> + ))} + + )} + + + + + + + ); +}; diff --git a/packages/app/src/systems/Transaction/components/TxHeader/TxHeader.test.tsx b/packages/app/src/systems/Transaction/components/TxHeader/TxHeader.test.tsx index 9c443e49a..c3665c128 100644 --- a/packages/app/src/systems/Transaction/components/TxHeader/TxHeader.test.tsx +++ b/packages/app/src/systems/Transaction/components/TxHeader/TxHeader.test.tsx @@ -71,7 +71,8 @@ describe('TxHeader', () => { ); }); - it('should copy transaction link', async () => { + // @TODO: re-add when we have explorer link updated + it.skip('should copy transaction link', async () => { render( { - const { href, openExplorer } = useExplorerLink(providerUrl, id); + const { href: _href, openExplorer: _openExplorer } = useExplorerLink( + providerUrl, + id + ); return ( @@ -53,7 +56,7 @@ export const TxHeader: TxHeaderComponent = ({ }} tooltipMessage="Copy Transaction ID" /> - - + */} diff --git a/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.stories.tsx b/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.stories.tsx index 7d8888126..6c6e05df6 100644 --- a/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.stories.tsx +++ b/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.stories.tsx @@ -1,8 +1,8 @@ import { Box } from '@fuel-ui/react'; import { TransactionStatus } from 'fuels'; -import { fuelAssets } from '~/systems/Core'; import { + MOCK_FUEL_ASSETS, MOCK_OPERATION_CONTRACT_CALL, MOCK_OPERATION_CONTRACT_CREATED, MOCK_OPERATION_MINT, @@ -25,7 +25,7 @@ export const ContractCall = (args: TxOperationProps) => ( {...args} operation={MOCK_OPERATION_CONTRACT_CALL} status={TransactionStatus.success} - assets={fuelAssets} + assets={MOCK_FUEL_ASSETS} /> ); @@ -38,7 +38,7 @@ export const Transfer = (args: TxOperationProps) => ( {...args} operation={MOCK_OPERATION_TRANSFER} status={TransactionStatus.success} - assets={fuelAssets} + assets={MOCK_FUEL_ASSETS} /> ); @@ -51,7 +51,7 @@ export const ContractCreated = (args: TxOperationProps) => ( {...args} operation={MOCK_OPERATION_CONTRACT_CREATED} status={TransactionStatus.success} - assets={fuelAssets} + assets={MOCK_FUEL_ASSETS} /> ); @@ -64,7 +64,7 @@ export const Mint = (args: TxOperationProps) => ( {...args} operation={MOCK_OPERATION_MINT} status={TransactionStatus.success} - assets={fuelAssets} + assets={MOCK_FUEL_ASSETS} /> ); diff --git a/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.test.tsx b/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.test.tsx index 99f745c6a..ccf680129 100644 --- a/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.test.tsx +++ b/packages/app/src/systems/Transaction/components/TxOperation/TxOperation.test.tsx @@ -1,7 +1,8 @@ import { render, screen, testA11y } from '@fuel-ui/test-utils'; -import { TestWrapper, fuelAssets } from '~/systems/Core'; +import { TestWrapper } from '~/systems/Core'; import { + MOCK_FUEL_ASSETS, MOCK_OPERATION_CONTRACT_CALL, MOCK_OPERATION_TRANSFER, } from '../../__mocks__/operation'; @@ -10,7 +11,7 @@ import { TxOperation } from './TxOperation'; const PROPS = { operation: MOCK_OPERATION_CONTRACT_CALL, - assets: fuelAssets, + assets: MOCK_FUEL_ASSETS, }; describe('TxOperation', () => { diff --git a/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.stories.tsx b/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.stories.tsx index b4ced1d64..129294247 100644 --- a/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.stories.tsx +++ b/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.stories.tsx @@ -1,8 +1,8 @@ import { Box } from '@fuel-ui/react'; import { TransactionStatus } from 'fuels'; -import { fuelAssets } from '~/systems/Core'; import { + MOCK_FUEL_ASSETS, MOCK_OPERATION_CONTRACT_CALL, MOCK_OPERATION_TRANSFER, } from '../../__mocks__/operation'; @@ -23,7 +23,7 @@ export const Default = (args: TxOperationsProps) => ( {...args} operations={[MOCK_OPERATION_CONTRACT_CALL, MOCK_OPERATION_TRANSFER]} status={TransactionStatus.success} - assets={fuelAssets} + assets={MOCK_FUEL_ASSETS} /> ); diff --git a/packages/app/src/systems/Transaction/components/index.tsx b/packages/app/src/systems/Transaction/components/index.tsx index 4f0111988..01403451a 100644 --- a/packages/app/src/systems/Transaction/components/index.tsx +++ b/packages/app/src/systems/Transaction/components/index.tsx @@ -1,5 +1,5 @@ export * from './TxContent'; -export * from './TxDetails'; +export * from './TxFee'; export * from './TxFromTo'; export * from './TxHeader'; export * from './TxLink'; diff --git a/packages/app/src/systems/Transaction/hooks/useTxResult.tsx b/packages/app/src/systems/Transaction/hooks/useTxResult.tsx index 66f20acb5..ab3cb6904 100644 --- a/packages/app/src/systems/Transaction/hooks/useTxResult.tsx +++ b/packages/app/src/systems/Transaction/hooks/useTxResult.tsx @@ -46,7 +46,7 @@ export function useTxResult({ const { error, txResult } = context; // biome-ignore lint/correctness/useExhaustiveDependencies: - const { shouldShowAlert, shouldShowTx, shouldShowTxDetails } = useMemo(() => { + const { shouldShowAlert, shouldShowTx, shouldShowTxFee } = useMemo(() => { const shouldShowAlert = isTxNotFound || isInvalidTxId || @@ -54,12 +54,12 @@ export function useTxResult({ txResult?.isStatusFailure; const shouldShowTx = txResult && !isFetching && !isInvalidTxId && !isTxNotFound; - const shouldShowTxDetails = shouldShowTx && !txResult?.isTypeMint; + const shouldShowTxFee = shouldShowTx && !txResult?.isTypeMint; return { shouldShowAlert, shouldShowTx, - shouldShowTxDetails, + shouldShowTxFee, }; }, [ isTxNotFound, @@ -92,7 +92,7 @@ export function useTxResult({ isTxReceiptsNotFound, shouldShowAlert, shouldShowTx, - shouldShowTxDetails, + shouldShowTxFee, txResult, error, }; diff --git a/packages/app/src/systems/Transaction/machines/transactionMachine.test.tsx b/packages/app/src/systems/Transaction/machines/transactionMachine.test.tsx index 22824a22c..cb891febe 100644 --- a/packages/app/src/systems/Transaction/machines/transactionMachine.test.tsx +++ b/packages/app/src/systems/Transaction/machines/transactionMachine.test.tsx @@ -18,6 +18,9 @@ mockServer([ graphql.query('getNodeInfo', (_req, res, ctx) => { return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); }), + graphql.query('getLatestGasPrice', (_req, res, ctx) => { + return res(ctx.data(MOCK_TRANSACTION_WITH_RECEIPTS_GQL)); + }), ]); describe('transactionMachine', () => { diff --git a/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx b/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx index b5f6cb92b..3e4c23760 100644 --- a/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx +++ b/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx @@ -72,7 +72,10 @@ describe('TxApprove', () => { return false; } }), - txResult: mockTxResult, + shouldShowTxSimulated: true, + shouldShowActions: true, + simulateTxErrors: mockTxResult, + txSummaryExecuted: mockTxResult, approveStatus: jest.fn().mockReturnValue(TransactionStatus.success), handlers: { closeDialog: jest.fn(), @@ -83,6 +86,7 @@ describe('TxApprove', () => { shouldShowTx: true, title: 'Transaction Approval', providerUrl: 'https://example.com', + errors: {}, ...transactionRequestOverrides, }); @@ -122,27 +126,49 @@ describe('TxApprove', () => { }); it('displays success when a transaction was completed', () => { - setup({}, {}, { status: TxRequestStatus.success, result: true }); + setup( + { + shouldShowTxSimulated: false, + shouldShowActions: false, + shouldShowTxExecuted: true, + executedStatus: () => TransactionStatus.success, + }, + {}, + { status: TxRequestStatus.success, result: true } + ); expect(screen.getByText(/success/i)).toBeDefined(); }); it('displays an error message when the transaction fails', () => { setup( - { approveStatus: jest.fn().mockReturnValue(TransactionStatus.failure) }, + { + errors: { + simulateTxErrors: { + InsufficientInputAmount: true, + }, + }, + }, {}, { status: TxRequestStatus.failed, result: true } ); - expect(screen.getByText(/failure/i)).toBeDefined(); + expect(screen.getByText('Not enough funds')).toBeDefined(); }); it('does not show the approve button show actions is false', () => { - setup({ showActions: false }); + setup({ shouldShowActions: false }); expect(screen.queryByText(/approve/i)).toBeNull(); }); it('shows the try again button when the transaction has failed', () => { setup( - { txResult: { ...mockTxResult, status: TransactionStatus.failure } }, + { + shouldShowTxExecuted: true, + executedStatus: () => TransactionStatus.failure, + txSummaryExecuted: { + ...mockTxResult, + status: TransactionStatus.failure, + }, + }, {}, { status: TxRequestStatus.failed, result: true } ); @@ -151,7 +177,14 @@ describe('TxApprove', () => { it('calls the try again handler when try again button is clicked', () => { setup( - { txResult: { ...mockTxResult, status: TransactionStatus.failure } }, + { + shouldShowTxExecuted: true, + executedStatus: () => TransactionStatus.failure, + txSummaryExecuted: { + ...mockTxResult, + status: TransactionStatus.failure, + }, + }, {}, { status: TxRequestStatus.failed, result: true } ); diff --git a/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.tsx b/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.tsx index 9102b647e..0fd04c3a5 100644 --- a/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.tsx +++ b/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.tsx @@ -1,12 +1,12 @@ import { cssObj } from '@fuel-ui/css'; -import { Alert, Button, Dialog } from '@fuel-ui/react'; +import { Button, Dialog } from '@fuel-ui/react'; import { useNavigate } from 'react-router-dom'; import { useAssets } from '~/systems/Asset'; import { Pages } from '~/systems/Core'; import { coreStyles } from '~/systems/Core/styles'; import { useTransactionRequest } from '~/systems/DApp'; import { OverlayDialogTopbar } from '~/systems/Overlay'; -import { TxContent, TxHeader } from '~/systems/Transaction'; +import { TxContent } from '~/systems/Transaction'; export const TxApprove = () => { const ctx = useTransactionRequest(); @@ -21,14 +21,6 @@ export const TxApprove = () => { navigate(Pages.index()); }; - const Header = ( - - - Carefully check if all the details in your transaction are correct - - - ); - return ( <> { {ctx.title} - {ctx.shouldShowLoader && } - {ctx.shouldShowTx && ( + {ctx.shouldShowLoader && } + {ctx.shouldShowTxSimulated && ( )} - {(ctx.status('success') || ctx.status('failed')) && ( + {ctx.shouldShowTxExecuted && ( - } + providerUrl={ctx.providerUrl} footer={ ctx.status('failed') && (