From 14e63852efc57f2ee02106b784ec9b500f5b9f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lcio=20Franco?= Date: Wed, 18 Sep 2024 10:11:30 -0400 Subject: [PATCH] fix: fee estimation error messages (#1479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Closes https://github.com/FuelLabs/fuels-wallet/issues/1457 --- | 📷 Demo | | --- | | Screenshot 2024-09-17 at 11 46 36 | --- .changeset/chilly-planets-hide.md | 5 + .changeset/giant-horses-play.md | 5 + .changeset/swift-bugs-film.md | 5 + examples/cra-dapp/src/Connected.tsx | 8 +- .../background/services/BackgroundService.ts | 12 ++- .../DApp/hooks/useTransactionRequest.tsx | 4 +- .../components/TxContent/TxContent.tsx | 44 ++------- .../components/TxOperations/TxOperations.tsx | 12 ++- .../pages/TxApprove/TxApprove.test.tsx | 8 +- .../Transaction/services/transaction.tsx | 44 +++++---- .../src/systems/Transaction/utils/error.tsx | 93 ++++++------------- pnpm-lock.yaml | 23 +++-- 12 files changed, 120 insertions(+), 143 deletions(-) create mode 100644 .changeset/chilly-planets-hide.md create mode 100644 .changeset/giant-horses-play.md create mode 100644 .changeset/swift-bugs-film.md diff --git a/.changeset/chilly-planets-hide.md b/.changeset/chilly-planets-hide.md new file mode 100644 index 000000000..02dc32d29 --- /dev/null +++ b/.changeset/chilly-planets-hide.md @@ -0,0 +1,5 @@ +--- +"fuels-wallet": minor +--- + +Improve how error messages are displayed/parsed during fee estimation. diff --git a/.changeset/giant-horses-play.md b/.changeset/giant-horses-play.md new file mode 100644 index 000000000..954664ce2 --- /dev/null +++ b/.changeset/giant-horses-play.md @@ -0,0 +1,5 @@ +--- +"fuels-wallet": minor +--- + +Display fees options even when there are tx simulation errors. diff --git a/.changeset/swift-bugs-film.md b/.changeset/swift-bugs-film.md new file mode 100644 index 000000000..308202d8d --- /dev/null +++ b/.changeset/swift-bugs-film.md @@ -0,0 +1,5 @@ +--- +"fuels-wallet": patch +--- + +Allow dApps to pass account owner with `0x` address. diff --git a/examples/cra-dapp/src/Connected.tsx b/examples/cra-dapp/src/Connected.tsx index bce4333b9..2833ea738 100644 --- a/examples/cra-dapp/src/Connected.tsx +++ b/examples/cra-dapp/src/Connected.tsx @@ -11,9 +11,12 @@ import { } from '@fuels/react'; import { DEVNET_NETWORK_URL, TESTNET_NETWORK_URL, bn } from 'fuels'; +import { useState } from 'react'; import './App.css'; export function Connected() { + const [loading, setLoading] = useState(false); + const { fuel } = useFuel(); const { disconnect } = useDisconnect(); const { wallet } = useWallet(); @@ -60,6 +63,7 @@ export function Connected() { diff --git a/packages/app/src/systems/CRX/background/services/BackgroundService.ts b/packages/app/src/systems/CRX/background/services/BackgroundService.ts index 827ba56cb..16018bb89 100644 --- a/packages/app/src/systems/CRX/background/services/BackgroundService.ts +++ b/packages/app/src/systems/CRX/background/services/BackgroundService.ts @@ -43,6 +43,7 @@ export class BackgroundService { 'accounts', 'connect', 'network', + 'networks', 'disconnect', 'signMessage', 'sendTransaction', @@ -302,13 +303,22 @@ export class BackgroundService { ); } + const { address, provider, transaction } = input; + const popupService = await PopUpService.open( origin, Pages.requestTransaction(), this.communicationProtocol ); + + // We need to forward bech32 addresses to the popup, regardless if we receive a b256 here + // our database is storing fuel addresses + const bech32Address = Address.fromDynamicInput(address).toString(); + const signedMessage = await popupService.sendTransaction({ - ...input, + address: bech32Address, + provider, + transaction, origin, title, favIconUrl, diff --git a/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx b/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx index e99a8d839..c7c00f350 100644 --- a/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx +++ b/packages/app/src/systems/DApp/hooks/useTransactionRequest.tsx @@ -29,9 +29,7 @@ const selectors = { errors(state: TransactionRequestState) { if (!state.context.errors) return {}; const simulateTxErrors = state.context.errors?.simulateTxErrors; - const hasSimulateTxErrors = Boolean( - Object.keys(simulateTxErrors || {}).length - ); + const hasSimulateTxErrors = Boolean(simulateTxErrors); const txApproveError = state.context.errors?.txApproveError; return { txApproveError, simulateTxErrors, hasSimulateTxErrors }; }, diff --git a/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx b/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx index 46b9f563e..61a54f645 100644 --- a/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx +++ b/packages/app/src/systems/Transaction/components/TxContent/TxContent.tsx @@ -23,45 +23,15 @@ import { import { TxFeeOptions } from '../TxFeeOptions/TxFeeOptions'; const ErrorHeader = ({ errors }: { errors?: GroupedErrors }) => { - const errorMessages = useMemo(() => { - const messages = []; - if (errors) { - if (errors.InsufficientInputAmount || errors.NotEnoughCoins) { - messages.push('Not enough funds'); - } - - // biome-ignore lint: will not be a large array - Object.keys(errors).forEach((key: string) => { - if (key === 'InsufficientInputAmount' || key === 'NotEnoughCoins') { - 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} - - ))} + + {errors} ); diff --git a/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.tsx b/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.tsx index 4938b6812..aea0c9dd9 100644 --- a/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.tsx +++ b/packages/app/src/systems/Transaction/components/TxOperations/TxOperations.tsx @@ -1,4 +1,4 @@ -import { Box } from '@fuel-ui/react'; +import { Alert, Box } from '@fuel-ui/react'; import type { AssetData } from '@fuel-wallet/types'; import type { Operation, TransactionStatus } from 'fuels'; import type { Maybe } from '~/systems/Core'; @@ -18,6 +18,16 @@ export function TxOperations({ assets, isLoading, }: TxOperationsProps) { + if (operations?.length === 0) { + return ( + + + No operations found in this transaction + + + ); + } + return ( {operations?.map((operation, index) => ( 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 26c2f2474..2ed27e442 100644 --- a/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx +++ b/packages/app/src/systems/Transaction/pages/TxApprove/TxApprove.test.tsx @@ -75,7 +75,7 @@ describe('TxApprove', () => { shouldShowTxSimulated: true, shouldShowTxExecuted: false, shouldShowActions: true, - simulateTxErrors: mockTxResult, + simulateTxErrors: 'Unknown error', txSummarySimulated: mockTxResult, approveStatus: jest.fn().mockReturnValue(TransactionStatus.success), handlers: { @@ -168,15 +168,13 @@ describe('TxApprove', () => { setup( { errors: { - simulateTxErrors: { - InsufficientInputAmount: true, - }, + simulateTxErrors: 'Insufficient Input Amount', }, }, {}, { status: TxRequestStatus.failed, result: true } ); - expect(screen.getByText('Not enough funds')).toBeDefined(); + expect(screen.getByText('Insufficient Input Amount')).toBeDefined(); }); it('does not show the approve button show actions is false', () => { diff --git a/packages/app/src/systems/Transaction/services/transaction.tsx b/packages/app/src/systems/Transaction/services/transaction.tsx index f6943dc60..95e0521fe 100644 --- a/packages/app/src/systems/Transaction/services/transaction.tsx +++ b/packages/app/src/systems/Transaction/services/transaction.tsx @@ -4,6 +4,8 @@ import type { TransactionRequest, WalletLocked } from 'fuels'; import { Address, type BN, + ErrorCode, + FuelError, TransactionResponse, TransactionStatus, assembleTransactionSummary, @@ -19,7 +21,7 @@ import { createProvider } from '@fuel-wallet/connections'; import { AccountService } from '~/systems/Account/services/account'; import { NetworkService } from '~/systems/Network/services/network'; import type { Transaction } from '../types'; -import { getAbiMap, getGroupedErrors } from '../utils'; +import { type GroupedErrors, getAbiMap, getErrorMessage } from '../utils'; import { getCurrentTips } from '../utils/fee'; export type TxInputs = { @@ -206,16 +208,11 @@ export class TxService { minGasLimit: customFee?.gasUsed, txSummary: { ...txSummary, - // if customFee was chosen, we override the txSummary fee with the customFee fee: feeAdaptedToSdkDiff, gasUsed: txSummary.gasUsed, - // fee: customFee?.txCost?.maxFee || feeAdaptedToSdkDiff, - // gasUsed: customFee?.txCost?.gasUsed || txSummary.gasUsed, }, }; - - // biome-ignore lint/suspicious/noExplicitAny: allow any - } catch (e: any) { + } catch (e) { const { gasPerByte, gasPriceFactor, gasCosts, maxGasPerTx } = provider.getGasConfig(); const consensusParameters = provider.getChain().consensusParameters; @@ -228,9 +225,8 @@ export class TxService { inputs: transaction.inputs, }); - const errorsToParse = - e.name === 'FuelError' ? [{ message: e.message }] : e.response?.errors; - const simulateTxErrors = getGroupedErrors(errorsToParse); + const simulateTxErrors: GroupedErrors = + e instanceof FuelError ? getErrorMessage(e) : 'Unknown error'; const gasPrice = await provider.getLatestGasPrice(); const baseAssetId = provider.getBaseAssetId(); @@ -251,9 +247,14 @@ export class TxService { txSummary.isStatusFailure = true; txSummary.status = TransactionStatus.failure; + // Fallback to the values from the transactionRequest + if ('gasLimit' in transactionRequest) { + txSummary.gasUsed = transactionRequest.gasLimit; + } + return { - baseFee: undefined, - minGasLimit: undefined, + baseFee: txSummary.fee.add(1), + minGasLimit: txSummary.gasUsed, txSummary, simulateTxErrors, }; @@ -361,16 +362,10 @@ export class TxService { } catch (e) { attempts += 1; - // @TODO: Waiting to match with FuelError type and ErrorCode enum from "fuels" - // These types are not exported from "fuels" package, but they exists in the "@fuels-ts/errors" - if ( - e instanceof Error && - 'toObject' in e && - typeof e.toObject === 'function' - ) { - const error: { code: string } = e.toObject(); + if (e instanceof FuelError) { + const error = e.toObject(); - if (error.code === 'gas-limit-too-low') { + if (error.code === ErrorCode.GAS_LIMIT_TOO_LOW) { throw e; } } @@ -386,7 +381,7 @@ export class TxService { }; } - static async computeCustomFee({ + private static async computeCustomFee({ wallet, transactionRequest, }: TxInputs['computeCustomFee']) { @@ -399,7 +394,10 @@ export class TxService { // funding the transaction with the required quantities (the maxFee might have changed) await wallet.fund(transactionRequest, { - ...txCost, + estimatedPredicates: txCost.estimatedPredicates, + addedSignatures: txCost.addedSignatures, + gasPrice: txCost.gasPrice, + updateMaxFee: txCost.updateMaxFee, requiredQuantities: [], }); diff --git a/packages/app/src/systems/Transaction/utils/error.tsx b/packages/app/src/systems/Transaction/utils/error.tsx index 7508f22d7..8c053d78b 100644 --- a/packages/app/src/systems/Transaction/utils/error.tsx +++ b/packages/app/src/systems/Transaction/utils/error.tsx @@ -1,3 +1,5 @@ +import { ErrorCode, type FuelError } from 'fuels'; + export type VMApiError = { // biome-ignore lint/suspicious/noExplicitAny: request: any; @@ -11,76 +13,39 @@ export type VMApiError = { }; }; -export type VmErrorType = 'InsufficientInputAmount' | string; - export type InsufficientInputAmountError = { asset: string; expected: string; provided: string; }; -export type GroupedErrors = { - InsufficientInputAmount: InsufficientInputAmountError; - NotEnoughCoins: string; - // biome-ignore lint/suspicious/noExplicitAny: allow any - [key: VmErrorType]: Record | string | unknown; -}; - -export const getGroupedErrors = (rawErrors?: { message: string }[]) => { - if (!rawErrors) return undefined; - - const groupedErrors = rawErrors.reduce( - (prevGroupedError, rawError) => { - const { message } = rawError; - // in some case I had to add the Validity() to the regex. why? - // const regex = /Validity\((\w+)\s+(\{.*\})\)/; - const regex = /(\w+)\s+(\{.*\})/; +export type GroupedErrors = string | undefined; - const match = message.match(regex); - if (match) { - const errorType = match[1]; - const errorMessage = match[2]; - - // biome-ignore lint/suspicious/noImplicitAnyLet: allow any - let errorValue; - try { - const keyValuesMessage = errorMessage - .replace('{ ', '') - .replace(' }', '') - .split(', '); - const errorParsed = keyValuesMessage.reduce((prevError, keyValue) => { - const [key, value] = keyValue.split(': '); - - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...prevError, - [key]: key === 'asset' ? `0x${value}` : value, - }; - }, {}); - errorValue = errorParsed; - } catch (_) { - errorValue = errorMessage; - } - - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...prevGroupedError, - [errorType]: errorValue, - }; - } - if (message.includes('not enough coins to fit the target')) { - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...prevGroupedError, - NotEnoughCoins: message, - }; - } - - return prevGroupedError; - }, - // biome-ignore lint/suspicious/noExplicitAny: allow any - {} as any - ); +const camelCaseToHuman = (message: string): string => { + return message.replace(/([a-z])([A-Z])/g, '$1 $2'); +}; - return groupedErrors; +export const getErrorMessage = ( + error: FuelError | undefined +): GroupedErrors => { + if (!error) return undefined; + + // FuelCore error with Validity() + // Example: Validity(TransactionMaxGasExceeded) + if (error.message.startsWith('Validity(')) { + const validity = error.message.match(/\((\w+)\)/); + if (validity) { + return camelCaseToHuman(validity[1]); + } + } + + // FuelCore error with object + // Example: InsufficientMaxFee { max_fee_from_policies: 0, max_fee_from_gas_price: 571 } + const withObject = /^([a-zA-Z]+)(?:\s*\{(.+)\})?$/; + const match = error.message.match(withObject); + if (match) { + return `${camelCaseToHuman(match[1])} { ${match[2]} }`; + } + + return error.message; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e89afcea1..013b012bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3431,6 +3431,9 @@ packages: '@noble/curves@1.4.0': resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + '@noble/curves@1.5.0': + resolution: {integrity: sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==} + '@noble/curves@1.6.0': resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} engines: {node: ^14.21.3 || >=16} @@ -19004,7 +19007,7 @@ snapshots: '@metamask/utils@8.4.0': dependencies: '@ethereumjs/tx': 4.2.0 - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.4.0 '@scure/base': 1.1.6 '@types/debug': 4.1.12 debug: 4.3.4 @@ -19131,6 +19134,10 @@ snapshots: dependencies: '@noble/hashes': 1.4.0 + '@noble/curves@1.5.0': + dependencies: + '@noble/hashes': 1.4.0 + '@noble/curves@1.6.0': dependencies: '@noble/hashes': 1.5.0 @@ -21820,8 +21827,8 @@ snapshots: '@solana/web3.js@1.91.7(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.0 - '@noble/curves': 1.6.0 - '@noble/hashes': 1.5.0 + '@noble/curves': 1.5.0 + '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 bigint-buffer: 1.1.5 @@ -21842,8 +21849,8 @@ snapshots: '@solana/web3.js@1.93.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.0 - '@noble/curves': 1.6.0 - '@noble/hashes': 1.5.0 + '@noble/curves': 1.5.0 + '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 bigint-buffer: 1.1.5 @@ -23124,7 +23131,7 @@ snapshots: '@testing-library/jest-dom@6.1.4(@jest/globals@29.7.0)(@types/jest@28.1.3)(jest@29.7.0(@types/node@20.12.11)(ts-node@10.9.1(@swc/core@1.3.92)(@types/node@20.8.4)(typescript@5.2.2)))': dependencies: '@adobe/css-tools': 4.3.2 - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.0 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 @@ -33598,8 +33605,8 @@ snapshots: webauthn-p256@0.0.5: dependencies: - '@noble/curves': 1.6.0 - '@noble/hashes': 1.5.0 + '@noble/curves': 1.5.0 + '@noble/hashes': 1.4.0 webextension-polyfill@0.10.0: {}