From f7796988b3c6c1b23a433d1679a26654fab67489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 10 Oct 2024 11:24:57 +0200 Subject: [PATCH] refactor: perform eth_sendTransaction via EVM module (#51) --- package.json | 8 +- .../connections/dAppConnection/models.ts | 1 - .../connections/dAppConnection/registry.ts | 2 - .../connections/extensionConnection/models.ts | 1 + .../extensionConnection/registry.ts | 2 + .../ActiveNetworkMiddleware.test.ts | 51 + .../middlewares/ActiveNetworkMiddleware.ts | 36 +- .../services/actions/ActionsService.test.ts | 42 + .../services/actions/ActionsService.ts | 27 + .../actions/handlers/updateTxData.test.ts | 57 + .../services/actions/handlers/updateTxData.ts | 60 + src/background/services/actions/models.ts | 2 +- .../services/blockaid/BlockaidService.test.ts | 104 -- .../services/blockaid/BlockaidService.ts | 286 ---- .../services/network/NetworkService.test.ts | 26 + .../services/network/NetworkService.ts | 4 +- .../contracts/contractParsers/addLiquidity.ts | 116 -- .../contractParsers/addLiquidityAVAX.ts | 94 -- .../contracts/contractParsers/approve.ts | 84 -- .../contractParsers/contractParserMap.ts | 26 - .../contracts/contractParsers/models.ts | 26 - .../contractParsers/parseWithERC20Abi.ts | 91 -- .../contracts/contractParsers/simpleSwap.ts | 135 -- .../contractParsers/swapAvaxForExactTokens.ts | 104 -- .../contractParsers/swapExactTokensForAVAX.ts | 123 -- .../swapExactTokensForTokens.ts | 139 -- .../contractParsers/utils/helpers.ts | 3 - .../utils/parseBasicDisplayValues.ts | 75 - .../contracts/utils/dataParser.ts | 11 - .../eth_sendTransaction.test.ts | 1261 ----------------- .../eth_sendTransaction.ts | 461 ------ .../handlers/eth_sendTransaction/index.ts | 1 - .../utils/getTargetNetworkForTx.ts | 7 +- .../utils/getTxDescription.ts | 35 - .../eth_sendTransaction/utils/getTxInfo.ts | 83 -- .../utils/txToCustomEvmTx.ts | 50 - .../vmModules/ApprovalController.test.ts | 256 ++-- .../vmModules/ApprovalController.ts | 102 +- .../buildBtcSendTransactionAction.test.ts | 85 -- .../helpers/buildBtcSendTransactionAction.ts | 24 - src/components/common/MaliciousTxAlert.tsx | 18 +- src/contexts/BridgeProvider.test.tsx | 6 +- src/contexts/BridgeProvider.tsx | 7 +- src/contexts/SwapProvider/SwapProvider.tsx | 12 +- src/contexts/UnifiedBridgeProvider.test.tsx | 6 +- src/contexts/UnifiedBridgeProvider.tsx | 7 +- src/localization/locales/en/translation.json | 1 - .../ApproveAction/GenericApprovalScreen.tsx | 68 +- .../ApproveAction/hooks/useFeeCustomizer.tsx | 249 ++-- src/pages/Permissions/Permissions.tsx | 1 + .../Permissions/components/AlertDialog.tsx | 6 +- src/pages/Send/hooks/useSend/useEVMSend.ts | 15 +- src/pages/Send/utils/buildSendTx.test.ts | 8 +- src/pages/Send/utils/buildSendTx.ts | 8 +- src/pages/SignTransaction/SignTransaction.tsx | 381 ----- .../components/NftAccordion.tsx | 60 +- .../SpendLimitInfo/CustomSpendLimit.tsx | 4 +- .../NftCollectionSpendLimit.tsx | 62 - .../SpendLimitInfo/NftSpendLimit.tsx | 33 +- .../SpendLimitInfo/SpendLimitInfo.tsx | 79 +- .../SpendLimitInfo/TokenSpendLimit.tsx | 132 +- .../components/TransactionTokenCard.tsx | 84 +- .../components/TxBalanceChange.tsx | 215 +-- src/popup/ApprovalRoutes.tsx | 11 - src/tests/test-utils.tsx | 2 + src/utils/calculateGasAndFees.ts | 29 +- yarn.lock | 40 +- 67 files changed, 1021 insertions(+), 4624 deletions(-) create mode 100644 src/background/connections/middlewares/ActiveNetworkMiddleware.test.ts create mode 100644 src/background/services/actions/handlers/updateTxData.test.ts create mode 100644 src/background/services/actions/handlers/updateTxData.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidity.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidityAVAX.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/approve.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/contractParserMap.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/models.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/parseWithERC20Abi.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/simpleSwap.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapAvaxForExactTokens.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForAVAX.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForTokens.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/helpers.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/parseBasicDisplayValues.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/contracts/utils/dataParser.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/index.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxDescription.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxInfo.ts delete mode 100644 src/background/services/wallet/handlers/eth_sendTransaction/utils/txToCustomEvmTx.ts delete mode 100644 src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts delete mode 100644 src/background/vmModules/helpers/buildBtcSendTransactionAction.ts delete mode 100644 src/pages/SignTransaction/SignTransaction.tsx delete mode 100644 src/pages/SignTransaction/components/SpendLimitInfo/NftCollectionSpendLimit.tsx diff --git a/package.json b/package.json index 0ccfe4581..965e59a1d 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "sentry": "node sentryscript.js" }, "dependencies": { - "@avalabs/avalanche-module": "0.7.3", + "@avalabs/avalanche-module": "0.8.0", "@avalabs/avalanchejs": "4.0.5", - "@avalabs/bitcoin-module": "0.7.3", + "@avalabs/bitcoin-module": "0.8.0", "@avalabs/bridge-unified": "2.1.0", "@avalabs/core-bridge-sdk": "3.1.0-alpha.7", "@avalabs/core-chains-sdk": "3.1.0-alpha.7", @@ -37,11 +37,11 @@ "@avalabs/core-token-prices-sdk": "3.1.0-alpha.7", "@avalabs/core-utils-sdk": "3.1.0-alpha.7", "@avalabs/core-wallets-sdk": "3.1.0-alpha.7", - "@avalabs/evm-module": "0.7.3", + "@avalabs/evm-module": "0.8.0", "@avalabs/glacier-sdk": "3.1.0-alpha.7", "@avalabs/hw-app-avalanche": "0.14.1", "@avalabs/types": "3.1.0-alpha.3", - "@avalabs/vm-module-types": "0.7.3", + "@avalabs/vm-module-types": "0.8.0", "@blockaid/client": "0.10.0", "@coinbase/cbpay-js": "1.6.0", "@cubist-labs/cubesigner-sdk": "0.3.28", diff --git a/src/background/connections/dAppConnection/models.ts b/src/background/connections/dAppConnection/models.ts index 1eed2f5c7..e5e555f1a 100644 --- a/src/background/connections/dAppConnection/models.ts +++ b/src/background/connections/dAppConnection/models.ts @@ -15,7 +15,6 @@ export enum DAppProviderRequest { WALLET_GET_CHAIN = 'wallet_getEthereumChain', WALLET_SWITCH_ETHEREUM_CHAIN = 'wallet_switchEthereumChain', WALLET_WATCH_ASSET = 'wallet_watchAsset', - ETH_SEND_TX = 'eth_sendTransaction', PERSONAL_EC_RECOVER = 'personal_ecRecover', PERSONAL_SIGN = 'personal_sign', ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4', diff --git a/src/background/connections/dAppConnection/registry.ts b/src/background/connections/dAppConnection/registry.ts index 8283eab8d..01e1573c5 100644 --- a/src/background/connections/dAppConnection/registry.ts +++ b/src/background/connections/dAppConnection/registry.ts @@ -19,7 +19,6 @@ import { WalletGetPermissionsHandler } from '@src/background/services/permission import { WalletRequestPermissionsHandler } from '@src/background/services/permissions/handlers/wallet_requestPermissions'; import { WalletWatchAssetHandler } from '@src/background/services/settings/events/wallet_watchAsset'; import { WalletGetEthereumChainHandler } from '@src/background/services/network/handlers/wallet_getEthereumChain'; -import { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; import { AvalancheSelectWalletHandler } from '@src/background/services/web3/handlers/avalanche_selectWallet'; import { ConnectRequestHandler } from '@src/background/services/web3/handlers/connect'; import { AvalancheGetProviderState } from '@src/background/services/web3/handlers/avalanche_getProviderState'; @@ -69,7 +68,6 @@ import { AvalancheRenameAccountHandler } from '@src/background/services/accounts { token: 'DAppRequestHandler', useToken: WalletGetPermissionsHandler }, { token: 'DAppRequestHandler', useToken: WalletRequestPermissionsHandler }, { token: 'DAppRequestHandler', useToken: WalletWatchAssetHandler }, - { token: 'DAppRequestHandler', useToken: EthSendTransactionHandler }, { token: 'DAppRequestHandler', useToken: ConnectRequestHandler }, { token: 'DAppRequestHandler', useToken: AvalancheGetProviderState }, { diff --git a/src/background/connections/extensionConnection/models.ts b/src/background/connections/extensionConnection/models.ts index 8d23652cc..8104c38eb 100644 --- a/src/background/connections/extensionConnection/models.ts +++ b/src/background/connections/extensionConnection/models.ts @@ -54,6 +54,7 @@ export enum ExtensionRequest { ACTION_GET = 'action_getAction', ACTION_UPDATE = 'action_updateAction', + ACTION_UPDATE_TX_DATA = 'action_updateTxData', PERMISSIONS_ADD_DOMAIN = 'permissions_addDomain', PERMISSIONS_GET_PERMISSIONS = 'permissions_getPermissionsForDomain', diff --git a/src/background/connections/extensionConnection/registry.ts b/src/background/connections/extensionConnection/registry.ts index a4e5a130e..ff18bd518 100644 --- a/src/background/connections/extensionConnection/registry.ts +++ b/src/background/connections/extensionConnection/registry.ts @@ -128,6 +128,7 @@ import { StartBalancesPollingHandler } from '@src/background/services/balances/h import { StopBalancesPollingHandler } from '@src/background/services/balances/handlers/stopBalancesPolling'; import { BalancesUpdatedEvents } from '@src/background/services/balances/events/balancesUpdatedEvent'; import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBridge/handlers/unifiedBridgeTrackTransfer'; +import { UpdateActionTxDataHandler } from '@src/background/services/actions/handlers/updateTxData'; /** * TODO: GENERATE THIS FILE AS PART OF THE BUILD PROCESS @@ -141,6 +142,7 @@ import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBrid { token: 'ExtensionRequestHandler', useToken: DeleteAccountHandler }, { token: 'ExtensionRequestHandler', useToken: GetActionHandler }, { token: 'ExtensionRequestHandler', useToken: UpdateActionHandler }, + { token: 'ExtensionRequestHandler', useToken: UpdateActionTxDataHandler }, { token: 'ExtensionRequestHandler', useToken: ClearAnalyticsIdsHandler }, { token: 'ExtensionRequestHandler', useToken: GetAnalyticsIdsHandler }, { token: 'ExtensionRequestHandler', useToken: InitAnalyticsIdsHandler }, diff --git a/src/background/connections/middlewares/ActiveNetworkMiddleware.test.ts b/src/background/connections/middlewares/ActiveNetworkMiddleware.test.ts new file mode 100644 index 000000000..c18632c45 --- /dev/null +++ b/src/background/connections/middlewares/ActiveNetworkMiddleware.test.ts @@ -0,0 +1,51 @@ +import { NetworkService } from '@src/background/services/network/NetworkService'; +import { ActiveNetworkMiddleware } from './ActiveNetworkMiddleware'; +import { RpcMethod } from '@avalabs/vm-module-types'; +import getTargetNetworkForTx from '@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx'; + +jest.mock( + '@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx' +); + +describe('src/background/connections/middlewares/ActiveNetworkMiddleware', () => { + const networkService = { + getNetwork: jest.fn(), + } as unknown as NetworkService; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('errors out for cross-environment EVM transaction attempts', async () => { + const call = ActiveNetworkMiddleware(networkService); + + const next = jest.fn(); + const onError = jest.fn(); + const error = new Error('Cross-env error'); + + jest.mocked(getTargetNetworkForTx).mockRejectedValueOnce(error); + + await call( + { + request: { + params: { + scope: 'eip155:43114', // C-Chain Mainnet + request: { + method: RpcMethod.ETH_SEND_TRANSACTION, + params: [ + { + chainId: '0xa869', // C-Chain Fuji (43113) + }, + ], + }, + }, + }, + } as any, + next, + onError + ); + + expect(next).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalledWith(error); + }); +}); diff --git a/src/background/connections/middlewares/ActiveNetworkMiddleware.ts b/src/background/connections/middlewares/ActiveNetworkMiddleware.ts index 464c81c8a..c22305b16 100644 --- a/src/background/connections/middlewares/ActiveNetworkMiddleware.ts +++ b/src/background/connections/middlewares/ActiveNetworkMiddleware.ts @@ -7,6 +7,8 @@ import { ExtensionConnectionMessage, ExtensionConnectionMessageResponse, } from '../models'; +import { RpcMethod } from '@avalabs/vm-module-types'; +import getTargetNetworkForTx from '@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx'; export function ActiveNetworkMiddleware( networkService: NetworkService @@ -15,21 +17,39 @@ export function ActiveNetworkMiddleware( JsonRpcResponse | ExtensionConnectionMessageResponse > { return async (context, next, error) => { - const { scope } = context.request.params; + const { + scope, + request: { method, params }, + } = context.request.params; - if (scope) { - const network = await networkService.getNetwork( - context.request.params.scope - ); + if (!scope) { + next(); + return; + } + + const isEthSendTx = method === RpcMethod.ETH_SEND_TRANSACTION; + const hasParams = Array.isArray(params) && typeof params[0] === 'object'; + + let network; - if (!network) { - error(new Error(`Unrecognized network: ${scope}`)); + if (isEthSendTx && hasParams) { + try { + network = await getTargetNetworkForTx(params[0], networkService, scope); + } catch (err: any) { + error(err); return; } + } else { + network = await networkService.getNetwork(scope); + } - context.network = network; + if (!network) { + error(new Error(`Unrecognized network: ${scope}`)); + return; } + context.network = network; + next(); }; } diff --git a/src/background/services/actions/ActionsService.test.ts b/src/background/services/actions/ActionsService.test.ts index 81c6f4ba5..31800bc9d 100644 --- a/src/background/services/actions/ActionsService.test.ts +++ b/src/background/services/actions/ActionsService.test.ts @@ -64,6 +64,7 @@ describe('background/services/actions/ActionsService.ts', () => { approvalController = { onApproved: jest.fn(), onRejected: jest.fn(), + updateTx: jest.fn(), } as unknown as jest.Mocked; actionsService = new ActionsService( @@ -75,6 +76,47 @@ describe('background/services/actions/ActionsService.ts', () => { (filterStaleActions as jest.Mock).mockImplementation((a) => a); }); + describe('updateTx()', () => { + it('throws error if the request does not exist', async () => { + await expect(actionsService.updateTx('weird-id', {})).rejects.toThrow( + /No request found with id/ + ); + }); + + it('uses the ApprovalController.updateTx() to fetch the new action data & saves it', async () => { + const pendingActions = { + 'id-0': { + actionId: 'id-0', + }, + 'id-1': { + actionId: 'id-1', + }, + }; + jest + .spyOn(actionsService, 'getActions') + .mockResolvedValueOnce(pendingActions as any); + + const signingData = { outputs: [], inputs: [] } as any; + const newDisplayData = { ...displayData }; + const updatedActionData = { + signingData, + displayData: newDisplayData, + } as any; + + approvalController.updateTx.mockReturnValueOnce(updatedActionData); + + await actionsService.updateTx('id-1', { feeRate: 5 }); + + expect(storageService.save).toHaveBeenCalledWith(ACTIONS_STORAGE_KEY, { + ...pendingActions, + 'id-1': { + ...pendingActions['id-1'], + ...updatedActionData, + }, + }); + }); + }); + describe('getActions', () => { it('gets actions from storage and session when unlocked', async () => { const actions = { diff --git a/src/background/services/actions/ActionsService.ts b/src/background/services/actions/ActionsService.ts index 2317462c0..30a785b4a 100644 --- a/src/background/services/actions/ActionsService.ts +++ b/src/background/services/actions/ActionsService.ts @@ -19,6 +19,7 @@ import { ACTION_HANDLED_BY_MODULE } from '@src/background/models'; import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { getUpdatedSigningData } from '@src/utils/actions/getUpdatedActionData'; import { ApprovalController } from '@src/background/vmModules/ApprovalController'; +import { BtcTxUpdateFn, EvmTxUpdateFn } from '@avalabs/vm-module-types'; @singleton() export class ActionsService implements OnStorageReady { @@ -212,6 +213,32 @@ export class ActionsService implements OnStorageReady { } } + async updateTx( + id: string, + newData: Parameters[0] | Parameters[0] + ) { + const currentPendingRequests = await this.getActions(); + const pendingRequest = currentPendingRequests[id]; + + if (!pendingRequest) { + throw new Error(`No request found with id: ${id}`); + } + + const { signingData, displayData } = this.approvalController.updateTx( + id, + newData + ); + + await this.saveActions({ + ...currentPendingRequests, + [id]: { + ...pendingRequest, + signingData, + displayData, + }, + }); + } + addListener( event: ActionsEvent.ACTION_COMPLETED, callback: (data: { diff --git a/src/background/services/actions/handlers/updateTxData.test.ts b/src/background/services/actions/handlers/updateTxData.test.ts new file mode 100644 index 000000000..9d3b01964 --- /dev/null +++ b/src/background/services/actions/handlers/updateTxData.test.ts @@ -0,0 +1,57 @@ +import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; +import { UpdateActionTxDataHandler } from './updateTxData'; +import { matchingPayload } from '@src/tests/test-utils'; +import { SendErrorMessage } from '@src/utils/send/models'; + +describe('src/background/services/actions/handlers/updateTxData', () => { + const actionsService = { + getActions: jest.fn(), + updateTx: jest.fn(), + }; + + const handleRequest = async (request) => { + const handler = new UpdateActionTxDataHandler(actionsService as any); + + return handler.handle(request); + }; + + const getRequest = (params) => ({ + request: { + id: '1234', + method: ExtensionRequest.ACTION_UPDATE_TX_DATA, + params, + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('remaps BTC tx transaction error to insufficient balance for fee', async () => { + jest.mocked(actionsService.getActions).mockResolvedValue({ + id: {}, + }); + jest + .mocked(actionsService.updateTx) + .mockRejectedValueOnce({ message: 'Unable to create transaction' }); + + expect(await handleRequest(getRequest(['id', { feeRate: 5 }]))).toEqual( + matchingPayload({ error: SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE }) + ); + }); + + it('validates the request to update exists', async () => { + expect(await handleRequest(getRequest(['id', { feeRate: 5 }]))).toEqual( + matchingPayload({ error: 'no pending requests found' }) + ); + }); + + it('calls ActionsService.updateTx()', async () => { + jest.mocked(actionsService.getActions).mockResolvedValue({ + id: {}, + }); + + await handleRequest(getRequest(['id', { feeRate: 5 }])); + expect(actionsService.updateTx).toHaveBeenCalledWith('id', { feeRate: 5 }); + }); +}); diff --git a/src/background/services/actions/handlers/updateTxData.ts b/src/background/services/actions/handlers/updateTxData.ts new file mode 100644 index 000000000..e28d5653e --- /dev/null +++ b/src/background/services/actions/handlers/updateTxData.ts @@ -0,0 +1,60 @@ +import { injectable } from 'tsyringe'; +import { EvmTxUpdateFn, BtcTxUpdateFn } from '@avalabs/vm-module-types'; + +import { SendErrorMessage } from '@src/utils/send/models'; +import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; +import { ExtensionRequestHandler } from '@src/background/connections/models'; + +import { ActionsService } from '../ActionsService'; + +type HandlerType = ExtensionRequestHandler< + ExtensionRequest.ACTION_UPDATE_TX_DATA, + null, + [ + id: string, + newData: Parameters[0] | Parameters[0] + ] +>; + +@injectable() +export class UpdateActionTxDataHandler implements HandlerType { + method = ExtensionRequest.ACTION_UPDATE_TX_DATA as const; + + constructor(private actionsService: ActionsService) {} + handle: HandlerType['handle'] = async ({ request }) => { + const [id, newData] = request.params; + + if (!id) { + return { + ...request, + error: 'no request id in params', + }; + } + + const actions = await this.actionsService.getActions(); + + if (!actions) { + return { ...request, error: 'no pending requests found' }; + } + + const action = actions[id]; + + if (!action) { + return { ...request, error: 'no request found with that id' }; + } + + try { + await this.actionsService.updateTx(id, newData); + return { ...request, result: null }; + } catch (err: any) { + if (err?.message === 'Unable to create transaction') { + return { + ...request, + error: SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE, + }; + } + + return { ...request, error: err }; + } + }; +} diff --git a/src/background/services/actions/models.ts b/src/background/services/actions/models.ts index 4b1c9c05a..013e45dfd 100644 --- a/src/background/services/actions/models.ts +++ b/src/background/services/actions/models.ts @@ -44,7 +44,7 @@ export interface ActionUpdate { id: any; status: ActionStatus; displayData?: DisplayData; - signingData?: SigningData; + signingData?: never; // Don't allow overriding signingData this way result?: any; error?: string; tabId?: number; diff --git a/src/background/services/blockaid/BlockaidService.test.ts b/src/background/services/blockaid/BlockaidService.test.ts index 08d0fc3c2..a4f6fd928 100644 --- a/src/background/services/blockaid/BlockaidService.test.ts +++ b/src/background/services/blockaid/BlockaidService.test.ts @@ -106,17 +106,6 @@ jest.mock('@blockaid/client', () => { })); }); -const txMock = { - chainId: '0xa86a', - from: 'fromAddress', - to: 'toAddress', - value: '200000000000000000', - type: 2, - maxFeePerGas: '0xbfda3a300', - maxPriorityFeePerGas: '0x1dcd6500', - gasLimit: '0x5208', -}; - const networkMock = { chainName: 'test chain', chainId: 123, @@ -147,38 +136,6 @@ describe('background/services/blockaid/BlockaidService', () => { } as any; }); - it('should throw an error because the `BLOCKAID_TRANSACTION_SCAN` feature flag is disabled', async () => { - featureFlagService = { - featureFlags: { - [FeatureGates.BLOCKAID_TRANSACTION_SCAN]: false, - }, - } as any; - const service = new BlockaidService(featureFlagService); - await expect( - service.parseTransaction('core.test', networkMock, txMock) - ).rejects.toThrow('The transaction scanning is disabled'); - }); - - it('should call the blockaid transaction scan', async () => { - const service = new BlockaidService(featureFlagService); - const transactionScanSpy = jest.spyOn(service, 'transactionScan'); - await service.parseTransaction('core.test', networkMock, txMock); - expect(transactionScanSpy).toHaveBeenCalled(); - }); - - it('should return the `isMalicious` as true', async () => { - const service = new BlockaidService(featureFlagService); - const result = await service.parseTransaction( - 'core.test', - networkMock, - txMock - ); - expect(result).toMatchObject({ - isMalicious: true, - isSuspicious: false, - preExecSuccess: true, - }); - }); it('should return `null` because the `JSONRPC SCAN` feature flag is disabled', async () => { featureFlagService = { featureFlags: { @@ -206,65 +163,4 @@ describe('background/services/blockaid/BlockaidService', () => { status: 'Success', }); }); - - it('should return the `isMalicious` as true', async () => { - const service = new BlockaidService(featureFlagService); - const result = await service.parseTransaction( - 'core.test', - networkMock, - txMock - ); - expect(result).toMatchObject({ - isMalicious: true, - isSuspicious: false, - preExecSuccess: true, - }); - }); - - it('should parse the token data correctly', async () => { - const service = new BlockaidService(featureFlagService); - const result = await service.parseTransaction( - 'core.test', - networkMock, - txMock - ); - expect(result).toMatchObject({ - fromAddress: 'fromAddress', - balanceChange: { - usdValueChange: 1, - sendTokenList: [ - { - usdValue: 7.266, - amount: BigInt(rawValue), - symbol: tokenDetails.symbol, - name: tokenDetails.name, - decimals: tokenDetails.decimals, - address: 'avax', - }, - ], - }, - }); - }); - - it('should parse the nft data correctly', async () => { - const service = new BlockaidService(featureFlagService); - const result = await service.parseTransaction( - 'core.test', - networkMock, - txMock - ); - expect(result).toMatchObject({ - fromAddress: 'fromAddress', - balanceChange: { - sendNftList: [ - { - amount: BigInt(nftValue), - symbol: nftDetails.symbol, - name: nftDetails.name, - address: nftDetails.address, - }, - ], - }, - }); - }); }); diff --git a/src/background/services/blockaid/BlockaidService.ts b/src/background/services/blockaid/BlockaidService.ts index e575e852b..3349f9a61 100644 --- a/src/background/services/blockaid/BlockaidService.ts +++ b/src/background/services/blockaid/BlockaidService.ts @@ -1,26 +1,9 @@ import { singleton } from 'tsyringe'; import { FeatureFlagService } from '../featureFlags/FeatureFlagService'; import Blockaid from '@blockaid/client'; -import { Network } from '@avalabs/core-chains-sdk'; -import { - EthSendTransactionParamsWithGas, - TransactionAction, - TransactionDisplayValues, - TransactionNft, - TransactionToken, - TransactionType, -} from '../wallet/handlers/eth_sendTransaction/models'; -import { - NftDetails, - TokenDetails, - getValidationResultType, - isNft, - isToken, -} from './utils'; import { FeatureGates } from '../featureFlags/models'; import { JsonRpcRequestPayload } from '@src/background/connections/dAppConnection/models'; import { MessageType } from '../messages/models'; -import { TokenType } from '@avalabs/vm-module-types'; @singleton() export class BlockaidService { @@ -35,63 +18,6 @@ export class BlockaidService { }); } - parseTokenData(token: TokenDetails): TransactionToken | undefined { - if (token.type === 'ERC20') { - return { - address: token.address, - decimals: token.decimals, - logoUri: token.logo_url, - symbol: token.symbol || 'N/A', - name: token.name || 'N/A', - }; - } - if (token.type === 'NATIVE') { - return { - address: token.symbol?.toLowerCase() || 'avax', - decimals: token.decimals, - logoUri: token.logo_url, - symbol: token.symbol || 'N/A', - name: token.name || 'N/A', - }; - } - } - - parseNftData(nft: NftDetails): TransactionNft | undefined { - if (nft.type === 'ERC1155') { - return { - type: TokenType.ERC1155, - address: nft.address, - amount: BigInt(0), - logoUri: nft.logo_url, - symbol: nft.symbol, - name: nft.name || '', - description: '', - }; - } - if (nft.type === 'ERC721') { - return { - type: TokenType.ERC721, - address: nft.address, - amount: BigInt(0), - logoUri: nft.logo_url, - symbol: nft.symbol, - name: nft.name || '', - description: '', - }; - } - if (nft.type === 'NONERC') { - return { - type: TokenType.ERC721, - address: nft.address, - amount: BigInt(0), - logoUri: nft.logo_url, - symbol: nft.symbol, - name: nft.name || '', - description: '', - }; - } - } - async jsonRPCScan( chainId: string, from: string, @@ -116,216 +42,4 @@ export class BlockaidService { return null; } } - - async transactionScan( - tx: EthSendTransactionParamsWithGas, - chainId: string, - domain: string - ) { - if ( - !this.featureFlagService.featureFlags[ - FeatureGates.BLOCKAID_TRANSACTION_SCAN - ] - ) { - return null; - } - return await this.#blockaid.evm.transaction.scan({ - account_address: tx.from, - chain: chainId, - options: ['validation', 'simulation'], - data: { - from: tx.from, - to: tx.to, - data: tx.data, - // BigInt cannot be JSON-stringified - value: - typeof tx.value === 'bigint' - ? `0x${tx.value.toString(16)}` - : tx.value, - }, - metadata: { domain }, - }); - } - - #collectTokenLists(senderAssetDiff: Blockaid.Evm.AssetDiff[]) { - const sendTokenList: TransactionToken[] = []; - const receiveTokenList: TransactionToken[] = []; - const sendNftList: TransactionNft[] = []; - const receiveNftList: TransactionNft[] = []; - - for (const diffData of senderAssetDiff) { - let token: TransactionToken | undefined = undefined; - let nft: TransactionNft | undefined = undefined; - if (isToken(diffData.asset)) { - token = this.parseTokenData(diffData.asset); - } - if (isNft(diffData.asset)) { - nft = this.parseNftData(diffData.asset); - } - - if (diffData.in.length && (token || nft)) { - for (const assetDiff of diffData.in) { - let value: bigint = BigInt(0); - if (assetDiff && 'raw_value' in assetDiff) { - value = BigInt(assetDiff.raw_value); - } else if (assetDiff && 'value' in assetDiff) { - value = BigInt(assetDiff.value); - } - token && - receiveTokenList.push({ - ...token, - usdValue: Number(assetDiff.usd_price), - amount: value, - }); - nft && - receiveNftList.push({ - ...nft, - amount: value, - size: diffData.in.length, - }); - } - } - - if (diffData.out.length && (token || nft)) { - for (const assetDiff of diffData.out) { - let value: bigint = BigInt(0); - if (assetDiff && 'raw_value' in assetDiff) { - value = BigInt(assetDiff.raw_value); - } else if (assetDiff && 'value' in assetDiff) { - value = BigInt(assetDiff.value); - } - token && - sendTokenList.push({ - ...token, - usdValue: Number(assetDiff.usd_price), - amount: value, - }); - nft && - sendNftList.push({ - ...nft, - amount: value, - size: diffData.out.length, - }); - } - } - } - return { sendTokenList, receiveTokenList, sendNftList, receiveNftList }; - } - - #parseTransactionAction( - exposures: Blockaid.Evm.AddressAssetExposure[], - txFrom: string - ) { - const actions: TransactionAction[] = []; - - for (const exposure of exposures) { - let token: TransactionToken | undefined = undefined; - let nft: TransactionNft | undefined = undefined; - if (isToken(exposure.asset)) { - token = this.parseTokenData(exposure.asset); - } - if (isNft(exposure.asset)) { - nft = this.parseNftData(exposure.asset); - } - for (const spenderId in exposure.spenders) { - const spender = exposure.spenders[spenderId]; - if (spender && 'approval' in spender && token) { - actions.push({ - type: TransactionType.APPROVE_TOKEN, - spender: { - address: spenderId, - }, - token: { ...token, amount: BigInt(spender.approval) }, - }); - } - if (spender && 'approval' in spender && nft) { - actions.push({ - type: TransactionType.APPROVE_NFT, - owner: txFrom, - spender: { - address: spenderId, - }, - token: { ...nft, amount: BigInt(spender.approval) }, - }); - } - } - } - return { actions }; - } - - async parseTransaction( - domain: string, - network: Network, - tx: EthSendTransactionParamsWithGas - ): Promise { - const response = await this.transactionScan( - tx, - network.chainId.toString(), - domain - ); - - if (!response) { - throw new Error('The transaction scanning is disabled'); - } - - const { simulation, validation } = response; - if (simulation?.status !== 'Success') { - throw new Error('Transaction simulation unsuccessful'); - } - - const senderAssetDiff = simulation.account_summary.assets_diffs; - const { sendTokenList, receiveTokenList, sendNftList, receiveNftList } = - this.#collectTokenLists(senderAssetDiff); - - let actions: TransactionAction[] = []; - if ( - !sendTokenList.length && - !receiveTokenList.length && - !sendNftList.length && - !receiveNftList.length - ) { - const parsedData = this.#parseTransactionAction( - simulation.account_summary.exposures, - tx.from - ); - actions = parsedData.actions; - } - - const displayValues: TransactionDisplayValues = { - fromAddress: tx.from, - balanceChange: { - usdValueChange: Number( - simulation.account_summary.total_usd_diff?.total - ), - sendTokenList: [...sendTokenList], - receiveTokenList, - sendNftList: [...sendNftList], - receiveNftList, - }, - isMalicious: getValidationResultType(validation).isMalicious, - isSuspicious: getValidationResultType(validation).isSuspicious, - preExecSuccess: simulation.status === 'Success', - gas: { - maxPriorityFeePerGas: tx.maxPriorityFeePerGas - ? BigInt(tx.maxPriorityFeePerGas) - : undefined, - maxFeePerGas: BigInt(tx.maxFeePerGas), - gasLimit: Number(tx.gasLimit), - recommendedGasLimit: undefined, - }, - abi: undefined, - actions: actions.length - ? actions - : [ - { - type: TransactionType.CALL, - fromAddress: tx.from, - contract: { - address: tx.to ?? '', - }, - }, - ], - }; - return displayValues; - } } diff --git a/src/background/services/network/NetworkService.test.ts b/src/background/services/network/NetworkService.test.ts index b8e0b337e..42f7c7545 100644 --- a/src/background/services/network/NetworkService.test.ts +++ b/src/background/services/network/NetworkService.test.ts @@ -157,6 +157,32 @@ describe('background/services/network/NetworkService', () => { process.env = env; }); + describe('.getNetwork()', () => { + beforeEach(() => { + jest.spyOn(service.allNetworks, 'promisify').mockResolvedValue( + Promise.resolve({ + [ethMainnet.chainId]: ethMainnet, + [avaxMainnet.chainId]: avaxMainnet, + }) + ); + }); + + it('works with hexadecimal chain ids', async () => { + expect(await service.getNetwork('0x1')).toEqual(ethMainnet); + expect(await service.getNetwork('0xa86a')).toEqual(avaxMainnet); + }); + + it('works with numeric chain ids', async () => { + expect(await service.getNetwork(1)).toEqual(ethMainnet); + expect(await service.getNetwork(43114)).toEqual(avaxMainnet); + }); + + it('works with caip ids', async () => { + expect(await service.getNetwork('eip155:1')).toEqual(ethMainnet); + expect(await service.getNetwork('eip155:43114')).toEqual(avaxMainnet); + }); + }); + describe('.getInitialNetworkForDapp()', () => { const chainlist = { [ethMainnet.chainId]: ethMainnet, diff --git a/src/background/services/network/NetworkService.ts b/src/background/services/network/NetworkService.ts index 876104c0b..f3f4aa29e 100644 --- a/src/background/services/network/NetworkService.ts +++ b/src/background/services/network/NetworkService.ts @@ -466,7 +466,9 @@ export class NetworkService implements OnLock, OnStorageReady { ): Promise { const chainId = typeof scopeOrChainId === 'string' - ? caipToChainId(scopeOrChainId) + ? scopeOrChainId.startsWith('0x') + ? Number(scopeOrChainId) + : caipToChainId(scopeOrChainId) : scopeOrChainId; const activeNetworks = await this.allNetworks.promisify(); diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidity.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidity.ts deleted file mode 100644 index de4d9c427..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidity.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { findToken } from '../../../../../../utils/findToken'; -import { Network } from '@avalabs/core-chains-sdk'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { TransactionDescription } from 'ethers'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -export interface AddLiquidityData { - amountAMin: bigint; - amountADesired: bigint; - amountBMin: bigint; - amountBDesired: bigint; - contractCall: ContractCall.ADD_LIQUIDITY; - deadline: string; - tokenA: string; - tokenB: string; - to: string; -} - -export async function addLiquidityHandler( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: AddLiquidityData, - txDetails: TransactionDescription | null -): Promise { - const tokenA = (await findToken(data.tokenA.toLowerCase(), network)) as - | TokenWithBalanceERC20 - | NetworkTokenWithBalance; - const tokenB = (await findToken(data.tokenB.toLowerCase(), network)) as - | TokenWithBalanceERC20 - | NetworkTokenWithBalance; - - const sendTokenList: TransactionToken[] = []; - - sendTokenList.push({ - address: tokenA.type === TokenType.ERC20 ? tokenA.address : tokenA.symbol, - decimals: tokenA.decimals, - symbol: tokenA.symbol, - name: tokenA.name, - logoUri: tokenA.logoUri, - - amount: BigInt(data.amountADesired), - usdValue: - tokenA.priceInCurrency !== undefined - ? Number(tokenA.priceInCurrency) * - bigintToBig(data.amountADesired, tokenA.decimals).toNumber() - : undefined, - usdPrice: tokenA.priceInCurrency, - }); - - sendTokenList.push({ - address: tokenB.type === TokenType.ERC20 ? tokenB.address : tokenB.symbol, - decimals: tokenB.decimals, - symbol: tokenB.symbol, - name: tokenB.name, - logoUri: tokenB.logoUri, - - amount: BigInt(data.amountBDesired), - usdValue: - tokenB.priceInCurrency !== undefined - ? Number(tokenB.priceInCurrency) * - bigintToBig(data.amountBDesired, tokenB.decimals).toNumber() - : undefined, - usdPrice: tokenB.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - - result.balanceChange.sendTokenList = [ - ...result.balanceChange.sendTokenList, - ...sendTokenList, - ]; - - return result; -} - -export const AddLiquidityParser: ContractParser = [ - ContractCall.ADD_LIQUIDITY, - addLiquidityHandler, -]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidityAVAX.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidityAVAX.ts deleted file mode 100644 index 6e508a8e4..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/addLiquidityAVAX.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { Network } from '@avalabs/core-chains-sdk'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { TransactionDescription } from 'ethers'; -import { findToken } from '../../../../../../utils/findToken'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -export interface AddLiquidityAvaxData { - amountAVAXMin: bigint; - amountTokenDesired: bigint; - amountTokenMin: bigint; - contractCall: ContractCall.ADD_LIQUIDITY_AVAX; - deadline: string; - token: string; - to: string; -} - -export async function addLiquidityAvaxHandler( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: AddLiquidityAvaxData, - txDetails: TransactionDescription | null -): Promise { - const token = (await findToken(data.token.toLowerCase(), network)) as - | NetworkTokenWithBalance - | TokenWithBalanceERC20; - const sendTokenList: TransactionToken[] = []; - - sendTokenList.push({ - address: token.type === TokenType.ERC20 ? token.address : token.symbol, - decimals: token.decimals, - symbol: token.symbol, - name: token.name, - logoUri: token.logoUri, - - amount: BigInt(data.amountTokenDesired), - usdValue: - token.priceInCurrency !== undefined - ? Number(token.priceInCurrency) * - bigintToBig(data.amountTokenDesired, token.decimals).toNumber() - : undefined, - usdPrice: token.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - - result.balanceChange.sendTokenList = [ - ...result.balanceChange.sendTokenList, - ...sendTokenList, - ]; - - return result; -} - -export const AddLiquidityAvaxParser: ContractParser = [ - ContractCall.ADD_LIQUIDITY_AVAX, - addLiquidityAvaxHandler, -]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/approve.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/approve.ts deleted file mode 100644 index 136663d2f..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/approve.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Network } from '@avalabs/core-chains-sdk'; -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { ContractCall, ContractParser } from './models'; -import { findToken } from '../../../../../../utils/findToken'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { TransactionDescription } from 'ethers'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -type ApproveData = { - spender: string; - amount?: string; - rawAmount?: string; -}; - -export async function approveTxHandler( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: ApproveData, - txDetails: TransactionDescription | null -): Promise { - if (!request.to) { - throw new Error('Contract address not defined'); - } - const tokenToBeApproved = (await findToken( - request.to.toLowerCase(), - network - )) as TokenWithBalanceERC20 | NetworkTokenWithBalance; - - const displayData = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - const amount = data.amount ?? data.rawAmount; - - displayData.actions.push({ - type: TransactionType.APPROVE_TOKEN, - token: { - address: - tokenToBeApproved.type === TokenType.ERC20 - ? tokenToBeApproved.address - : tokenToBeApproved.symbol, - decimals: tokenToBeApproved.decimals, - symbol: tokenToBeApproved.symbol, - name: tokenToBeApproved.name, - logoUri: tokenToBeApproved.logoUri, - - amount: amount ? BigInt(amount) : undefined, - usdValue: - amount && tokenToBeApproved.priceInCurrency - ? Number(tokenToBeApproved.priceInCurrency) * - bigintToBig(BigInt(amount), tokenToBeApproved.decimals).toNumber() - : undefined, - usdPrice: tokenToBeApproved.priceInCurrency, - }, - spender: { - address: data.spender, - }, - }); - - return displayData; -} - -export const ApproveTxParser: ContractParser = [ - ContractCall.APPROVE, - approveTxHandler, -]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/contractParserMap.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/contractParserMap.ts deleted file mode 100644 index 025f546dd..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/contractParserMap.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AddLiquidityParser } from './addLiquidity'; -import { AddLiquidityAvaxParser } from './addLiquidityAVAX'; -import { ApproveTxParser } from './approve'; -import { ContractParserHandler } from './models'; -import { - SwapAvaxForExactTokensParser, - SwapExactAvaxForTokensParser, -} from './swapAvaxForExactTokens'; -import { SwapExactTokensForAvaxParser } from './swapExactTokensForAVAX'; -import { - SwapExactTokensForTokenParser, - SwapTokensForExactTokensParser, -} from './swapExactTokensForTokens'; -import { SimpleSwapParser } from './simpleSwap'; - -export const contractParserMap = new Map>([ - SwapExactTokensForTokenParser, - SwapTokensForExactTokensParser, - SwapAvaxForExactTokensParser, - SwapExactAvaxForTokensParser, - SwapExactTokensForAvaxParser, - ApproveTxParser, - AddLiquidityAvaxParser, - AddLiquidityParser, - SimpleSwapParser, -]); diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/models.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/models.ts deleted file mode 100644 index eb7fcb723..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/models.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Network } from '@avalabs/core-chains-sdk'; -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { Result, TransactionDescription } from 'ethers'; - -export type ContractParserHandler = ( - network: Network, - request: EthSendTransactionParamsWithGas, - data: ParsedData, - txDetails: TransactionDescription | null -) => Promise; -export type ContractParser = [ContractCall, ContractParserHandler]; - -export enum ContractCall { - APPROVE = 'approve', - SWAP_EXACT_TOKENS_FOR_TOKENS = 'swapExactTokensForTokens', - SWAP_TOKENS_FOR_EXACT_TOKENS = 'swapTokensForExactTokens', - SWAP_AVAX_FOR_EXACT_TOKENS = 'swapAVAXForExactTokens', - SWAP_EXACT_TOKENS_FOR_AVAX = 'swapExactTokensForAVAX', - SWAP_EXACT_AVAX_FOR_TOKENS = 'swapExactAVAXForTokens', - ADD_LIQUIDITY = 'addLiquidity', - ADD_LIQUIDITY_AVAX = 'addLiquidityAVAX', - SIMPLE_SWAP = 'simpleSwap', -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/parseWithERC20Abi.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/parseWithERC20Abi.ts deleted file mode 100644 index cbe498a17..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/parseWithERC20Abi.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json'; -import { ethers } from 'ethers'; -import { NetworkContractToken } from '@avalabs/core-chains-sdk'; - -export function parseWithERC20Abi( - tx: EthSendTransactionParamsWithGas, - token: NetworkContractToken -): TransactionDisplayValues { - if (!tx.data) { - throw new Error('Invalid input'); - } - const iface = new ethers.Interface(ERC20.abi); - const calledFunction = iface.getFunction(tx.data.slice(0, 10)); - - if (!calledFunction) { - throw new Error('Unable to get function'); - } - - const decodeFunctionData = iface.decodeFunctionData( - tx.data.slice(0, 10), - tx.data - ); - - const displayData: TransactionDisplayValues = { - fromAddress: tx.from, - abi: { - func: calledFunction?.name ?? '', - params: decodeFunctionData.toArray(), - }, - actions: [], - balanceChange: { - sendTokenList: [], - sendNftList: [], - receiveNftList: [], - receiveTokenList: [], - }, - gas: { - maxFeePerGas: BigInt(tx.maxFeePerGas), - gasLimit: Number(tx.gasLimit), - }, - preExecSuccess: false, - }; - - if (calledFunction?.name === 'transfer') { - displayData.actions.push({ - type: TransactionType.SEND_TOKEN, - fromAddress: tx.from, - toAddress: decodeFunctionData['to'], - token: { - address: token.address, - decimals: token.decimals, - symbol: token.symbol, - name: token.name, - logoUri: token.logoUri, - - amount: BigInt(decodeFunctionData['amount']), - }, - }); - } else if (calledFunction?.name === 'approve') { - displayData.actions.push({ - type: TransactionType.APPROVE_TOKEN, - spender: { - address: decodeFunctionData['spender'], - }, - token: { - address: token.address, - decimals: token.decimals, - symbol: token.symbol, - name: token.name, - logoUri: token.logoUri, - - amount: BigInt(decodeFunctionData['amount']), - }, - }); - } else { - displayData.actions.push({ - type: TransactionType.CALL, - fromAddress: tx.from, - contract: { - address: token.address, - }, - }); - } - - return displayData; -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/simpleSwap.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/simpleSwap.ts deleted file mode 100644 index bcd22ece4..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/simpleSwap.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { findToken } from '../../../../../../utils/findToken'; -import { Network } from '@avalabs/core-chains-sdk'; -import { TransactionDescription } from 'ethers'; -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { isNetworkToken } from './utils/helpers'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; -export interface SimpleSwapData { - data: { - beneficiary: string; - callees: string[]; - deadline: bigint; - exchangeData: string; - exchangeAmount: bigint; - feePercent: bigint; - fromAmount: bigint; - fromToken: string; - partner: string; - permint: string; - startIndexes: bigint[]; - toAmount: bigint; - toToken: string; - uuid: string; - }; -} - -export async function simpleSwapHandler( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - { data }: SimpleSwapData, - txDetails: TransactionDescription | null -): Promise { - const fromToken = ( - isNetworkToken(data.fromToken) - ? await findToken(network.networkToken.symbol, network) - : await findToken(data.fromToken, network) - ) as NetworkTokenWithBalance | TokenWithBalanceERC20; - const toToken = ( - isNetworkToken(data.toToken) - ? await findToken(network.networkToken.symbol, network) - : await findToken(data.toToken, network) - ) as NetworkTokenWithBalance | TokenWithBalanceERC20; - - const sendTokenList: TransactionToken[] = []; - const receiveTokenList: TransactionToken[] = []; - - sendTokenList.push({ - address: - fromToken.type === TokenType.NATIVE - ? fromToken.symbol - : fromToken.address, - decimals: fromToken.decimals, - symbol: fromToken.symbol, - name: fromToken.name, - logoUri: fromToken.logoUri, - - amount: BigInt(data.fromAmount), - usdValue: fromToken.priceInCurrency - ? Number(fromToken.priceInCurrency) * - bigintToBig(data.fromAmount, fromToken.decimals).toNumber() - : undefined, - usdPrice: fromToken.priceInCurrency, - }); - - receiveTokenList.push({ - address: - toToken.type === TokenType.NATIVE ? toToken.symbol : toToken.address, - decimals: toToken.decimals, - symbol: toToken.symbol, - name: toToken.name, - logoUri: toToken.logoUri, - - amount: BigInt(data.toAmount), - usdValue: toToken.priceInCurrency - ? Number(toToken.priceInCurrency) * - bigintToBig(data.fromAmount, toToken.decimals).toNumber() - : undefined, - usdPrice: toToken.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - - result.balanceChange.sendTokenList = [ - ...result.balanceChange.sendTokenList, - ...sendTokenList, - ]; - - result.balanceChange.receiveTokenList = [ - ...result.balanceChange.receiveTokenList, - ...receiveTokenList, - ]; - - return result; -} - -export const SimpleSwapParser: ContractParser = [ - ContractCall.SIMPLE_SWAP, - simpleSwapHandler, -]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapAvaxForExactTokens.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapAvaxForExactTokens.ts deleted file mode 100644 index 9fcd615da..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapAvaxForExactTokens.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { findToken } from '../../../../../../utils/findToken'; -import { Network } from '@avalabs/core-chains-sdk'; -import { TransactionDescription } from 'ethers'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -export interface SwapAVAXForExactTokensData { - /** - * Depending on function call one of these amounts will be truthy - */ - amountOutMin: bigint; - amountOut: bigint; - contractCall: ContractCall.SWAP_EXACT_TOKENS_FOR_TOKENS; - deadline: string; - path: string[]; - to: string; -} - -export async function swapAVAXForExactTokens( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: SwapAVAXForExactTokensData, - txDetails: TransactionDescription | null -): Promise { - const lastTokenInPath = (await findToken( - data.path[data.path.length - 1]?.toLowerCase() || '', - network - )) as TokenWithBalanceERC20 | NetworkTokenWithBalance; - - const receiveTokenList: TransactionToken[] = []; - - receiveTokenList.push({ - address: - lastTokenInPath.type === TokenType.ERC20 - ? lastTokenInPath.address - : lastTokenInPath.symbol, - decimals: lastTokenInPath.decimals, - symbol: lastTokenInPath.symbol, - name: lastTokenInPath.name, - logoUri: lastTokenInPath.logoUri, - - amount: BigInt(data.amountOut || data.amountOutMin), - usdValue: lastTokenInPath.priceInCurrency - ? Number(lastTokenInPath.priceInCurrency) * - bigintToBig( - data.amountOut || data.amountOutMin, - lastTokenInPath.decimals - ).toNumber() - : undefined, - usdPrice: lastTokenInPath.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - - result.balanceChange.receiveTokenList = [ - ...result.balanceChange.receiveTokenList, - ...receiveTokenList, - ]; - - return result; -} - -export const SwapAvaxForExactTokensParser: ContractParser = - [ContractCall.SWAP_AVAX_FOR_EXACT_TOKENS, swapAVAXForExactTokens]; - -export const SwapExactAvaxForTokensParser: ContractParser = - [ContractCall.SWAP_EXACT_AVAX_FOR_TOKENS, swapAVAXForExactTokens]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForAVAX.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForAVAX.ts deleted file mode 100644 index 013ecc952..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForAVAX.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { findToken } from '../../../../../../utils/findToken'; -import { Network } from '@avalabs/core-chains-sdk'; -import { TransactionDescription } from 'ethers'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; -export interface SwapExactTokensForAVAXData { - amountOutMin: bigint; - amountIn: bigint; - contractCall: ContractCall.SWAP_EXACT_TOKENS_FOR_TOKENS; - deadline: string; - path: string[]; - to: string; -} - -export async function swapExactTokensForAvax( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: SwapExactTokensForAVAXData, - txDetails: TransactionDescription | null -): Promise { - const firstTokenInPath = (await findToken( - data.path[0]?.toLowerCase() || '', - network - )) as NetworkTokenWithBalance | TokenWithBalanceERC20; - const networkTokenWithBalance = (await findToken( - network.networkToken.symbol, - network - )) as NetworkTokenWithBalance; - - const sendTokenList: TransactionToken[] = []; - - sendTokenList.push({ - address: - firstTokenInPath.type === TokenType.NATIVE - ? firstTokenInPath.symbol - : firstTokenInPath.address, - decimals: firstTokenInPath.decimals, - symbol: firstTokenInPath.symbol, - name: firstTokenInPath.name, - logoUri: firstTokenInPath.logoUri, - - amount: data.amountIn ? BigInt(data.amountIn) : undefined, - usdValue: - data.amountIn && firstTokenInPath.priceInCurrency - ? Number(firstTokenInPath.priceInCurrency) * - bigintToBig(data.amountIn, firstTokenInPath.decimals).toNumber() - : undefined, - usdPrice: firstTokenInPath.priceInCurrency, - }); - - const receiveTokenList: TransactionToken[] = []; - - receiveTokenList.push({ - address: networkTokenWithBalance.symbol, - decimals: networkTokenWithBalance.decimals, - symbol: networkTokenWithBalance.symbol, - name: networkTokenWithBalance.name, - logoUri: networkTokenWithBalance.logoUri, - - amount: data.amountOutMin ? BigInt(data.amountOutMin) : undefined, - usdValue: - data.amountOutMin && networkTokenWithBalance.priceInCurrency - ? networkTokenWithBalance.priceInCurrency * - bigintToBig( - data.amountOutMin, - networkTokenWithBalance.decimals - ).toNumber() - : undefined, - usdPrice: networkTokenWithBalance.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - result.balanceChange.sendTokenList = [ - ...result.balanceChange.sendTokenList, - ...sendTokenList, - ]; - - result.balanceChange.receiveTokenList = [ - ...result.balanceChange.receiveTokenList, - ...receiveTokenList, - ]; - return result; -} - -export const SwapExactTokensForAvaxParser: ContractParser = - [ContractCall.SWAP_EXACT_TOKENS_FOR_AVAX, swapExactTokensForAvax]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForTokens.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForTokens.ts deleted file mode 100644 index 4d8a6d80d..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/swapExactTokensForTokens.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { ContractCall, ContractParser } from './models'; -import { parseBasicDisplayValues } from './utils/parseBasicDisplayValues'; -import { findToken } from '../../../../../../utils/findToken'; -import { Network } from '@avalabs/core-chains-sdk'; -import { TransactionDescription } from 'ethers'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -export interface SwapExactTokensForTokenData { - amountInMin: bigint; - amountIn: bigint; - amountInMax: bigint; - - amountOutMin: bigint; - amountOut: bigint; - amountOutMax: bigint; - - contractCall: ContractCall.SWAP_EXACT_TOKENS_FOR_TOKENS; - deadline: string; - path: string[]; - to: string; -} - -export async function swapExactTokensForTokenHandler( - network: Network, - /** - * The from on request represents the wallet and the to represents the contract - */ - request: EthSendTransactionParamsWithGas, - /** - * Data is the values sent to the above contract and this is the instructions on how to - * execute - */ - data: SwapExactTokensForTokenData, - txDetails: TransactionDescription | null -): Promise { - const firstTokenInPath = (await findToken( - data.path[0]?.toLowerCase() || '', - network - )) as NetworkTokenWithBalance | TokenWithBalanceERC20; - const lastTokenInPath = (await findToken( - data.path[data.path.length - 1]?.toLowerCase() || '', - network - )) as NetworkTokenWithBalance | TokenWithBalanceERC20; - - const sendTokenList: TransactionToken[] = []; - const inAmount = data.amountIn || data.amountInMin || data.amountInMax; - sendTokenList.push({ - address: - firstTokenInPath.type === TokenType.ERC20 - ? firstTokenInPath.address - : firstTokenInPath.symbol, - decimals: firstTokenInPath.decimals, - symbol: firstTokenInPath.symbol, - name: firstTokenInPath.name, - logoUri: firstTokenInPath.logoUri, - - amount: inAmount ? BigInt(inAmount) : undefined, - usdValue: - inAmount && firstTokenInPath.priceInCurrency - ? Number(firstTokenInPath.priceInCurrency) * - bigintToBig(inAmount, firstTokenInPath.priceInCurrency).toNumber() - : undefined, - usdPrice: firstTokenInPath.priceInCurrency, - }); - - const receiveTokenList: TransactionToken[] = []; - const outAmout = data.amountOut || data.amountOutMin || data.amountOutMax; - receiveTokenList.push({ - address: - lastTokenInPath.type === TokenType.ERC20 - ? lastTokenInPath.address - : lastTokenInPath.symbol, - decimals: lastTokenInPath.decimals, - symbol: lastTokenInPath.symbol, - name: lastTokenInPath.name, - logoUri: lastTokenInPath.logoUri, - - amount: outAmout ? BigInt(outAmout) : undefined, - usdValue: - outAmout && lastTokenInPath.priceInCurrency - ? Number(lastTokenInPath.priceInCurrency) * - bigintToBig(outAmout, lastTokenInPath.decimals).toNumber() - : undefined, - usdPrice: lastTokenInPath.priceInCurrency, - }); - - const result: TransactionDisplayValues = await parseBasicDisplayValues( - network, - request, - txDetails - ); - - result.actions.push({ - type: TransactionType.CALL, - fromAddress: request.from, - contract: { - address: request.to ?? '', - }, - }); - result.balanceChange = result.balanceChange ?? { - sendTokenList: [], - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }; - result.balanceChange.sendTokenList = [ - ...result.balanceChange.sendTokenList, - ...sendTokenList, - ]; - - result.balanceChange.receiveTokenList = [ - ...result.balanceChange.receiveTokenList, - ...receiveTokenList, - ]; - - return result; -} - -export const SwapExactTokensForTokenParser: ContractParser = - [ContractCall.SWAP_EXACT_TOKENS_FOR_TOKENS, swapExactTokensForTokenHandler]; - -/** - * This is for swaps from a token into a stable coin, same logic - * its just telling the contract that the latter token needs to be - * exact amount - */ -export const SwapTokensForExactTokensParser: ContractParser = - [ContractCall.SWAP_TOKENS_FOR_EXACT_TOKENS, swapExactTokensForTokenHandler]; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/helpers.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/helpers.ts deleted file mode 100644 index b228a37fc..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isNetworkToken(address: string) { - return address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/parseBasicDisplayValues.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/parseBasicDisplayValues.ts deleted file mode 100644 index f0c67bca4..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/contractParsers/utils/parseBasicDisplayValues.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { calculateGasAndFees } from '@src/utils/calculateGasAndFees'; -import { Network } from '@avalabs/core-chains-sdk'; -import { TransactionDescription } from 'ethers'; -import { - EthSendTransactionParamsWithGas, - TransactionDisplayValues, - TransactionToken, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { findToken } from '../../../../../../../utils/findToken'; -import { TokenWithBalanceEVM } from '@avalabs/vm-module-types'; - -export async function parseBasicDisplayValues( - network: Network, - request: EthSendTransactionParamsWithGas, - description: TransactionDescription | null -): Promise { - const networkTokenWithBalance = (await findToken( - network.networkToken.symbol, - network - )) as TokenWithBalanceEVM; - const name = description?.name; - - const sendTokenList: TransactionToken[] = []; - - if (request.value && BigInt(request.value) > 0n) { - sendTokenList.push({ - address: network.networkToken.symbol, - decimals: network.networkToken.decimals, - symbol: network.networkToken.symbol, - name: network.networkToken.name, - logoUri: network.networkToken.logoUri, - amount: BigInt(request.value), - usdValue: networkTokenWithBalance.priceInCurrency - ? networkTokenWithBalance.priceInCurrency * - bigintToBig( - BigInt(request.value), - network.networkToken.decimals - ).toNumber() - : undefined, - usdPrice: networkTokenWithBalance.priceInCurrency, - }); - } - const gasInfo = calculateGasAndFees({ - maxFeePerGas: BigInt(request.maxFeePerGas), - gasLimit: Number(request.gasLimit), - tokenPrice: networkTokenWithBalance.priceInCurrency, - tokenDecimals: network.networkToken.decimals, - }); - - return { - /** - * The wallet this is being sent from - */ - fromAddress: request.from, - - gas: { - maxFeePerGas: BigInt(gasInfo.maxFeePerGas ?? 0), - gasLimit: gasInfo.gasLimit, - }, - abi: description - ? { - func: name ? (name[0] || '').toUpperCase() + name.slice(1) : '', - params: description?.args.toArray() ?? [], - } - : undefined, - actions: [], - balanceChange: { - sendTokenList, - receiveTokenList: [], - sendNftList: [], - receiveNftList: [], - }, - }; -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/utils/dataParser.ts b/src/background/services/wallet/handlers/eth_sendTransaction/contracts/utils/dataParser.ts deleted file mode 100644 index 8d08ca235..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/contracts/utils/dataParser.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function dataParser(data: any) { - return (data.params as any[]).reduce( - (acc, prop) => { - return { - ...acc, - [prop.name]: prop.value, - }; - }, - { contractCall: data.name } - ); -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts deleted file mode 100644 index b71683a09..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts +++ /dev/null @@ -1,1261 +0,0 @@ -import { NetworkService } from '@src/background/services/network/NetworkService'; -import { EthSendTransactionHandler } from './eth_sendTransaction'; -import { NetworkFeeService } from '@src/background/services/networkFee/NetworkFeeService'; -import { BalanceAggregatorService } from '@src/background/services/balances/BalanceAggregatorService'; -import { AccountsService } from '@src/background/services/accounts/AccountsService'; -import { TokenManagerService } from '@src/background/services/tokens/TokenManagerService'; -import { AnalyticsServicePosthog } from '@src/background/services/analytics/AnalyticsServicePosthog'; -import { WalletService } from '@src/background/services/wallet/WalletService'; -import { - EthSendTransactionParams, - Transaction, - TransactionDisplayValues, - TransactionType, -} from './models'; -import { ethErrors } from 'eth-rpc-errors'; -import { AccountType } from '@src/background/services/accounts/models'; -import { NetworkContractToken, NetworkVMType } from '@avalabs/core-chains-sdk'; -import { JsonRpcBatchInternal } from '@avalabs/core-wallets-sdk'; -import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitcoinNetwork'; -import getTargetNetworkForTx from './utils/getTargetNetworkForTx'; -import { encryptAnalyticsData } from '@src/background/services/analytics/utils/encryptAnalyticsData'; -import { getTxDescription } from './utils/getTxDescription'; -import { parseWithERC20Abi } from './contracts/contractParsers/parseWithERC20Abi'; -import { contractParserMap } from './contracts/contractParsers/contractParserMap'; -import { TransactionDescription } from 'ethers'; -import { parseBasicDisplayValues } from './contracts/contractParsers/utils/parseBasicDisplayValues'; -import { Action, ActionStatus } from '@src/background/services/actions/models'; -import browser from 'webextension-polyfill'; -import { txToCustomEvmTx } from './utils/txToCustomEvmTx'; -import { getExplorerAddressByNetwork } from '@src/utils/getExplorerAddress'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; -import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -import { buildRpcCall } from '@src/tests/test-utils'; -import { BlockaidService } from '@src/background/services/blockaid/BlockaidService'; -import { caipToChainId } from '@src/utils/caipConversion'; -import { measureDuration } from '@src/utils/measureDuration'; -import { LockService } from '@src/background/services/lock/LockService'; - -jest.mock('@src/utils/caipConversion'); -jest.mock('@src/background/runtime/openApprovalWindow'); -jest.mock('@src/utils/network/getProviderForNetwork'); -jest.mock('@src/background/services/analytics/AnalyticsServicePosthog'); -jest.mock('@src/background/services/tokens/TokenManagerService'); -jest.mock('@src/background/services/networkFee/NetworkFeeService'); -jest.mock('@src/background/services/network/NetworkService'); -jest.mock('@src/background/services/balances/BalanceAggregatorService'); -jest.mock('@src/background/services/featureFlags/FeatureFlagService'); -jest.mock('@src/background/services/wallet/WalletService'); -jest.mock('@src/background/services/lock/LockService'); -jest.mock('./utils/getTargetNetworkForTx'); -jest.mock('@src/background/services/network/utils/isBitcoinNetwork'); -jest.mock('@src/background/services/analytics/utils/encryptAnalyticsData'); -jest.mock('./contracts/contractParsers/parseWithERC20Abi'); -jest.mock('./utils/getTxDescription'); -jest.mock('./contracts/contractParsers/utils/parseBasicDisplayValues'); -jest.mock('@src/utils/measureDuration', () => { - const measureDurationMock = { - start: jest.fn(), - end: jest.fn(), - }; - return { - measureDuration: () => measureDurationMock, - }; -}); -jest.mock('./contracts/contractParsers/contractParserMap', () => ({ - contractParserMap: new Map([['function', jest.fn()]]), -})); -jest.mock('./utils/txToCustomEvmTx'); -jest.mock('@src/utils/getExplorerAddress'); -jest.mock('webextension-polyfill', () => ({ - notifications: { - create: jest.fn(), - onClicked: { - addListener: jest.fn(), - removeListener: jest.fn(), - }, - onClosed: { - addListener: jest.fn(), - removeListener: jest.fn(), - }, - clear: jest.fn(), - }, - tabs: { - create: jest.fn(), - }, - windows: { - onRemoved: { - addListener: jest.fn(), - removeListener: jest.fn(), - }, - onFocusChanged: { - addListener: jest.fn(), - removeListener: jest.fn(), - }, - }, - i18n: { - getMessage: jest.fn(), - }, - runtime: { - id: 'runtime-id', - }, -})); -jest.mock('@blockaid/client', () => { - return jest.fn(); -}); - -const buildMessage = (params: EthSendTransactionParams) => ({ - method: DAppProviderRequest.ETH_SEND_TX, - id: Math.floor(1_000_000 * Math.random()).toString(), - params: [params], - site: { - tabId: '1', - } as any, -}); - -const gweiToBig = (gwei: number) => BigInt(`0x${(gwei * 1e9).toString(16)}`); - -const mockedFees = { - displayDecimals: 9, // gwei - low: { maxFee: gweiToBig(30), maxTip: gweiToBig(1) }, - medium: { maxFee: gweiToBig(33), maxTip: gweiToBig(1.5) }, - high: { maxFee: gweiToBig(36), maxTip: gweiToBig(2) }, - isFixedFee: false, -}; - -const displayValuesMock: TransactionDisplayValues = { - fromAddress: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - gas: { - maxFeePerGas: 123n, - gasLimit: 123, - }, - actions: [ - { - type: TransactionType.SEND_TOKEN, - fromAddress: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - toAddress: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - token: { - address: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa', - decimals: 18, - symbol: 'TT', - name: 'Test Token', - - amount: 123123n, - }, - }, - ], -}; - -describe('background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts', () => { - const networkService = new NetworkService({} as any, {} as any); - const networkFeeService = new NetworkFeeService({} as any); - const balanceAggregatorService = new BalanceAggregatorService( - {} as any, - {} as any, - {} as any, - {} as any, - {} as any - ); - const accountsService = new AccountsService( - {} as any, - {} as any, - {} as any, - {} as any, - {} as any, - {} as any, - {} as any - ); - - const tokenManagerService = new TokenManagerService({} as any, {} as any); - const walletService = new WalletService( - {} as any, - {} as any, - {} as any, - {} as any, - {} as any, - {} as any, - {} as any - ); - const analyticsServicePosthog = new AnalyticsServicePosthog( - {} as any, - {} as any, - {} as any - ); - const blockaidService = new BlockaidService({} as any); - const lockService = new LockService({} as any, {} as any); - - const accountMock = { - type: AccountType.PRIMARY, - id: '12', - name: '', - addressC: '0x123123123', - addressBTC: 'tb1123123', - index: 1, - walletId: 'wallet-id', - }; - const networkMock = { - chainId: 1, - caipId: 'eip155:1', - vmName: NetworkVMType.EVM, - chainName: '', - rpcUrl: '', - explorerUrl: 'https://explorer.url', - logoUri: '', - networkToken: { - name: 'Ethereum', - symbol: 'ETH', - decimals: 18, - description: '', - logoUri: '', - }, - }; - const mockedEncryptResult = { - data: 'testData', - enc: 'testEnc', - keyID: 'testKeyId', - }; - let handler: EthSendTransactionHandler; - let provider: JsonRpcBatchInternal; - beforeEach(() => { - jest.clearAllMocks(); - jest.useFakeTimers(); - jest.mocked(caipToChainId).mockReturnValue(43114); - jest.mocked(browser.notifications.clear).mockResolvedValue(true); - jest - .spyOn(accountsService, 'activeAccount', 'get') - .mockReturnValue(accountMock); - jest.mocked(getTargetNetworkForTx).mockResolvedValue(networkMock); - provider = new JsonRpcBatchInternal(123); - jest.spyOn(provider, 'getCode').mockResolvedValue('0x'); - jest.spyOn(provider, 'getTransactionCount').mockResolvedValue(3); // dummy nonce - jest.spyOn(provider, 'estimateGas').mockResolvedValue(21000n); // dummy gas limit - jest.spyOn(provider, 'waitForTransaction').mockRejectedValue(new Error()); - jest.mocked(getProviderForNetwork).mockReturnValue(provider); - jest.mocked(networkFeeService).getNetworkFee.mockResolvedValue(mockedFees); - jest.mocked(networkFeeService).estimateGasLimit.mockResolvedValue(1234); - jest.mocked(tokenManagerService).getTokensByChainId.mockResolvedValue([]); - jest - .mocked(analyticsServicePosthog) - .captureEncryptedEvent.mockResolvedValue(); - jest.mocked(isBitcoinNetwork).mockReturnValue(false); - (encryptAnalyticsData as jest.Mock).mockResolvedValue(mockedEncryptResult); - jest.mocked(openApprovalWindow).mockResolvedValue({} as any); - jest.mocked(txToCustomEvmTx).mockReturnValue({ - maxFeePerGas: '0x54', - maxPriorityFeePerGas: 1n, - gasLimit: 123, - data: '0x', - to: '0x123123', - from: '0x345', - value: '0x1', - type: 2, - }); - handler = new EthSendTransactionHandler( - networkService, - networkFeeService, - accountsService, - balanceAggregatorService, - tokenManagerService, - walletService, - analyticsServicePosthog, - blockaidService, - lockService - ); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - describe('handleUnauthenticated', () => { - it('returns error', async () => { - const request = { - id: '123', - method: 'eth_sendTransaction', - params: [], - }; - expect( - await handler.handleUnauthenticated(buildRpcCall(request)) - ).toStrictEqual({ - ...request, - error: ethErrors.provider.unauthorized(), - }); - }); - }); - - describe('handleAuthenticated', () => { - describe('adds gas information', () => { - it('sets type to "2" (EIP-1559) when not specified', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - }); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - type: 2, - }), - }), - }), - 'sign/transaction' - ); - }); - it('upgrades the transaction to EIP-1559 type', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - type: 0, - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - type: 2, - }), - }), - }), - 'sign/transaction' - ); - }); - it('does not downgrade transactions with higher type', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - type: 3, - }); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - type: 2, - }), - }), - }), - 'sign/transaction' - ); - }); - it('uses the suggested gas limit', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - gas: 1, - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - gasLimit: '0x1', - }), - }), - }), - 'sign/transaction' - ); - }); - it('uses the suggested gas limit from the gasLimit param first', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - gasLimit: '0x123', - gas: 1337, - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - gasLimit: '0x123', - }), - }), - }), - 'sign/transaction' - ); - }); - it('estimates gasLimit when not provided', async () => { - jest.mocked(networkFeeService).estimateGasLimit.mockResolvedValue(1); - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - gasLimit: '0x1', - }), - }), - }), - 'sign/transaction' - ); - }); - it('throws error when gasLimit estimation fails', async () => { - jest - .mocked(networkFeeService) - .estimateGasLimit.mockRejectedValue(new Error('failed request')); - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - }); - - await expect( - handler.handleAuthenticated(buildRpcCall(message)) - ).rejects.toThrow(new Error('failed request')); - expect(openApprovalWindow).not.toHaveBeenCalled(); - }); - it('uses maxFeePerGas and maxPriorityFeePerGas if provided', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x543543', - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x543543', - }), - }), - }), - 'sign/transaction' - ); - }); - it('adds maxFeePerGas and maxPriorityFeePerGas if missing', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - value: '0x5af3107a4000', - }); - - await handler.handleAuthenticated(buildRpcCall(message)); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - txParams: expect.objectContaining({ - maxFeePerGas: '0x6fc23ac00', - maxPriorityFeePerGas: '0x3b9aca00', - }), - }), - }), - 'sign/transaction' - ); - }); - }); - describe('parses transaction', () => { - it('updates balances', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '', - value: '0x5af3107a4000', - data: '0x123123123123', - chainId: '0x2', - }); - jest.mocked(getTargetNetworkForTx).mockResolvedValue({ - ...networkMock, - chainId: 2, - }); - await handler.handleAuthenticated(buildRpcCall(message)); - expect( - balanceAggregatorService.getBalancesForNetworks - ).toHaveBeenCalledTimes(1); - expect( - balanceAggregatorService.getBalancesForNetworks - ).toHaveBeenCalledWith([2], [accountMock]); - }); - it('parses the transaction with blockaid', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '', - value: '0x5af3107a4000', - data: '0x123123123123', - }); - blockaidService.parseTransaction = jest - .fn() - .mockResolvedValue(displayValuesMock); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledWith( - '', - networkMock, - { - chainId: '0xa86a', // 43114 - data: '0x123123123123', - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - gasLimit: '0x4d2', - maxFeePerGas: '0x6fc23ac00', - maxPriorityFeePerGas: '0x3b9aca00', - to: '', - type: 2, - value: '0x5af3107a4000', - } - ); - expect(parseWithERC20Abi).not.toHaveBeenCalled(); - expect(getTxDescription).not.toHaveBeenCalled(); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - it('parses the transaction with ERC20 ABI if debank parsing throws error', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - value: '', - data: '0x123123123123', - }); - const mockToken: NetworkContractToken = { - address: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa', - decimals: 18, - name: 'Test Token', - symbol: 'TT', - contractType: 'ERC-20', - }; - jest - .mocked(tokenManagerService) - .getTokensByChainId.mockResolvedValue([mockToken]); - jest.mocked(parseWithERC20Abi).mockReturnValue(displayValuesMock); - jest - .mocked(blockaidService.parseTransaction) - .mockRejectedValue(new Error('network error')); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).toHaveBeenCalledWith( - { - chainId: '0xa86a', // 43114 - data: '0x123123123123', - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - gasLimit: '0x4d2', - maxFeePerGas: '0x6fc23ac00', - maxPriorityFeePerGas: '0x3b9aca00', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - type: 2, - value: '', - }, - mockToken - ); - expect(getTxDescription).not.toHaveBeenCalled(); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - it('parses the transaction with contract parser map when ERC20 parsing throws an error', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - value: '', - data: '0x123123123123', - }); - jest.mocked(tokenManagerService).getTokensByChainId.mockResolvedValue([ - { - address: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa', - } as any, - ]); - jest.mocked(parseWithERC20Abi).mockImplementation(() => { - throw new Error('parsing failed'); - }); - jest - .mocked(blockaidService.parseTransaction) - .mockRejectedValue(new Error('network error')); - jest - .mocked(getTxDescription) - .mockResolvedValue({ name: 'function' } as TransactionDescription); - const parser = contractParserMap.get('function'); - jest.mocked(parser)?.mockResolvedValue(displayValuesMock); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).toHaveBeenCalledTimes(1); - expect(getTxDescription).toHaveBeenCalledTimes(1); - expect(parser).toHaveBeenCalledTimes(1); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - it('parses the transaction with contract parser map when not a known ERC20 call', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - value: '', - data: '0x123123123123', - }); - jest.mocked(tokenManagerService).getTokensByChainId.mockResolvedValue([ - { - address: '0xBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa', - } as any, - ]); - jest - .mocked(blockaidService.parseTransaction) - .mockRejectedValue(new Error('network error')); - jest - .mocked(getTxDescription) - .mockResolvedValue({ name: 'function' } as TransactionDescription); - const parser = contractParserMap.get('function'); - jest.mocked(parser)?.mockResolvedValue(displayValuesMock); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).not.toHaveBeenCalled(); - expect(getTxDescription).toHaveBeenCalledTimes(1); - expect(parser).toHaveBeenCalledTimes(1); - expect(parseBasicDisplayValues).not.toHaveBeenCalled(); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - it('uses basic display values when parser throws error', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - value: '', - data: '0x123123123123', - }); - jest - .mocked(blockaidService.parseTransaction) - .mockRejectedValue(new Error('network error')); - jest.mocked(getTxDescription).mockResolvedValue({ - name: 'function', - } as TransactionDescription); - const parser = contractParserMap.get('function'); - jest.mocked(parser)?.mockRejectedValue(new Error('parsing error')); - jest - .mocked(parseBasicDisplayValues) - .mockResolvedValue(displayValuesMock); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).not.toHaveBeenCalled(); - expect(parser).toHaveBeenCalledTimes(1); - expect(parseBasicDisplayValues).toHaveBeenCalledTimes(1); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - it('uses basic display values when no matching parser found', async () => { - const message = buildMessage({ - from: '0x473B6494E2632ec1c9F90Ce05327e96e30767638', - to: '0xaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - value: '', - data: '0x123123123123', - }); - jest - .mocked(blockaidService.parseTransaction) - .mockRejectedValue(new Error('network error')); - jest.mocked(getTxDescription).mockResolvedValue({ - name: 'someOtherFunction', - } as TransactionDescription); - const parser = contractParserMap.get('function'); - jest - .mocked(parseBasicDisplayValues) - .mockResolvedValue(displayValuesMock); - await handler.handleAuthenticated(buildRpcCall(message)); - expect(blockaidService.parseTransaction).toHaveBeenCalledTimes(1); - expect(parseWithERC20Abi).not.toHaveBeenCalled(); - expect(parser).not.toHaveBeenCalled(); - expect(parseBasicDisplayValues).toHaveBeenCalledTimes(1); - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - displayData: expect.objectContaining({ - displayValues: displayValuesMock, - }), - }), - 'sign/transaction' - ); - }); - }); - }); - - describe('onActionApproved', () => { - const mockAction: Action = { - id: 'actionId', - params: [], - method: DAppProviderRequest.ETH_SEND_TX, - site: { - domain: 'example.com', - }, - actionId: 'actiondId', - status: ActionStatus.PENDING, - time: 123123, - scope: 'eip:43114', - displayData: { - chainId: '0xa86a', // 43114 - method: DAppProviderRequest.ETH_SEND_TX, - txParams: { - chainId: '0xa86a', - to: '0x32131', - from: '0x123123123', - type: 2, - gasLimit: '0x1', - maxFeePerGas: '0x123', - gas: 1, - }, - displayValues: displayValuesMock, - site: { - domain: 'example.com', - }, - }, - }; - - const onSuccessMock = jest.fn(); - const onErrorMock = jest.fn(); - - it('returns error when network not found', async () => { - jest - .mocked(getTargetNetworkForTx) - .mockRejectedValue(new Error('Network not found')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(getTargetNetworkForTx).toHaveBeenCalledTimes(1); - expect(getTargetNetworkForTx).toHaveBeenCalledWith( - mockAction.displayData.txParams, - networkService, - mockAction.scope - ); - expect(onSuccessMock).not.toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalledWith(new Error('Network not found')); - }); - - it('returns error when network fees are not available', async () => { - jest - .mocked(networkFeeService.getNetworkFee) - .mockRejectedValue(new Error('Network fee error')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(networkFeeService.getNetworkFee).toHaveBeenCalledTimes(1); - expect(networkFeeService.getNetworkFee).toHaveBeenCalledWith(networkMock); - expect(onSuccessMock).not.toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalledWith(new Error('Network fee error')); - }); - - it('returns error when converting to evm tx fails', async () => { - jest.mocked(txToCustomEvmTx).mockImplementation(() => { - throw new Error('Invalid tx params'); - }); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(txToCustomEvmTx).toHaveBeenCalledTimes(1); - expect(txToCustomEvmTx).toHaveBeenCalledWith( - mockAction.displayData.txParams, - mockedFees - ); - expect(onSuccessMock).not.toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalledWith(new Error('Invalid tx params')); - }); - - it('returns error when signing fails', async () => { - jest - .mocked(walletService.sign) - .mockRejectedValue(new Error('Wallet signing error')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock, - 111 - ); - - expect(walletService.sign).toHaveBeenCalledTimes(1); - expect(walletService.sign).toHaveBeenCalledWith( - { - chainId: 43114, - data: '0x', - gasLimit: 123, - maxFeePerGas: '0x54', - maxPriorityFeePerGas: 1n, - nonce: 3, - to: '0x123123', - type: 2, - value: '0x1', - }, - networkMock, - 111 - ); - expect(onSuccessMock).not.toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalledWith( - new Error('Wallet signing error') - ); - }); - - it('returns error when sending the transction fails', async () => { - jest - .mocked(walletService.sign) - .mockResolvedValue({ signedTx: '0x11111111' }); - jest - .mocked(networkService.sendTransaction) - .mockRejectedValue(new Error('Tx send error')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(networkService.sendTransaction).toHaveBeenCalledTimes(1); - expect(networkService.sendTransaction).toHaveBeenCalledWith( - { signedTx: '0x11111111' }, - networkMock - ); - expect(onSuccessMock).not.toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalled(); - expect(onErrorMock).toHaveBeenCalledWith(new Error('Tx send error')); - }); - - it('creates notification on error', async () => { - jest - .mocked(getTargetNetworkForTx) - .mockRejectedValue(new Error('Network not found')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith({ - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Failed transaction', - message: `Transaction failed! Network not found`, - priority: 2, - }); - }); - - it('captures encrypted analytics event on error', async () => { - jest - .mocked(getTargetNetworkForTx) - .mockRejectedValue(new Error('Network not found')); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenCalledTimes(1); - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenCalledWith({ - name: 'transactionFailed', - properties: { - address: '0x123123123', - chainId: '0xa86a', - method: 'eth_sendTransaction', - txHash: undefined, - }, - windowId: '00000000-0000-0000-0000-000000000000', - }); - }); - - it('sends transaction', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(onSuccessMock).toHaveBeenCalled(); - expect(onSuccessMock).toHaveBeenCalledWith('0x0123'); - expect(onErrorMock).not.toHaveBeenCalled(); - }); - - it('captures encrypted analytics event on success', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenCalledTimes(1); - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenCalledWith({ - name: 'transactionSuccessful', - properties: { - address: '0x123123123', - chainId: '0xa86a', - method: 'eth_sendTransaction', - txHash: '0x0123', - }, - windowId: '00000000-0000-0000-0000-000000000000', - }); - }); - - it('opens explorer link on pending notification click', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000', - { - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Pending transaction', - message: `Transaction pending! View on the explorer.`, - priority: 2, - } - ); - - expect(browser.notifications.onClicked.addListener).toHaveBeenCalledTimes( - 1 - ); - - expect(browser.tabs.create).not.toHaveBeenCalled(); - - jest - .mocked(browser.notifications.onClicked.addListener) - .mock.calls[0]?.[0]?.('000'); - - expect(browser.tabs.create).not.toHaveBeenCalled(); - - jest - .mocked(browser.notifications.onClicked.addListener) - .mock.calls[0]?.[0]?.('00000000-0000-0000-0000-000000000000'); - - expect(getExplorerAddressByNetwork).toHaveBeenCalledWith( - networkMock, - '0x0123' - ); - - expect(browser.tabs.create).toHaveBeenCalledTimes(1); - expect(browser.tabs.create).toHaveBeenCalledWith({ - url: 'https://explorer.example.com', - }); - }); - - it('unsubcribes from clicks when pending browser notification closed', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000', - { - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Pending transaction', - message: `Transaction pending! View on the explorer.`, - priority: 2, - } - ); - - expect(browser.notifications.onClosed.addListener).toHaveBeenCalledTimes( - 1 - ); - - expect( - browser.notifications.onClicked.removeListener - ).not.toHaveBeenCalled(); - - ( - jest.mocked(browser.notifications.onClosed.addListener).mock - .calls[0]?.[0] as any - )?.('000'); - - expect( - browser.notifications.onClicked.removeListener - ).not.toHaveBeenCalled(); - - ( - jest.mocked(browser.notifications.onClosed.addListener).mock - .calls[0]?.[0] as any - )?.('00000000-0000-0000-0000-000000000000'); - - expect( - browser.notifications.onClicked.removeListener - ).toHaveBeenCalledWith( - jest.mocked(browser.notifications.onClicked.addListener).mock - .calls[0]?.[0] - ); - }); - - it('dismisses pending browser notification after 5 seconds', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000', - { - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Pending transaction', - message: `Transaction pending! View on the explorer.`, - priority: 2, - } - ); - - expect(browser.notifications.onClosed.addListener).toHaveBeenCalledTimes( - 1 - ); - - jest.advanceTimersByTime(4999); - - expect( - browser.notifications.onClicked.removeListener - ).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - - expect(browser.notifications.clear).toHaveBeenCalledTimes(1); - expect(browser.notifications.clear).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000' - ); - expect( - browser.notifications.onClicked.removeListener - ).toHaveBeenCalledWith( - jest.mocked(browser.notifications.onClicked.addListener).mock - .calls[0]?.[0] - ); - }); - - it('creates transaction confirmed browser notification if wallet is unlocked', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - - let resolveTransaction; - jest.spyOn(provider, 'waitForTransaction').mockReturnValue( - new Promise((resolve) => { - resolveTransaction = resolve; - }) - ); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.clear).toHaveBeenCalledTimes(0); - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000', - expect.objectContaining({ - title: 'Pending transaction', - }) - ); - - resolveTransaction({ status: 1 }); - await Promise.resolve(); - await Promise.resolve(); - - // clears prevous pending notification - expect(browser.notifications.clear).toHaveBeenCalledTimes(1); - expect(browser.notifications.clear).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000' - ); - - // creates new notification - expect(browser.notifications.create).toHaveBeenCalledTimes(2); - expect(browser.notifications.create).toHaveBeenNthCalledWith( - 2, - '00000000-0000-0000-0000-000000000000', - expect.objectContaining({ - title: 'Confirmed transaction', - }) - ); - }); - - it('does not create transaction confirmed browser notification when wallet gets locked while waiting', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - - let resolveTransaction; - jest.spyOn(provider, 'waitForTransaction').mockReturnValue( - new Promise((resolve) => { - resolveTransaction = resolve; - }) - ); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(browser.notifications.clear).toHaveBeenCalledTimes(0); - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - expect(browser.notifications.create).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000', - expect.objectContaining({ - title: 'Pending transaction', - }) - ); - - (lockService as any).locked = true; - - resolveTransaction({ status: 1 }); - await Promise.resolve(); - await Promise.resolve(); - - // clears prevous pending notification - expect(browser.notifications.clear).toHaveBeenCalledTimes(1); - expect(browser.notifications.clear).toHaveBeenCalledWith( - '00000000-0000-0000-0000-000000000000' - ); - - // creates new notification - expect(browser.notifications.create).toHaveBeenCalledTimes(1); - }); - - it('measures time to confirmation and reports it', async () => { - jest.mocked(networkService.sendTransaction).mockResolvedValue('0x0123'); - jest - .mocked(getExplorerAddressByNetwork) - .mockReturnValue('https://explorer.example.com'); - const durationMock = measureDuration(); - jest.mocked(durationMock.end).mockReturnValue(1000); - - let resolveTransaction; - jest.spyOn(provider, 'waitForTransaction').mockReturnValue( - new Promise((resolve) => { - resolveTransaction = resolve; - }) - ); - - await handler.onActionApproved( - mockAction, - undefined, - onSuccessMock, - onErrorMock - ); - - expect(durationMock.start).toHaveBeenCalledTimes(1); - - resolveTransaction({ status: 1 }); - await jest.runAllTimersAsync(); - - expect(durationMock.end).toHaveBeenCalledTimes(1); - - // creates new notification - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenCalledTimes(2); - expect( - analyticsServicePosthog.captureEncryptedEvent - ).toHaveBeenNthCalledWith(2, { - name: 'TransactionTimeToConfirmation', - properties: { - chainId: '0xa86a', - duration: 1000, - rpcUrl: '', - site: 'example.com', - success: true, - txType: 'eth_sendTransaction', - }, - windowId: '00000000-0000-0000-0000-000000000000', - }); - }); - }); -}); diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts deleted file mode 100644 index 522973588..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts +++ /dev/null @@ -1,461 +0,0 @@ -import { - DAppProviderRequest, - JsonRpcRequestParams, -} from '@src/background/connections/dAppConnection/models'; -import { injectable } from 'tsyringe'; -import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { DAppRequestHandler } from '@src/background/connections/dAppConnection/DAppRequestHandler'; -import { ethErrors } from 'eth-rpc-errors'; -import { Action } from '@src/background/services/actions/models'; -import { NetworkService } from '@src/background/services/network/NetworkService'; -import getTargetNetworkForTx from './utils/getTargetNetworkForTx'; -import { - ContractTransaction, - JsonRpcApiProvider, - Result, - TransactionDescription, -} from 'ethers'; -import { - EthSendTransactionParams, - EthSendTransactionParamsWithGas, - Transaction, - TransactionDisplayValues, - isTxParams, -} from './models'; -import { NetworkFeeService } from '@src/background/services/networkFee/NetworkFeeService'; -import { BalanceAggregatorService } from '@src/background/services/balances/BalanceAggregatorService'; -import { AccountsService } from '@src/background/services/accounts/AccountsService'; -import { TokenManagerService } from '@src/background/services/tokens/TokenManagerService'; -import { parseWithERC20Abi } from './contracts/contractParsers/parseWithERC20Abi'; -import { getTxDescription } from './utils/getTxDescription'; -import { ContractParserHandler } from './contracts/contractParsers/models'; -import { contractParserMap } from './contracts/contractParsers/contractParserMap'; -import { parseBasicDisplayValues } from './contracts/contractParsers/utils/parseBasicDisplayValues'; -import browser, { runtime } from 'webextension-polyfill'; -import { getExplorerAddressByNetwork } from '@src/utils/getExplorerAddress'; -import { txToCustomEvmTx } from './utils/txToCustomEvmTx'; -import { WalletService } from '@src/background/services/wallet/WalletService'; -import { JsonRpcBatchInternal } from '@avalabs/core-wallets-sdk'; -import { AnalyticsServicePosthog } from '@src/background/services/analytics/AnalyticsServicePosthog'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; -import { BlockaidService } from '@src/background/services/blockaid/BlockaidService'; -import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -import { EnsureDefined } from '@src/background/models'; -import { caipToChainId } from '@src/utils/caipConversion'; -import { TxDisplayOptions } from '../models'; -import { measureDuration } from '@src/utils/measureDuration'; -import { noop } from '@src/utils/noop'; -import { NetworkWithCaipId } from '@src/background/services/network/models'; -import { LockService } from '@src/background/services/lock/LockService'; - -type TxPayload = EthSendTransactionParams | ContractTransaction; -type Params = [TxPayload] | [TxPayload, TxDisplayOptions]; - -@injectable() -export class EthSendTransactionHandler extends DAppRequestHandler< - Params, - string -> { - methods = [DAppProviderRequest.ETH_SEND_TX]; - - constructor( - private networkService: NetworkService, - private networkFeeService: NetworkFeeService, - private accountsService: AccountsService, - private balancesService: BalanceAggregatorService, - private tokenManagerService: TokenManagerService, - private walletService: WalletService, - private analyticsServicePosthog: AnalyticsServicePosthog, - private blockaidService: BlockaidService, - private lockService: LockService - ) { - super(); - } - - handleUnauthenticated = async ({ request }) => { - return { - ...request, - error: ethErrors.provider.unauthorized(), - }; - }; - - handleAuthenticated = async ({ - request, - scope, - }: JsonRpcRequestParams) => { - const { params, site } = request; - - const rawParams = (params || [])[0] as EthSendTransactionParams; - const displayOptions = params[1]; - - // Only the extension UI is allowed to suggest custom display options - if (displayOptions && site?.domain !== runtime.id) { - throw new Error('Unauthorized use of display options'); - } - - const trxParams = { - ...rawParams, - chainId: rawParams.chainId ?? `0x${caipToChainId(scope).toString(16)}`, - }; - const network = await getTargetNetworkForTx( - trxParams, - this.networkService, - scope - ); - - if (!network) { - throw Error('no network selected'); - } - - const provider = getProviderForNetwork(network, true); - if (!provider || !(provider instanceof JsonRpcApiProvider)) { - throw Error('provider not available'); - } - - if (!trxParams || !isTxParams(trxParams)) { - throw new Error('invalid transaction data'); - } - - const txPayload = await this.#addGasInformation(network, trxParams); - - if (this.accountsService.activeAccount) { - await this.balancesService.getBalancesForNetworks( - [network.chainId], - [this.accountsService.activeAccount] - ); - } - - let displayValues: TransactionDisplayValues | undefined = undefined; - - // order of parsing a transaction: - // 1. use TX pre execution if available - // 2. if not, check if toAddress is a known ERC20 token - // 3. if not, use default basic approval data - try { - displayValues = await this.blockaidService.parseTransaction( - site?.domain || '', - network, - txPayload - ); - } catch (e) { - // Blockaid parsing failed, try ERC20 parsing next - } - - // if debank parsing failed check if toAddress is a known ERC20 - if (!displayValues && txPayload.to) { - const erc20Tokens = await this.tokenManagerService.getTokensByChainId( - network.chainId - ); - const lowerCaseAddress = txPayload.to.toLowerCase(); - const erc20Token = erc20Tokens.find( - (t) => t.address.toLowerCase() === lowerCaseAddress - ); - if (erc20Token) { - try { - displayValues = parseWithERC20Abi(txPayload, erc20Token); - } catch (e) { - // ERC20 parsing failed fall back to local ABI parsers - } - } - } - - // if debank and ERC20 parsing failed attempt to use ABI parsing - if (!displayValues) { - // the toAddress is empty for contract deployments - const txDescription: TransactionDescription | null = - await getTxDescription(network, provider, trxParams); - - const decodedData: Result | undefined = txDescription?.args; - const parser: ContractParserHandler | undefined = contractParserMap.get( - txDescription?.name ?? '' - ); - - try { - displayValues = parser - ? await parser(network, txPayload, decodedData, txDescription) - : await parseBasicDisplayValues(network, txPayload, txDescription); - } catch (err) { - displayValues = await parseBasicDisplayValues( - network, - txPayload, - txDescription - ); - } - } - - const actionData: Action = { - ...request, - scope, - displayData: { - site, - method: request.method, - chainId: network.chainId.toString(), - txParams: txPayload, - displayValues, - displayOptions, - }, - }; - - await openApprovalWindow(actionData, `sign/transaction`); - - return { ...request, result: DEFERRED_RESPONSE }; - }; - - onActionApproved = async ( - pendingAction: Action, - result: any, - onSuccess: (result: unknown) => Promise, - onError: (error: Error) => Promise, - tabId?: number | undefined - ) => { - const measurement = measureDuration(); - measurement.start(); - - try { - const network = await getTargetNetworkForTx( - pendingAction.displayData.txParams, - this.networkService, - pendingAction.scope - ); - - const calculatedFee = await this.networkFeeService.getNetworkFee(network); - - if (!network) { - throw new Error('network not found'); - } - - const provider = getProviderForNetwork(network) as JsonRpcBatchInternal; - - const nonce = await provider.getTransactionCount( - pendingAction.displayData.txParams.from - ); - const chainId = pendingAction.displayData.chainId; - - const { - maxFeePerGas, - maxPriorityFeePerGas, - gasLimit, - data, - to, - value, - type, - } = txToCustomEvmTx(pendingAction.displayData.txParams, calculatedFee); - - const signingResult = await this.walletService.sign( - { - nonce, - chainId: Number(BigInt(chainId)), - maxFeePerGas, - maxPriorityFeePerGas, - gasLimit: gasLimit, - data: data, - to: to, - value: value, - type, - }, - network, - tabId - ); - - const txHash = await this.networkService.sendTransaction( - signingResult, - network - ); - - const notificationId = crypto.randomUUID(); - - await this.#createTxNotification( - notificationId, - { - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Pending transaction', - message: `Transaction pending! View on the explorer.`, - priority: 2, - }, - network, - txHash - ); - - provider - .waitForTransaction(txHash) - .then(async (tx) => { - const duration = measurement.end(); - - const isTxSuccessul = tx?.status === 1; - await browser.notifications.clear(notificationId); // close transaction pending notification - if (!this.lockService.locked) { - await this.#createTxNotification( - crypto.randomUUID(), - { - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: isTxSuccessul - ? 'Confirmed transaction' - : 'Failed transaction', - message: `Transaction ${ - isTxSuccessul ? 'confirmed' : 'failed' - }! View on the explorer.`, - priority: 2, - }, - network, - txHash - ); - } - - this.analyticsServicePosthog.captureEncryptedEvent({ - name: 'TransactionTimeToConfirmation', - windowId: crypto.randomUUID(), - properties: { - duration, - txType: pendingAction.displayData.method, - chainId, - success: isTxSuccessul, - rpcUrl: network.rpcUrl, - site: pendingAction.displayData.site?.domain, - }, - }); - }) - .catch(noop); - - // No need to await the request here. - this.analyticsServicePosthog.captureEncryptedEvent({ - name: 'transactionSuccessful', - windowId: crypto.randomUUID(), - properties: { - address: this.accountsService.activeAccount?.addressC, - txHash, - method: pendingAction.method, - chainId, - }, - }); - - onSuccess(txHash); - } catch (err: any) { - // Stop and clean up measurement - // Some error happened during transaction creation, no need to measure end-to-end time till confirmation - measurement.end(); - const errorMessage: string = - err instanceof Error ? err.message : err.toString(); - - await browser.notifications.create({ - type: 'basic', - iconUrl: '../../../../images/icon-32.png', - title: 'Failed transaction', - message: `Transaction failed! ${errorMessage}`, - priority: 2, - }); - - // No need to await the request here. - this.analyticsServicePosthog.captureEncryptedEvent({ - name: 'transactionFailed', - windowId: crypto.randomUUID(), - properties: { - address: this.accountsService.activeAccount?.addressC, - txHash: undefined, - method: pendingAction.method, - chainId: pendingAction.displayData.chainId, - }, - }); - - onError(err); - } - }; - - async #createTxNotification( - notificationId: string, - notificationParams: browser.Notifications.CreateNotificationOptions, - network: NetworkWithCaipId, - txHash: string - ) { - await browser.notifications.create(notificationId, notificationParams); - - const openTab = async (id: string) => { - if (id === notificationId) { - const explorerUrl = getExplorerAddressByNetwork(network, txHash); - await browser.tabs.create({ url: explorerUrl }); - } - }; - - browser.notifications.onClicked.addListener(openTab); - browser.notifications.onClosed.addListener((id: string) => { - if (id === notificationId) { - browser.notifications.onClicked.removeListener(openTab); - } - }); - - /* - notifications.onClosed is only triggered when a user close the notification. - And the notification is automatically closed 5 secs if user does not close it. - To mimic onClosed for system, using setTimeout. - */ - setTimeout(async () => { - browser.notifications.onClicked.removeListener(openTab); - await browser.notifications.clear(notificationId); - }, 5000); - } - - async #addGasInformation( - network: NetworkWithCaipId, - tx: EnsureDefined - ): Promise> { - const fees = await this.networkFeeService.getNetworkFee(network); - const maxFeePerGas = fees?.low.maxFee ?? 0n; - const maxPriorityFeePerGas = fees?.low.maxTip - ? BigInt(fees?.low.maxTip) - : undefined; - const typeFromPayload = tx.maxFeePerGas ? 2 : 0; - - let gasLimit: string; - - if (!tx.gasLimit) { - try { - const gasLimitEstimation = await (tx.gas - ? BigInt(tx.gas) - : this.networkFeeService.estimateGasLimit( - tx.from, - tx.to ?? '', - tx.data as string, - network, - tx.value ? BigInt(tx.value) : undefined - )); - - // we should always be able to calculate gas limit. If we don't have the limit the TX would fail anyways - if (!gasLimitEstimation) { - throw new Error('Unable to calculate gas limit'); - } - - gasLimit = `0x${gasLimitEstimation.toString(16)}`; - } catch (e: any) { - // handle gas estimation erros with the correct error message - if (e.error?.error) { - throw e.error.error; - } - throw e; - } - } else { - gasLimit = tx.gasLimit; - } - - return { - ...tx, - /** - * We use EIP-1559 market fees (maxFeePerGas/maxPriorityFeePerGas), - * and they require `type` to be set accordingly (to a value of 2). - * - * If the transaction payload explicitly sets a higher `type`, - * we won't change it hoping it's still backwards-compatible. - * - * At the moment of writing this comment, "2" is the highest tx type available. - */ - type: Math.max(typeFromPayload, 2), - maxFeePerGas: tx.maxFeePerGas - ? tx.maxFeePerGas - : `0x${maxFeePerGas.toString(16)}`, - maxPriorityFeePerGas: tx.maxPriorityFeePerGas - ? tx.maxPriorityFeePerGas - : `0x${maxPriorityFeePerGas?.toString(16)}`, - gasLimit, - }; - } -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/index.ts b/src/background/services/wallet/handlers/eth_sendTransaction/index.ts deleted file mode 100644 index 4926858eb..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './eth_sendTransaction'; diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx.ts b/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx.ts index 7129948d4..d8c60b7d8 100644 --- a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx.ts +++ b/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx.ts @@ -3,7 +3,6 @@ import { NetworkService } from '@src/background/services/network/NetworkService' import { ethErrors } from 'eth-rpc-errors'; import { EthSendTransactionParams } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; import { Network } from '@src/background/services/network/models'; -import { EnsureDefined } from '@src/background/models'; const assertSameEnvironment = (networkA: Network, activeNetwork?: Network) => { if (Boolean(networkA.isTestnet) !== Boolean(activeNetwork?.isTestnet)) { @@ -15,10 +14,14 @@ const assertSameEnvironment = (networkA: Network, activeNetwork?: Network) => { }; const getTargetNetworkForTx = async ( - tx: EnsureDefined, + tx: EthSendTransactionParams, networkService: NetworkService, activeScope: string ) => { + if (typeof tx.chainId === 'undefined') { + return networkService.getNetwork(activeScope); + } + const { uiActiveNetwork } = networkService; const chainId = parseInt(tx.chainId); diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxDescription.ts b/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxDescription.ts deleted file mode 100644 index 1a9d0e9e7..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxDescription.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getTxInfo } from './getTxInfo'; -import { resolve } from '@src/utils/promiseResolver'; -import { JsonRpcApiProvider, TransactionDescription } from 'ethers'; -import { EthSendTransactionParams } from '../models'; -import { Network } from '@avalabs/core-chains-sdk'; - -export async function getTxDescription( - network: Network, - provider: JsonRpcApiProvider, - tx: EthSendTransactionParams -): Promise { - const toAddress = tx.to?.toLocaleLowerCase() || ''; - - // the toAddress is empty for contract deployments - if (!toAddress) { - return null; - } - const [contractByteCode, error] = await resolve(provider.getCode(toAddress)); - - if (error) { - return null; - } - - // the response is always `0x` if the address is EOA and it's the contract's source byte code otherwise - // see https://docs.ethers.org/v5/single-page/#/v5/api/providers/provider/-%23-Provider-getCode - if (contractByteCode === '0x') { - return null; - } - try { - return await getTxInfo(toAddress, tx.data ?? '', tx.value ?? '', network); - } catch (err) { - console.error(err); - return null; - } -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxInfo.ts b/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxInfo.ts deleted file mode 100644 index 680901fda..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/utils/getTxInfo.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Interface, TransactionDescription, toBeHex } from 'ethers'; -import { - getABIForContract, - getSourceForContract, -} from '@avalabs/core-snowtrace-sdk'; -import { ChainId, Network } from '@avalabs/core-chains-sdk'; -import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json'; - -function parseDataWithABI( - data: string, - value: string, - contractInterface: Interface -): TransactionDescription { - const finalResponse = contractInterface.parseTransaction({ - data: data, - value: value ? value : undefined, - }); - - if (!finalResponse) { - throw new Error('no matching function found'); - } - - return finalResponse; -} - -async function getAvalancheABIFromSource(address: string, isMainnet: boolean) { - const contractSourceResponse = await getSourceForContract( - address, - isMainnet, - process.env.GLACIER_API_KEY - ); - - if (!contractSourceResponse.result[0]) { - throw new Error('Missing ContractSourceCodeResponse'); - } - - const contractSource = contractSourceResponse.result[0]; - - const response = await (contractSource.Proxy === '1' && - contractSource.Implementation.length > 0 - ? getABIForContract( - contractSource.Implementation, - isMainnet, - process.env.GLACIER_API_KEY - ) - : Promise.resolve(undefined)); - - return { result: response?.result, contractSource }; -} - -export async function getTxInfo( - address: string, - data: string, - value: string | bigint, - network: Network -): Promise { - const hexValue = toBeHex(value); - /** - * We already eliminate BTC as a tx requestor so we only need to verify if we are still on a - * avalanche net. At this point anything else would be a subnet - */ - if ( - network?.chainId !== ChainId.AVALANCHE_TESTNET_ID && - network?.chainId !== ChainId.AVALANCHE_MAINNET_ID - ) { - return parseDataWithABI(data, hexValue, new Interface(ERC20.abi)); - } - - const { result, contractSource } = await getAvalancheABIFromSource( - address, - !network.isTestnet - ); - - if (contractSource?.ABI === 'Contract source code not verified') { - throw new Error('Contract source code not verified'); - } - - const abi = result || contractSource?.ABI; - if (!abi) { - throw new Error('unable to get abi'); - } - return parseDataWithABI(data, hexValue, new Interface(abi)); -} diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/utils/txToCustomEvmTx.ts b/src/background/services/wallet/handlers/eth_sendTransaction/utils/txToCustomEvmTx.ts deleted file mode 100644 index 26629d803..000000000 --- a/src/background/services/wallet/handlers/eth_sendTransaction/utils/txToCustomEvmTx.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NetworkFee } from '@src/background/services/networkFee/models'; -import { EthSendTransactionParamsWithGas } from '../models'; - -export function txToCustomEvmTx( - txParams?: EthSendTransactionParamsWithGas, - networkFee?: NetworkFee | null -) { - if (!txParams) { - throw new Error('transaction is malformed'); - } - - const { - gas, - to, - from, - data, - type, - value, - gasPrice, - maxFeePerGas, - maxPriorityFeePerGas, - } = txParams; - - if (!gas || !networkFee) { - throw new Error('Gas or gas estimate is malformed'); - } - - if (!to && !data) { - throw new Error('the to or data is malformed'); - } - - const gasLimit = Number(gas); - - if (!maxFeePerGas && !gasPrice) { - throw new Error( - `not enough gas price data: provide values for [gasPrice] or [maxFeePerGas]` - ); - } - - return { - maxFeePerGas: maxFeePerGas || gasPrice || networkFee.low.maxFee, - maxPriorityFeePerGas: maxPriorityFeePerGas || networkFee.low.maxTip, - gasLimit: gasLimit, - to, - from, - data, - type, - value, - }; -} diff --git a/src/background/vmModules/ApprovalController.test.ts b/src/background/vmModules/ApprovalController.test.ts index fbc7b5b5b..09e73cc00 100644 --- a/src/background/vmModules/ApprovalController.test.ts +++ b/src/background/vmModules/ApprovalController.test.ts @@ -1,9 +1,8 @@ import { ChainId } from '@avalabs/core-chains-sdk'; -import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { DappInfo, DetailItemType, RpcMethod } from '@avalabs/vm-module-types'; import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; -import { buildBtcTx } from '@src/utils/send/btcSendUtils'; import { chainIdToCaip } from '@src/utils/caipConversion'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; @@ -12,8 +11,9 @@ import { NetworkService } from '../services/network/NetworkService'; import { openApprovalWindow } from '../runtime/openApprovalWindow'; import { ApprovalParamsWithContext } from './models'; -import { buildBtcSendTransactionAction } from './helpers/buildBtcSendTransactionAction'; import { ApprovalController } from './ApprovalController'; +import { ACTION_HANDLED_BY_MODULE } from '../models'; +import { ActionStatus } from '../services/actions/models'; jest.mock('tsyringe', () => { return { @@ -25,7 +25,6 @@ jest.mock('tsyringe', () => { }); jest.mock('../runtime/openApprovalWindow'); jest.mock('@src/utils/network/getProviderForNetwork'); -jest.mock('@src/utils/send/btcSendUtils'); const btcNetwork = { chainId: ChainId.BITCOIN_TESTNET, @@ -38,25 +37,137 @@ const dappInfo: DappInfo = { url: 'https://extension.url', }; +const actionId = crypto.randomUUID(); +const getExpectedAction = (params) => ({ + [ACTION_HANDLED_BY_MODULE]: true, + dappInfo: params.request.dappInfo, + signingData: params.signingData, + context: params.request.context, + status: ActionStatus.PENDING, + tabId: params.request.context?.tabId, + params: params.request.params, + displayData: params.displayData, + scope: params.request.chainId, + id: params.request.requestId, + method: params.request.method, +}); + describe('src/background/vmModules/ApprovalController', () => { - describe('requestApproval()', () => { - let walletService: jest.Mocked; - let networkService: jest.Mocked; - let controller: ApprovalController; + const params: BitcoinSendTransactionParams = { + amount: 100_000_000, + feeRate: 50, + from: 'from', + to: 'to', + }; + + const approvalParams: ApprovalParamsWithContext = { + request: { + chainId: chainIdToCaip(btcNetwork.chainId), + method: RpcMethod.BITCOIN_SEND_TRANSACTION, + requestId: 'requestId', + sessionId: 'sessionId', + dappInfo, + context: { + tabId: 1234, + }, + params, + }, + displayData: { + details: [ + { + title: 'Transaction Details', + items: [ + { + label: 'From', + type: DetailItemType.ADDRESS, + value: params.from, + }, + ], + }, + ], + network: btcNetwork, + title: 'Approve Transaction', + networkFeeSelector: true, + }, + signingData: { + account: params.from, + type: RpcMethod.BITCOIN_SEND_TRANSACTION, + data: { + amount: params.amount, + balance: {} as any, + gasLimit: 200, + fee: params.feeRate * 200, // 200 bytes vsize + feeRate: params.feeRate, + to: params.to, + inputs: [], + outputs: [], + }, + }, + }; + + const provider = {} as any; + const btcTx = { + inputs: [], + outputs: [], + fee: params.feeRate * 200, + }; + + let walletService: jest.Mocked; + let networkService: jest.Mocked; + let controller: ApprovalController; + + beforeEach(() => { + walletService = { + sign: jest.fn(), + } as any; + + networkService = { + getNetwork: jest.fn(), + } as any; + + controller = new ApprovalController(walletService, networkService); + + jest.mocked(networkService.getNetwork).mockResolvedValue(btcNetwork); + jest.mocked(getProviderForNetwork).mockReturnValue(provider); + jest.mocked(openApprovalWindow).mockImplementation(async (action) => ({ + ...action, + actionId, + })); + }); + + describe('updateTx()', () => { + it('uses `updateTx` callback to update the transaction payload', async () => { + const updateTx = jest.fn().mockImplementation(({ maxFeeRate }) => ({ + ...approvalParams.signingData, + data: { + ...approvalParams.signingData, + feeRate: Number(maxFeeRate), + }, + })); + + controller.requestApproval({ + ...approvalParams, + updateTx, + }); + + await new Promise(process.nextTick); - beforeEach(() => { - walletService = { - sign: jest.fn(), - } as any; + const result = controller.updateTx(actionId, { + maxFeeRate: 150n, + }); - networkService = { - getNetwork: jest.fn(), - } as any; + expect(updateTx).toHaveBeenCalledWith({ maxFeeRate: 150n }); - controller = new ApprovalController(walletService, networkService); + expect(result).toEqual({ + ...approvalParams.signingData, + data: { ...approvalParams.signingData, feeRate: 150 }, + }); }); + }); + describe('requestApproval()', () => { it('returns error if network cannot be resolved', async () => { + jest.mocked(networkService.getNetwork).mockResolvedValueOnce(undefined); expect( await controller.requestApproval({ request: { @@ -67,103 +178,15 @@ describe('src/background/vmModules/ApprovalController', () => { error: expect.objectContaining({ message: 'Unsupported network' }), }); }); - it('returns error if signing data is of unknown format', async () => { - networkService.getNetwork.mockResolvedValue(btcNetwork); - - expect( - await controller.requestApproval({ - request: { - chainId: btcNetwork.chainId, - method: RpcMethod.BITCOIN_SEND_TRANSACTION, - }, - signingData: { - type: 'weird-format', - }, - } as any) - ).toEqual({ - error: expect.objectContaining({ - code: errorCodes.rpc.methodNotSupported, - }), - }); - }); describe(`after approval`, () => { - const params: BitcoinSendTransactionParams = { - amount: 100_000_000, - feeRate: 50, - from: 'from', - to: 'to', - }; - - const approvalParams: ApprovalParamsWithContext = { - request: { - chainId: chainIdToCaip(btcNetwork.chainId), - method: RpcMethod.BITCOIN_SEND_TRANSACTION, - requestId: 'requestId', - sessionId: 'sessionId', - dappInfo, - context: { - tabId: 1234, - }, - params, - }, - displayData: { - details: [ - { - title: 'Transaction Details', - items: [ - { - label: 'From', - type: DetailItemType.ADDRESS, - value: params.from, - }, - ], - }, - ], - network: btcNetwork, - title: 'Approve Transaction', - networkFeeSelector: true, - }, - signingData: { - account: params.from, - type: RpcMethod.BITCOIN_SEND_TRANSACTION, - data: { - amount: params.amount, - balance: {} as any, - gasLimit: 200, - fee: params.feeRate * 200, // 200 bytes vsize - feeRate: params.feeRate, - to: params.to, - inputs: [], - outputs: [], - }, - }, - }; - - const provider = {} as any; - const btcTx = { - inputs: [], - outputs: [], - fee: params.feeRate * 200, - }; - - beforeEach(() => { - jest.mocked(networkService.getNetwork).mockResolvedValue(btcNetwork); - jest.mocked(getProviderForNetwork).mockReturnValue(provider); - jest.mocked(buildBtcTx).mockResolvedValue(btcTx); - jest.mocked(openApprovalWindow).mockImplementation(async (action) => ({ - ...action, - actionId: crypto.randomUUID(), - })); - }); - it('opens the generic approval screen', async () => { controller.requestApproval(approvalParams); await new Promise(process.nextTick); expect(openApprovalWindow).toHaveBeenCalledWith( - buildBtcSendTransactionAction(approvalParams), + getExpectedAction(approvalParams), 'approve/generic' ); }); @@ -174,7 +197,7 @@ describe('src/background/vmModules/ApprovalController', () => { await new Promise(process.nextTick); const action = { - ...buildBtcSendTransactionAction(approvalParams), + ...getExpectedAction(approvalParams), actionId: crypto.randomUUID(), }; @@ -191,7 +214,7 @@ describe('src/background/vmModules/ApprovalController', () => { const promise = controller.requestApproval(approvalParams); const action = { - ...buildBtcSendTransactionAction(approvalParams), + ...getExpectedAction(approvalParams), actionId: crypto.randomUUID(), }; @@ -204,10 +227,10 @@ describe('src/background/vmModules/ApprovalController', () => { await new Promise(process.nextTick); expect(walletService.sign).toHaveBeenCalledWith( - { + expect.objectContaining({ inputs: btcTx.inputs, outputs: btcTx.outputs, - }, + }), btcNetwork ); @@ -230,7 +253,7 @@ describe('src/background/vmModules/ApprovalController', () => { const promise = controller.requestApproval(approvalParams); const action = { - ...buildBtcSendTransactionAction(approvalParams), + ...getExpectedAction(approvalParams), actionId: crypto.randomUUID(), }; @@ -239,23 +262,14 @@ describe('src/background/vmModules/ApprovalController', () => { controller.onApproved(action); - const signingData = action.signingData as any; - - expect(buildBtcTx).toHaveBeenCalledWith(signingData.account, provider, { - amount: signingData.data.amount, - address: signingData.data.to, - feeRate: signingData.data.feeRate, - token: signingData.data.balance, - }); - // Wait for transaction to be constructed await new Promise(process.nextTick); expect(walletService.sign).toHaveBeenCalledWith( - { + expect.objectContaining({ inputs: btcTx.inputs, outputs: btcTx.outputs, - }, + }), btcNetwork ); diff --git a/src/background/vmModules/ApprovalController.ts b/src/background/vmModules/ApprovalController.ts index 15dcbce75..1f164ee28 100644 --- a/src/background/vmModules/ApprovalController.ts +++ b/src/background/vmModules/ApprovalController.ts @@ -2,25 +2,23 @@ import { singleton } from 'tsyringe'; import { ApprovalParams, ApprovalResponse, + BtcTxUpdateFn, + DisplayData, + EvmTxUpdateFn, ApprovalController as IApprovalController, - RpcError, RpcMethod, + SigningData, } from '@avalabs/vm-module-types'; -import { BitcoinProvider } from '@avalabs/core-wallets-sdk'; -import { rpcErrors, JsonRpcError, providerErrors } from '@metamask/rpc-errors'; - -import { buildBtcTx } from '@src/utils/send/btcSendUtils'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; +import { rpcErrors, providerErrors } from '@metamask/rpc-errors'; import { WalletService } from '../services/wallet/WalletService'; -import { Action } from '../services/actions/models'; +import { Action, ActionStatus } from '../services/actions/models'; import { openApprovalWindow } from '../runtime/openApprovalWindow'; import { NetworkService } from '../services/network/NetworkService'; import { NetworkWithCaipId } from '../services/network/models'; import { ApprovalParamsWithContext } from './models'; -import { buildBtcSendTransactionAction } from './helpers/buildBtcSendTransactionAction'; -import { EnsureDefined } from '../models'; +import { ACTION_HANDLED_BY_MODULE } from '../models'; @singleton() export class ApprovalController implements IApprovalController { @@ -131,18 +129,13 @@ export class ApprovalController implements IApprovalController { }; } - const [preparedAction, actionError] = this.#try(() => - this.#buildAction(params) - ); - if (actionError) return { error: actionError }; - - const action = (await openApprovalWindow( - preparedAction, + const action = await openApprovalWindow( + this.#buildAction(params), 'approve/generic' - )) as EnsureDefined; + ); return new Promise((resolve) => { - this.#requests.set(action.actionId, { + this.#requests.set(action.actionId as string, { params, network, resolve, @@ -150,19 +143,22 @@ export class ApprovalController implements IApprovalController { }); }; - #try any>( - fn: F - ): [ReturnType, null] | [null, RpcError] { - try { - return [fn(), null]; - } catch (err: any) { - const safeError = - err instanceof JsonRpcError - ? err - : rpcErrors.internal({ message: 'Unknown error', data: err }); - return [null, safeError]; + updateTx = ( + id: string, + newData: Parameters[0] | Parameters[0] + ): { signingData: SigningData; displayData: DisplayData } => { + const request = this.#requests.get(id); + + if (!request) { + throw new Error(`No request found with id: ${id}`); + } + + if (!request.params.updateTx) { + throw new Error(`No fee updater provided`); } - } + + return request.params.updateTx(newData); + }; #handleApproval = async ( params: ApprovalParams, @@ -171,35 +167,33 @@ export class ApprovalController implements IApprovalController { ) => { const { signingData } = action; - if (signingData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { - const { inputs, outputs } = await buildBtcTx( - signingData.account, - getProviderForNetwork(network) as BitcoinProvider, - { - amount: signingData.data.amount, - address: signingData.data.to, - feeRate: signingData.data.feeRate, - token: signingData.data.balance, - } - ); + if (!signingData) { + throw new Error('No signing data provided'); + } - if (!inputs || !outputs) { - throw new Error('Unable to construct BTC transaction'); - } + switch (signingData.type) { + case RpcMethod.BITCOIN_SEND_TRANSACTION: + case RpcMethod.ETH_SEND_TRANSACTION: + return await this.#walletService.sign(signingData.data, network); - return await this.#walletService.sign({ inputs, outputs }, network); + default: + throw new Error('Unrecognized method: ' + params.request.method); } - - throw new Error('Unrecognized method: ' + params.request.method); }; #buildAction = (params: ApprovalParamsWithContext): Action => { - if (params.signingData.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { - return buildBtcSendTransactionAction(params); - } - - throw rpcErrors.methodNotSupported({ - data: params.request.method, - }); + return { + [ACTION_HANDLED_BY_MODULE]: true, + dappInfo: params.request.dappInfo, + signingData: params.signingData, + context: params.request.context, + status: ActionStatus.PENDING, + tabId: params.request.context?.tabId, + params: params.request.params, + displayData: params.displayData, + scope: params.request.chainId, + id: params.request.requestId, + method: params.request.method, + }; }; } diff --git a/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts b/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts deleted file mode 100644 index 3197c74ec..000000000 --- a/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { DetailItemType, RpcMethod } from '@avalabs/vm-module-types'; -import { chainIdToCaip } from '@src/utils/caipConversion'; -import { ACTION_HANDLED_BY_MODULE } from '@src/background/models'; -import { ApprovalParamsWithContext } from '../models'; -import { ActionStatus } from '@src/background/services/actions/models'; -import { buildBtcSendTransactionAction } from './buildBtcSendTransactionAction'; - -describe('src/background/vmModules/helpers/buildBtcSendTransactionAction', () => { - const btcNetwork = { - chainId: ChainId.BITCOIN_TESTNET, - rpcUrl: '', - } as any; - const params: BitcoinSendTransactionParams = { - amount: 100_000_000, - feeRate: 50, - from: 'from', - to: 'to', - }; - const approvalParams: ApprovalParamsWithContext = { - request: { - chainId: chainIdToCaip(btcNetwork.chainId), - method: RpcMethod.BITCOIN_SEND_TRANSACTION, - requestId: 'requestId', - sessionId: 'sessionId', - dappInfo: { - icon: 'icon', - name: 'name', - url: 'https://extension.url', - }, - context: { - tabId: 1234, - }, - params, - }, - displayData: { - details: [ - { - title: 'Transaction Details', - items: [ - { - label: 'From', - type: DetailItemType.ADDRESS, - value: params.from, - }, - ], - }, - ], - network: btcNetwork, - title: 'Approve Transaction', - networkFeeSelector: true, - }, - signingData: { - account: params.from, - type: RpcMethod.BITCOIN_SEND_TRANSACTION, - data: { - amount: params.amount, - balance: {} as any, - gasLimit: 200, - fee: params.feeRate * 200, // 200 bytes vsize - feeRate: params.feeRate, - to: params.to, - inputs: [], - outputs: [], - }, - }, - }; - - it('generates valid action', () => { - expect(buildBtcSendTransactionAction(approvalParams)).toEqual({ - [ACTION_HANDLED_BY_MODULE]: true, - dappInfo: approvalParams.request.dappInfo, - signingData: approvalParams.signingData, - context: approvalParams.request.context, - status: ActionStatus.PENDING, - tabId: approvalParams.request.context?.tabId, - params: approvalParams.request.params, - displayData: approvalParams.displayData, - scope: approvalParams.request.chainId, - id: approvalParams.request.requestId, - method: approvalParams.request.method, - }); - }); -}); diff --git a/src/background/vmModules/helpers/buildBtcSendTransactionAction.ts b/src/background/vmModules/helpers/buildBtcSendTransactionAction.ts deleted file mode 100644 index 4ddd4d967..000000000 --- a/src/background/vmModules/helpers/buildBtcSendTransactionAction.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Action, ActionStatus } from '@src/background/services/actions/models'; -import { ACTION_HANDLED_BY_MODULE } from '@src/background/models'; - -import { ApprovalParamsWithContext } from '../models'; - -export const buildBtcSendTransactionAction = ( - params: ApprovalParamsWithContext -): Action => { - return { - // ActionService needs to know it should not look for the handler in the DI registry, - // but rather just emit the events for the ApprovalController to listen for - [ACTION_HANDLED_BY_MODULE]: true, - dappInfo: params.request.dappInfo, - signingData: params.signingData, - context: params.request.context, - status: ActionStatus.PENDING, - tabId: params.request.context?.tabId, - params: params.request.params, - displayData: params.displayData, - scope: params.request.chainId, - id: params.request.requestId, - method: params.request.method, - }; -}; diff --git a/src/components/common/MaliciousTxAlert.tsx b/src/components/common/MaliciousTxAlert.tsx index 11c8bd217..c9c126d07 100644 --- a/src/components/common/MaliciousTxAlert.tsx +++ b/src/components/common/MaliciousTxAlert.tsx @@ -5,11 +5,20 @@ import { useTranslation } from 'react-i18next'; interface MaliciousTxAlertProps { cancelHandler: () => void; showAlert?: boolean; + title?: string; + description?: string; + actionTitles?: { + reject: string; + proceed: string; + }; } export function MaliciousTxAlert({ showAlert, + title, + description, cancelHandler, + actionTitles, }: MaliciousTxAlertProps) { const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(true); const { t } = useTranslation(); @@ -21,9 +30,12 @@ export function MaliciousTxAlert({ open={isAlertDialogOpen} cancelHandler={cancelHandler} onClose={() => setIsAlertDialogOpen(false)} - title={t('Scam Transaction')} - text={t('This transaction is malicious do not proceed.')} - rejectLabel={t('Reject Transaction')} + title={title || t('Scam Transaction')} + text={ + description || t('This transaction is malicious do not proceed.') + } + proceedLabel={actionTitles?.proceed || t('Proceed Anyway')} + rejectLabel={actionTitles?.reject || t('Reject Transaction')} /> )} diff --git a/src/contexts/BridgeProvider.test.tsx b/src/contexts/BridgeProvider.test.tsx index 358839766..c4d46305a 100644 --- a/src/contexts/BridgeProvider.test.tsx +++ b/src/contexts/BridgeProvider.test.tsx @@ -19,7 +19,7 @@ import { useBridgeContext, } from './BridgeProvider'; import { act } from 'react-dom/test-utils'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; +import { RpcMethod } from '@avalabs/vm-module-types'; const ACTIVE_ACCOUNT_ADDRESS = 'addressC'; @@ -196,7 +196,7 @@ describe('contexts/BridgeProvider', () => { signAndSendEVM(fakeDepositTx); expect(requestFn).toHaveBeenCalledWith({ - method: DAppProviderRequest.ETH_SEND_TX, + method: RpcMethod.BITCOIN_SEND_TRANSACTION, params: [ { ...fakeDepositTx }, { @@ -220,7 +220,7 @@ describe('contexts/BridgeProvider', () => { signAndSendEVM(fakeTransferTx); expect(requestFn).toHaveBeenCalledWith({ - method: DAppProviderRequest.ETH_SEND_TX, + method: RpcMethod.BITCOIN_SEND_TRANSACTION, params: [ { ...fakeTransferTx }, { diff --git a/src/contexts/BridgeProvider.tsx b/src/contexts/BridgeProvider.tsx index 884ac6ce7..464ee9e24 100644 --- a/src/contexts/BridgeProvider.tsx +++ b/src/contexts/BridgeProvider.tsx @@ -34,11 +34,10 @@ import { import { filter, map } from 'rxjs'; import { useConnectionContext } from './ConnectionProvider'; import { useNetworkContext } from './NetworkProvider'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { useAccountsContext } from './AccountsProvider'; -import { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; import type { ContractTransaction } from 'ethers'; import { useTranslation } from 'react-i18next'; +import { RpcMethod } from '@avalabs/vm-module-types'; export interface BridgeContext { createBridgeTransaction(tx: PartialBridgeTransaction): Promise; @@ -229,8 +228,8 @@ function InnerBridgeProvider({ children }: { children: any }) { signAndSendEVM: (txData) => { const tx = txData as ContractTransaction; - return request({ - method: DAppProviderRequest.ETH_SEND_TX, + return request({ + method: RpcMethod.BITCOIN_SEND_TRANSACTION, params: [ { ...tx, diff --git a/src/contexts/SwapProvider/SwapProvider.tsx b/src/contexts/SwapProvider/SwapProvider.tsx index daf413fa8..0f176f40a 100644 --- a/src/contexts/SwapProvider/SwapProvider.tsx +++ b/src/contexts/SwapProvider/SwapProvider.tsx @@ -22,8 +22,7 @@ import { incrementalPromiseResolve } from '@src/utils/incrementalPromiseResolve' import Big from 'big.js'; import { resolve } from '@src/utils/promiseResolver'; import { ethers } from 'ethers'; -import type { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; +import { RpcMethod, TokenType } from '@avalabs/vm-module-types'; import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; import { BN } from 'bn.js'; import { useAnalyticsContext } from '../AnalyticsProvider'; @@ -38,7 +37,6 @@ import { hasParaswapError, DISALLOWED_SWAP_ASSETS, } from './models'; -import { TokenType } from '@avalabs/vm-module-types'; export const SwapContext = createContext({} as any); @@ -360,8 +358,8 @@ export function SwapContextProvider({ children }: { children: any }) { sourceAmount ); const [hash, signError] = await resolve( - request({ - method: DAppProviderRequest.ETH_SEND_TX, + request({ + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { chainId: ChainId.AVALANCHE_MAINNET_ID.toString(), @@ -427,8 +425,8 @@ export function SwapContextProvider({ children }: { children: any }) { } const [swapTxHash, signError] = await resolve( - request({ - method: DAppProviderRequest.ETH_SEND_TX, + request({ + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { chainId: ChainId.AVALANCHE_MAINNET_ID.toString(), diff --git a/src/contexts/UnifiedBridgeProvider.test.tsx b/src/contexts/UnifiedBridgeProvider.test.tsx index d3ee521ec..abfe1b21d 100644 --- a/src/contexts/UnifiedBridgeProvider.test.tsx +++ b/src/contexts/UnifiedBridgeProvider.test.tsx @@ -21,7 +21,7 @@ import { NetworkVMType } from '@avalabs/core-chains-sdk'; import { chainIdToCaip } from '@src/utils/caipConversion'; import { CommonError } from '@src/utils/errors'; import { UnifiedBridgeError } from '@src/background/services/unifiedBridge/models'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; +import { RpcMethod } from '@avalabs/vm-module-types'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; const ACTIVE_ACCOUNT_ADDRESS = 'addressC'; @@ -261,7 +261,7 @@ describe('contexts/UnifiedBridgeProvider', () => { }); expect(requestFn).toHaveBeenCalledWith({ - method: DAppProviderRequest.ETH_SEND_TX, + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { ...allowanceTx }, { @@ -274,7 +274,7 @@ describe('contexts/UnifiedBridgeProvider', () => { ], }); expect(requestFn).toHaveBeenCalledWith({ - method: DAppProviderRequest.ETH_SEND_TX, + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { ...transferTx }, { diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index 3fd1c1210..e1967a537 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -42,9 +42,8 @@ import { JsonRpcApiProvider } from 'ethers'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; import { JsonRpcBatchInternal } from '@avalabs/core-wallets-sdk'; import { NetworkVMType } from '@avalabs/core-chains-sdk'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; -import { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBridge/handlers/unifiedBridgeTrackTransfer'; +import { RpcMethod } from '@avalabs/vm-module-types'; export interface UnifiedBridgeContext { estimateTransferGas( @@ -406,8 +405,8 @@ export function UnifiedBridgeProvider({ assert(to, UnifiedBridgeError.InvalidTxPayload); assert(data, UnifiedBridgeError.InvalidTxPayload); - return request({ - method: DAppProviderRequest.ETH_SEND_TX, + return request({ + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { from, diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 5449194cc..af15750cb 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -413,7 +413,6 @@ "Instant": "Instant", "Insufficient balance": "Insufficient balance", "Insufficient balance for fee": "Insufficient balance for fee", - "Insufficient balance to cover gas costs.
Please add {{symbol}}.": "Insufficient balance to cover gas costs.
Please add {{symbol}}.", "Insufficient balance to cover gas costs.
Please add {{tokenSymbol}}.": "Insufficient balance to cover gas costs.
Please add {{tokenSymbol}}.", "Insufficient balance to cover gas costs.
Please add {{token}}.": "Insufficient balance to cover gas costs.
Please add {{token}}.", "Insufficient balance.": "Insufficient balance.", diff --git a/src/pages/ApproveAction/GenericApprovalScreen.tsx b/src/pages/ApproveAction/GenericApprovalScreen.tsx index 4d401b63d..aaa4f3067 100644 --- a/src/pages/ApproveAction/GenericApprovalScreen.tsx +++ b/src/pages/ApproveAction/GenericApprovalScreen.tsx @@ -4,8 +4,7 @@ import { useGetRequestId } from '@src/hooks/useGetRequestId'; import { useCallback, useEffect, useState } from 'react'; import { LoadingOverlay } from '../../components/common/LoadingOverlay'; import { useTranslation } from 'react-i18next'; -import { DisplayData } from '@avalabs/vm-module-types'; -import { LedgerAppType } from '@src/contexts/LedgerProvider'; +import { AlertType, DisplayData } from '@avalabs/vm-module-types'; import { Box, Button, @@ -27,6 +26,12 @@ import { useFeeCustomizer } from './hooks/useFeeCustomizer'; import { DeviceApproval } from './components/DeviceApproval'; import { NetworkWithCaipId } from '@src/background/services/network/models'; import { useNetworkContext } from '@src/contexts/NetworkProvider'; +import { TxBalanceChange } from '../SignTransaction/components/TxBalanceChange'; +import { AlertBox } from '../Permissions/components/AlertBox'; +import { WarningBox } from '../Permissions/components/WarningBox'; +import { MaliciousTxAlert } from '@src/components/common/MaliciousTxAlert'; +import { SpendLimitInfo } from '../SignTransaction/components/SpendLimitInfo/SpendLimitInfo'; +import { NetworkDetails } from '../SignTransaction/components/ApprovalTxDetails'; export function GenericApprovalScreen() { const { t } = useTranslation(); @@ -37,7 +42,12 @@ export function GenericApprovalScreen() { const isUsingKeystoneWallet = useIsUsingKeystoneWallet(); const [network, setNetwork] = useState(); const { getNetwork } = useNetworkContext(); - const { isCalculatingFee, feeError, renderFeeWidget } = useFeeCustomizer({ + const { + isCalculatingFee, + feeError, + hasEnoughForNetworkFee, + renderFeeWidget, + } = useFeeCustomizer({ actionId: requestId, network, }); @@ -45,12 +55,12 @@ export function GenericApprovalScreen() { const { displayData, context } = action ?? {}; useEffect(() => { - if (!action?.scope) { + if (!displayData?.network?.chainId) { return; } - setNetwork(getNetwork(action.scope)); - }, [getNetwork, action?.scope]); + setNetwork(getNetwork(displayData.network.chainId)); + }, [getNetwork, displayData?.network?.chainId]); const handleRejection = useCallback(() => { cancelHandler(); @@ -67,7 +77,7 @@ export function GenericApprovalScreen() { }, [requestId, updateAction, isUsingLedgerWallet, isUsingKeystoneWallet]); // Make the user switch to the correct app or close the window - useLedgerDisconnectedDialog(handleRejection, LedgerAppType.BITCOIN); + useLedgerDisconnectedDialog(handleRejection); if (!action || !displayData) { return ; @@ -103,6 +113,31 @@ export function GenericApprovalScreen() { + {displayData.alert && ( + + {displayData.alert.type === AlertType.DANGER ? ( + <> + + + + ) : ( + + )} + + )} + @@ -118,9 +153,25 @@ export function GenericApprovalScreen() { item={item} /> ))} + {sectionIndex === 0 && network && ( + + )} ))} + {displayData.balanceChange && ( + + )} + {displayData.tokenApprovals && ( + + )} {displayData.networkFeeSelector && renderFeeWidget()} @@ -160,7 +211,8 @@ export function GenericApprovalScreen() { !displayData || action.status === ActionStatus.SUBMITTING || Boolean(feeError) || - isCalculatingFee + isCalculatingFee || + !hasEnoughForNetworkFee } isLoading={ action.status === ActionStatus.SUBMITTING || isCalculatingFee diff --git a/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx b/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx index ed0e01f04..c7e902eaf 100644 --- a/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx +++ b/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx @@ -1,29 +1,33 @@ -import { BitcoinProvider } from '@avalabs/core-wallets-sdk'; import { Skeleton, Stack } from '@avalabs/core-k2-components'; -import { DisplayData, RpcMethod, SigningData } from '@avalabs/vm-module-types'; +import { + DisplayData, + NetworkTokenWithBalance, + RpcMethod, + SigningData, + TokenType, +} from '@avalabs/vm-module-types'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { NetworkWithCaipId } from '@src/background/services/network/models'; import { NetworkFee } from '@src/background/services/networkFee/models'; -import { ActionStatus } from '@src/background/services/actions/models'; import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; import { CustomFees, GasFeeModifier } from '@src/components/common/CustomFees'; import { useApproveAction } from '@src/hooks/useApproveAction'; -import { buildBtcTx } from '@src/utils/send/btcSendUtils'; import { SendErrorMessage } from '@src/utils/send/models'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; +import { useConnectionContext } from '@src/contexts/ConnectionProvider'; +import { UpdateActionTxDataHandler } from '@src/background/services/actions/handlers/updateTxData'; +import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; +import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -const getInitialFeeRate = (data?: SigningData): bigint => { - if (!data) { - return 0n; - } - - if (data.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { +const getInitialFeeRate = (data?: SigningData): bigint | undefined => { + if (data?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { return BigInt(data.data.feeRate); } - return 0n; + if (data?.type === RpcMethod.ETH_SEND_TRANSACTION) { + return data.data.maxFeePerGas ? BigInt(data.data.maxFeePerGas) : undefined; + } }; export const useFeeCustomizer = ({ @@ -33,7 +37,8 @@ export const useFeeCustomizer = ({ actionId: string; network?: NetworkWithCaipId; }) => { - const { action, updateAction } = useApproveAction(actionId); + const { action } = useApproveAction(actionId); + const { request } = useConnectionContext(); const [networkFee, setNetworkFee] = useState(); const [feeError, setFeeError] = useState(); @@ -44,19 +49,132 @@ export const useFeeCustomizer = ({ GasFeeModifier.NORMAL ); + const tokens = useTokensWithBalances({ + chainId: network?.chainId, + }); + + const nativeToken = useMemo( + () => tokens.find(({ type }) => type === TokenType.NATIVE) ?? null, + [tokens] + ) as NetworkTokenWithBalance | null; + const signingData = useMemo(() => { - if (action?.signingData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { - return action.signingData; + switch (action?.signingData?.type) { + // Request types that we know may require a fee + case RpcMethod.BITCOIN_SEND_TRANSACTION: + case RpcMethod.AVALANCHE_SEND_TRANSACTION: + case RpcMethod.ETH_SEND_TRANSACTION: + return action.signingData; + + default: + return undefined; } }, [action]); + const updateFee = useCallback( + async (maxFeeRate: bigint, maxTipRate?: bigint) => { + if (!actionId) { + return; + } + + const newFeeConfig = + signingData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION + ? { feeRate: Number(maxFeeRate) } + : { maxFeeRate, maxTipRate }; + + await request({ + method: ExtensionRequest.ACTION_UPDATE_TX_DATA, + params: [actionId, newFeeConfig], + }); + }, + [actionId, request, signingData?.type] + ); + + const getFeeInfo = useCallback((data: SigningData) => { + switch (data.type) { + case RpcMethod.AVALANCHE_SIGN_MESSAGE: + case RpcMethod.ETH_SIGN: + case RpcMethod.PERSONAL_SIGN: { + throw new Error( + `Unable to render fee widget for non-transaction (${data.type})` + ); + } + + case RpcMethod.BITCOIN_SEND_TRANSACTION: { + return { + feeRate: BigInt(data.data.feeRate), + limit: Math.ceil(data.data.fee / data.data.feeRate), + }; + } + + case RpcMethod.ETH_SEND_TRANSACTION: { + return { + feeRate: data.data.maxFeePerGas ? BigInt(data.data.maxFeePerGas) : 0n, + maxTipRate: data.data.maxPriorityFeePerGas + ? BigInt(data.data.maxPriorityFeePerGas) + : 0n, + limit: Number(data.data.gasLimit ?? 0), + }; + } + + default: + throw new Error(`Unable to render fee widget for ${data.type}`); + } + }, []); + + const hasEnoughForNetworkFee = useMemo(() => { + if (!nativeToken?.balance || !signingData) { + return false; + } + + const info = getFeeInfo(signingData); + const need = info.feeRate * BigInt(info.limit); + + return nativeToken.balance > need; + }, [getFeeInfo, nativeToken?.balance, signingData]); + + useEffect(() => { + const nativeBalance = nativeToken?.balance; + + if (!nativeBalance || !signingData) { + return; + } + + const info = getFeeInfo(signingData); + const need = info.feeRate * BigInt(info.limit); + + setFeeError( + nativeToken.balance >= need + ? undefined + : SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE + ); + }, [getFeeInfo, nativeToken?.balance, signingData]); + const [maxFeePerGas, setMaxFeePerGas] = useState( getInitialFeeRate(signingData) ); + const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState( + networkFee?.low?.maxTip + ); + + useEffect(() => { + if (!networkFee) { + return; + } + + // Initialize fee config with default values if they are not set at all + setMaxFeePerGas((previous) => previous ?? networkFee.low.maxFee); + setMaxPriorityFeePerGas((previous) => previous ?? networkFee.low.maxTip); + }, [networkFee]); const setCustomFee = useCallback( - (values: { maxFeePerGas: bigint; feeType: GasFeeModifier }) => { + (values: { + maxFeePerGas: bigint; + feeType: GasFeeModifier; + maxPriorityFeePerGas: bigint; + }) => { setMaxFeePerGas(values.maxFeePerGas); + setMaxPriorityFeePerGas(values.maxPriorityFeePerGas); setGasFeeModifier(values.feeType); }, [] @@ -81,113 +199,33 @@ export const useFeeCustomizer = ({ }; }, [getNetworkFee, network]); - const getUpdatedSigningData = useCallback( - async function ( - oldSigningData: T, - newMaxFeePerGas: bigint - ): Promise { - if (!network) { - throw new Error('Not ready yet'); - } - - if (oldSigningData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { - const tx = await buildBtcTx( - oldSigningData.account, - getProviderForNetwork(network) as BitcoinProvider, - { - amount: oldSigningData.data.amount, - address: oldSigningData.data.to, - token: oldSigningData.data.balance, - feeRate: Number(newMaxFeePerGas), - } - ); - - if (oldSigningData.data.amount > 0 && !tx.psbt) { - throw SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE; - } - - return { - ...oldSigningData, - data: { - ...oldSigningData.data, - ...tx, - feeRate: Number(newMaxFeePerGas), - }, - }; - } - - throw SendErrorMessage.UNKNOWN_ERROR; - }, - [network] - ); - useEffect(() => { - if (!maxFeePerGas || !signingData) { + if (typeof maxFeePerGas === 'undefined') { return; } let isMounted = true; setIsCalculatingFee(true); - getUpdatedSigningData(signingData, maxFeePerGas) - .then((newSigningData) => { - if (!isMounted || action?.status !== ActionStatus.PENDING) { - return; - } - - // Prevent infinite re-renders, only update the action if the feeRate actually changed - if (signingData.data.feeRate === newSigningData.data.feeRate) { - return; - } - - setFeeError(undefined); - updateAction({ - id: actionId, - status: ActionStatus.PENDING, - signingData: newSigningData, - }); - }) + updateFee(maxFeePerGas, maxPriorityFeePerGas) .catch((err) => { console.error(err); + if (!isMounted) { + return; + } setFeeError(err); }) .finally(() => { + if (!isMounted) { + return; + } setIsCalculatingFee(false); }); return () => { isMounted = false; }; - }, [ - actionId, - action?.status, - getUpdatedSigningData, - maxFeePerGas, - updateAction, - signingData, - ]); - - const getFeeInfo = useCallback((data: SigningData) => { - switch (data.type) { - case RpcMethod.AVALANCHE_SIGN_MESSAGE: - case RpcMethod.ETH_SIGN: - case RpcMethod.PERSONAL_SIGN: { - throw new Error( - `Unable to render fee widget for non-transaction (${data.type})` - ); - } - - case RpcMethod.BITCOIN_SEND_TRANSACTION: { - return { - feeRate: BigInt(data.data.feeRate), - limit: Math.ceil(data.data.fee / data.data.feeRate), - }; - } - - default: - throw new Error(`Unable to render fee widget for ${data.type}`); - } - }, []); + }, [maxFeePerGas, maxPriorityFeePerGas, updateFee]); const renderFeeWidget = useCallback(() => { if (!networkFee || !signingData) { @@ -222,6 +260,7 @@ export const useFeeCustomizer = ({ return { isCalculatingFee, + hasEnoughForNetworkFee, renderFeeWidget, feeError, }; diff --git a/src/pages/Permissions/Permissions.tsx b/src/pages/Permissions/Permissions.tsx index 161ba8331..24e29a271 100644 --- a/src/pages/Permissions/Permissions.tsx +++ b/src/pages/Permissions/Permissions.tsx @@ -244,6 +244,7 @@ export function PermissionsPage() { {featureFlags[FeatureGates.BLOCKAID_DAPP_SCAN] && isMaliciousDApp && ( { cancelHandler(); window.close(); diff --git a/src/pages/Permissions/components/AlertDialog.tsx b/src/pages/Permissions/components/AlertDialog.tsx index 060330f2f..21df11632 100644 --- a/src/pages/Permissions/components/AlertDialog.tsx +++ b/src/pages/Permissions/components/AlertDialog.tsx @@ -6,7 +6,6 @@ import { useTheme, RemoveModeratorIcon, } from '@avalabs/core-k2-components'; -import { useTranslation } from 'react-i18next'; interface AlertDialogProps { cancelHandler: () => void; @@ -15,6 +14,7 @@ interface AlertDialogProps { title: string; text: string; rejectLabel: string; + proceedLabel: string; } export function AlertDialog({ @@ -24,9 +24,9 @@ export function AlertDialog({ title, text, rejectLabel, + proceedLabel, }: AlertDialogProps) { const theme = useTheme(); - const { t } = useTranslation(); return ( - {t('Proceed Anyway')} + {proceedLabel} diff --git a/src/pages/Send/hooks/useSend/useEVMSend.ts b/src/pages/Send/hooks/useSend/useEVMSend.ts index 5f3d61cbb..2310253f8 100644 --- a/src/pages/Send/hooks/useSend/useEVMSend.ts +++ b/src/pages/Send/hooks/useSend/useEVMSend.ts @@ -1,10 +1,7 @@ import { useCallback, useState } from 'react'; import { SendErrorMessage } from '@src/utils/send/models'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { useConnectionContext } from '@src/contexts/ConnectionProvider'; -import type { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; -import { EthSendTransactionParams } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; import { buildTx, @@ -20,7 +17,7 @@ import { SendOptions, } from '../../models'; import { SendAdapterEVM } from './models'; -import { TokenType } from '@avalabs/vm-module-types'; +import { RpcMethod, TokenType } from '@avalabs/vm-module-types'; import { stringToBigint } from '@src/utils/stringToBigint'; export const useEVMSend: SendAdapterEVM = ({ @@ -49,15 +46,11 @@ export const useEVMSend: SendAdapterEVM = ({ const tx = await getTx(options); - const hash = await request< - EthSendTransactionHandler, - DAppProviderRequest.ETH_SEND_TX, - string - >({ - method: DAppProviderRequest.ETH_SEND_TX, + const hash = await request({ + method: RpcMethod.ETH_SEND_TRANSACTION, params: [ { - ...(tx as EthSendTransactionParams), + ...tx, chainId, }, ], diff --git a/src/pages/Send/utils/buildSendTx.test.ts b/src/pages/Send/utils/buildSendTx.test.ts index 2cb561e3c..a58fcba86 100644 --- a/src/pages/Send/utils/buildSendTx.test.ts +++ b/src/pages/Send/utils/buildSendTx.test.ts @@ -1,11 +1,11 @@ import { Contract } from 'ethers'; -import { stringToBN } from '@avalabs/core-utils-sdk'; import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json'; import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'; import ERC1155 from '@openzeppelin/contracts/build/contracts/ERC1155.json'; import * as builder from './buildSendTx'; import { TokenType } from '@avalabs/vm-module-types'; +import { stringToBigint } from '@src/utils/stringToBigint'; jest.mock('ethers'); @@ -51,7 +51,9 @@ describe('src/pages/Send/utils/buildSendTx', () => { expect(populateTransaction).toHaveBeenCalledWith( options.address, - stringToBN(options.amount, options.token.decimals).toString() + `0x${stringToBigint(options.amount, options.token.decimals).toString( + 16 + )}` ); }); @@ -189,7 +191,7 @@ describe('src/pages/Send/utils/buildSendTx', () => { expect(builder.buildNativeTx(from, options)).toEqual({ from, to: options.address, - value: stringToBN(options.amount, options.token.decimals).toString(), + value: '0x2bdc545d587500', }); }); }); diff --git a/src/pages/Send/utils/buildSendTx.ts b/src/pages/Send/utils/buildSendTx.ts index 9065a4f1c..ce0923503 100644 --- a/src/pages/Send/utils/buildSendTx.ts +++ b/src/pages/Send/utils/buildSendTx.ts @@ -1,7 +1,6 @@ import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json'; import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'; import ERC1155 from '@openzeppelin/contracts/build/contracts/ERC1155.json'; -import { stringToBN } from '@avalabs/core-utils-sdk'; import { JsonRpcBatchInternal } from '@avalabs/core-wallets-sdk'; import { Contract, TransactionRequest } from 'ethers'; @@ -12,6 +11,9 @@ import { SendOptions, } from '../models'; import { TokenType } from '@avalabs/vm-module-types'; +import { stringToBigint } from '@src/utils/stringToBigint'; + +const asHex = (value: bigint) => `0x${value.toString(16)}`; export const buildErc20Tx = async ( from: string, @@ -22,7 +24,7 @@ export const buildErc20Tx = async ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const populatedTransaction = await contract.transfer!.populateTransaction( address, - stringToBN(amount, token.decimals).toString() + asHex(stringToBigint(amount, token.decimals)) ); const unsignedTx: TransactionRequest = { ...populatedTransaction, // only includes `to` and `data` @@ -86,7 +88,7 @@ export const buildNativeTx = ( ): TransactionRequest => ({ from, to: address, - value: stringToBN(amount, token.decimals).toString(), + value: asHex(stringToBigint(amount, token.decimals)), }); export const isNativeSend = ( diff --git a/src/pages/SignTransaction/SignTransaction.tsx b/src/pages/SignTransaction/SignTransaction.tsx deleted file mode 100644 index c120e4383..000000000 --- a/src/pages/SignTransaction/SignTransaction.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { - Alert, - AlertContent, - AlertTitle, - Box, - Button, - CodeIcon, - IconButton, - LoadingDots, - Scrollbars, - Skeleton, - Stack, - Typography, -} from '@avalabs/core-k2-components'; -import { useGetRequestId } from '@src/hooks/useGetRequestId'; -import { useCallback, useMemo, useState } from 'react'; -import { useGetTransaction } from './hooks/useGetTransaction'; -import { LedgerApprovalOverlay } from './components/LedgerApprovalOverlay'; -import { SignTxErrorBoundary } from './components/SignTxErrorBoundary'; -import { useLedgerDisconnectedDialog } from './hooks/useLedgerDisconnectedDialog'; -import { TransactionProgressState } from './models'; -import { useWindowGetsClosedOrHidden } from '@src/utils/useWindowGetsClosedOrHidden'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import { Trans, useTranslation } from 'react-i18next'; -import { RawTransactionData } from './components/RawTransactionData'; -import { CustomFees } from '@src/components/common/CustomFees'; -import { useSignTransactionHeader } from './hooks/useSignTransactionHeader'; -import useIsUsingLedgerWallet from '@src/hooks/useIsUsingLedgerWallet'; -import { KeystoneApprovalOverlay } from './components/KeystoneApprovalOverlay'; -import useIsUsingKeystoneWallet from '@src/hooks/useIsUsingKeystoneWallet'; -import Dialog from '@src/components/common/Dialog'; -import { TransactionErrorDialog } from './components/TransactionErrorDialog'; -import { WalletConnectApprovalOverlay } from './components/WalletConnectApproval/WalletConnectApprovalOverlay'; -import useIsUsingWalletConnectAccount from '@src/hooks/useIsUsingWalletConnectAccount'; -import { useApprovalHelpers } from '@src/hooks/useApprovalHelpers'; -import useIsUsingFireblocksAccount from '@src/hooks/useIsUsingFireblocksAccount'; -import { FireblocksApprovalOverlay } from './components/FireblocksApproval/FireblocksApprovalOverlay'; -import { TxBalanceChange } from './components/TxBalanceChange'; -import { TransactionActionInfo } from './components/TransactionActionInfo/TransactionActionInfo'; -import { - ApprovalSection, - ApprovalSectionBody, - ApprovalSectionHeader, -} from '@src/components/common/approval/ApprovalSection'; -import { NetworkDetails, WebsiteDetails } from './components/ApprovalTxDetails'; -import { getToAddressesFromTransaction } from './utils/getToAddressesFromTransaction'; -import { SpendLimitInfo } from './components/SpendLimitInfo/SpendLimitInfo'; -import { ActionStatus } from '@src/background/services/actions/models'; -import { MaliciousTxAlert } from '@src/components/common/MaliciousTxAlert'; -import { TxWarningBox } from '@src/components/common/TxWarningBox'; -import { NetworkTokenWithBalance, TokenType } from '@avalabs/vm-module-types'; - -export function SignTransactionPage() { - const { t } = useTranslation(); - const requestId = useGetRequestId(); - const onTxError = useCallback(() => { - window.close(); - }, []); - const { - updateTransaction, - setCustomFee, - showRawTransactionData, - setShowRawTransactionData, - selectedGasFee, - network, - networkFee, - hasTransactionError, - setHasTransactionError, - transaction, - maxFeePerGas, - gasLimit, - fee, - } = useGetTransaction(requestId); - - const [transactionProgressState, setTransactionProgressState] = useState( - TransactionProgressState.NOT_APPROVED - ); - const tokens = useTokensWithBalances({ - chainId: network?.chainId, - }); - const header = useSignTransactionHeader(transaction?.displayData); - const isUsingLedgerWallet = useIsUsingLedgerWallet(); - const isUsingKeystoneWallet = useIsUsingKeystoneWallet(); - const isUsingWalletConnectAccount = useIsUsingWalletConnectAccount(); - const isUsingFireblocksAccount = useIsUsingFireblocksAccount(); - - const nativeTokenWithBalance = useMemo( - () => tokens.find(({ type }) => type === TokenType.NATIVE), - [tokens] - ) as NetworkTokenWithBalance; // This screen only shows up for EVM - - const hasEnoughForNetworkFee = useMemo(() => { - return ( - nativeTokenWithBalance?.balance >= - (transaction?.displayData?.displayValues?.gas.maxFeePerGas - ? transaction.displayData?.displayValues?.gas.maxFeePerGas * - BigInt(transaction.displayData?.displayValues?.gas.gasLimit || 0n) - : 0n) - ); - }, [ - nativeTokenWithBalance?.balance, - transaction?.displayData?.displayValues?.gas.gasLimit, - transaction?.displayData?.displayValues?.gas.maxFeePerGas, - ]); - - const cancelHandler = useCallback(() => { - if (transaction?.actionId) { - updateTransaction({ - status: ActionStatus.ERROR_USER_CANCELED, - id: transaction?.actionId, - }); - } - }, [transaction?.actionId, updateTransaction]); - - useLedgerDisconnectedDialog(cancelHandler, undefined, network); - useWindowGetsClosedOrHidden(cancelHandler); - - const isReadyForApproval = - nativeTokenWithBalance && - transactionProgressState === TransactionProgressState.NOT_APPROVED; - - const submit = useCallback(async () => { - setTransactionProgressState(TransactionProgressState.PENDING); - await updateTransaction( - { - status: ActionStatus.SUBMITTING, - id: transaction?.actionId, - }, - true - ); - }, [transaction?.actionId, updateTransaction]); - - const { handleApproval, handleRejection, isApprovalOverlayVisible } = - useApprovalHelpers({ - onApprove: submit, - onReject: cancelHandler, - }); - - if (!transaction) { - return ( - - - - ); - } - - if (showRawTransactionData) { - return ( - setShowRawTransactionData(false)} - /> - ); - } - - const isTransactionMalicious = - transaction.displayData.displayValues.isMalicious; - const isTransactionSuspicious = - transaction.displayData.displayValues.isSuspicious; - - const { contextInformation } = transaction.displayData.displayOptions ?? {}; - - return ( - <> - - {isApprovalOverlayVisible && ( - <> - {isUsingLedgerWallet && ( - - )} - - {isUsingKeystoneWallet && ( - - )} - - {isUsingWalletConnectAccount && ( - - )} - {isUsingFireblocksAccount && ( - - )} - - )} - - {/* Header */} - {header && ( - - {header} - - )} - {contextInformation && ( - - - {contextInformation.title} - {contextInformation.notice && ( - {contextInformation.notice} - )} - - - )} - {/* Actions */} - - - - - - - - - setShowRawTransactionData(true)} - > - - - - - {transaction.site && ( - - )} - {network && } - - - - - - - - - - {maxFeePerGas !== undefined && gasLimit ? ( - - ) : ( - <> - - - - )} - - {!hasEnoughForNetworkFee && ( - - - - - - )} - - - - - - {/* Action Buttons */} - - - - - - window.close()} - isCloseable={false} - open={hasTransactionError} - content={ - { - updateTransaction({ - status: ActionStatus.ERROR, - id: transaction?.actionId, - error: 'Invalid param: chainId', - }); - setHasTransactionError(false); - if (onTxError) { - onTxError(); - } - }} - /> - } - /> - - ); -} diff --git a/src/pages/SignTransaction/components/NftAccordion.tsx b/src/pages/SignTransaction/components/NftAccordion.tsx index 026ed11b4..10bb00312 100644 --- a/src/pages/SignTransaction/components/NftAccordion.tsx +++ b/src/pages/SignTransaction/components/NftAccordion.tsx @@ -5,25 +5,34 @@ import { Typography, AccordionDetails, } from '@avalabs/core-k2-components'; -import { TransactionNft } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; import { CollectibleMedia } from '@src/pages/Collectibles/components/CollectibleMedia'; import { TransactionTokenCard, TransactionTokenCardVariant, } from './TransactionTokenCard'; +import { + NetworkContractToken, + NetworkToken, + TokenDiffItem, +} from '@avalabs/vm-module-types'; interface NftAccordionProps { - nftList: TransactionNft[] | undefined; + diffItems: TokenDiffItem[]; + token: NetworkContractToken | NetworkToken; + variant: TransactionTokenCardVariant; } -export const NftAccordion = ({ nftList }: NftAccordionProps) => { - if (!nftList) { +export const NftAccordion = ({ + token, + diffItems, + variant, +}: NftAccordionProps) => { + if (!diffItems.length) { return null; } - const firstNft = nftList[0]; return ( - + { height="32px" width="auto" maxWidth="32px" - url={firstNft?.logoUri} + url={token.logoUri} hover={false} showPlayIcon={false} /> @@ -46,35 +55,26 @@ export const NftAccordion = ({ nftList }: NftAccordionProps) => { fontWeight="fontWeightSemibold" sx={{ ml: 2 }} > - {firstNft?.name} {firstNft?.size && `(${firstNft?.size})`} + {token.name} {diffItems.length ? `(${diffItems.length})` : ''} - {nftList.map((nft, index) => { - return ( - + + + {diffItems.map((item, index) => ( - - - - ); - })} + /> + ))} + + ); }; diff --git a/src/pages/SignTransaction/components/SpendLimitInfo/CustomSpendLimit.tsx b/src/pages/SignTransaction/components/SpendLimitInfo/CustomSpendLimit.tsx index c90eb253a..df1c12479 100644 --- a/src/pages/SignTransaction/components/SpendLimitInfo/CustomSpendLimit.tsx +++ b/src/pages/SignTransaction/components/SpendLimitInfo/CustomSpendLimit.tsx @@ -16,7 +16,7 @@ import { BNInput } from '@src/components/common/BNInput'; import { PageTitle } from '@src/components/common/PageTitle'; import { DomainMetadata } from '@src/background/models'; import { Limit, SpendLimit } from './TokenSpendLimit'; -import { TransactionToken } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; +import { ERC20Token } from '@avalabs/vm-module-types'; const SpendLimitOption = ({ label, value, checked, ...props }) => ( - - - - - - - - - - - ); -} diff --git a/src/pages/SignTransaction/components/SpendLimitInfo/NftSpendLimit.tsx b/src/pages/SignTransaction/components/SpendLimitInfo/NftSpendLimit.tsx index 2bc38a37c..754999b4f 100644 --- a/src/pages/SignTransaction/components/SpendLimitInfo/NftSpendLimit.tsx +++ b/src/pages/SignTransaction/components/SpendLimitInfo/NftSpendLimit.tsx @@ -1,14 +1,22 @@ import { Stack } from '@avalabs/core-k2-components'; import { useTranslation } from 'react-i18next'; +import { + ERC1155Token, + ERC721Token, + TokenApproval, +} from '@avalabs/vm-module-types'; + import { ApprovalSection, ApprovalSectionHeader, } from '@src/components/common/approval/ApprovalSection'; -import { TransactionNft } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { CollectibleMedia } from '@src/pages/Collectibles/components/CollectibleMedia'; import { TransactionTokenCard } from '../TransactionTokenCard'; -export function NftSpendLimit({ token }: { token: TransactionNft }) { +export function NftSpendLimit({ + approval, +}: { + approval: TokenApproval & { token: ERC1155Token | ERC721Token }; +}) { const { t } = useTranslation(); return ( @@ -20,17 +28,14 @@ export function NftSpendLimit({ token }: { token: TransactionNft }) { > - - - + ); diff --git a/src/pages/SignTransaction/components/SpendLimitInfo/SpendLimitInfo.tsx b/src/pages/SignTransaction/components/SpendLimitInfo/SpendLimitInfo.tsx index 1ea47219a..77cfd9689 100644 --- a/src/pages/SignTransaction/components/SpendLimitInfo/SpendLimitInfo.tsx +++ b/src/pages/SignTransaction/components/SpendLimitInfo/SpendLimitInfo.tsx @@ -1,55 +1,50 @@ -import { - Transaction, - TransactionType, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; import { TokenSpendLimit } from './TokenSpendLimit'; import { NftSpendLimit } from './NftSpendLimit'; -import { NftCollectionSpendLimit } from './NftCollectionSpendLimit'; -import { Action } from '@src/background/services/actions/models'; - -export const SpendLimitInfo = ({ - transaction, - updateTransaction, -}: { - transaction: Action | null; - updateTransaction: (update: any) => Promise; -}) => { - const approveTransactions = - transaction?.displayData?.displayValues?.actions.filter((action) => - [ - TransactionType.APPROVE_TOKEN, - TransactionType.REVOKE_TOKEN_APPROVAL, - TransactionType.APPROVE_NFT, - TransactionType.REVOKE_NFT_APPROVAL, - TransactionType.APPROVE_NFT_COLLECTION, - TransactionType.REVOKE_NFT_COLLECTION_APPROVAL, - ].includes(action.type) - ); +import { + ERC1155Token, + ERC20Token, + ERC721Token, + TokenApproval, + TokenApprovals, + TokenType, +} from '@avalabs/vm-module-types'; - if (!approveTransactions?.length || !transaction) { - return null; - } +type SpendLimitInfoProps = TokenApprovals & { actionId: string }; +export const SpendLimitInfo = ({ + approvals, + isEditable, + actionId, +}: SpendLimitInfoProps) => { return ( <> - {approveTransactions.map((action, index) => { - switch (action.type) { - case TransactionType.APPROVE_TOKEN: - case TransactionType.REVOKE_TOKEN_APPROVAL: + {approvals.map((approval) => { + switch (approval.token.type) { + case TokenType.ERC721: + case TokenType.ERC1155: + return ( + + ); + + case TokenType.ERC20: return ( ); - case TransactionType.APPROVE_NFT: - case TransactionType.REVOKE_NFT_APPROVAL: - return ; - case TransactionType.APPROVE_NFT_COLLECTION: - case TransactionType.REVOKE_NFT_COLLECTION_APPROVAL: - return ; + default: return null; } diff --git a/src/pages/SignTransaction/components/SpendLimitInfo/TokenSpendLimit.tsx b/src/pages/SignTransaction/components/SpendLimitInfo/TokenSpendLimit.tsx index f61687937..976bc5e3c 100644 --- a/src/pages/SignTransaction/components/SpendLimitInfo/TokenSpendLimit.tsx +++ b/src/pages/SignTransaction/components/SpendLimitInfo/TokenSpendLimit.tsx @@ -4,22 +4,20 @@ import { ApprovalSection, ApprovalSectionHeader, } from '@src/components/common/approval/ApprovalSection'; -import { - Transaction, - TransactionToken, -} from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; import { useCallback, useState } from 'react'; import { CustomSpendLimit } from './CustomSpendLimit'; import { TransactionTokenCard } from '../TransactionTokenCard'; -import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json'; import { MaxUint256 } from 'ethers'; -import Web3 from 'web3'; -import { TokenIcon } from '@src/components/common/TokenIcon'; import { - Action, - ActionStatus, - ActionUpdate, -} from '@src/background/services/actions/models'; + DisplayData, + ERC20Token, + TokenApproval, +} from '@avalabs/vm-module-types'; +import { TokenUnit } from '@avalabs/core-utils-sdk'; +import { useApproveAction } from '@src/hooks/useApproveAction'; +import { useConnectionContext } from '@src/contexts/ConnectionProvider'; +import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; +import { UpdateActionTxDataHandler } from '@src/background/services/actions/handlers/updateTxData'; export enum Limit { DEFAULT = 'DEFAULT', @@ -34,24 +32,17 @@ export interface SpendLimit { } export function TokenSpendLimit({ - spender, - token, - transaction, - updateTransaction, + actionId, + approval, + isEditable, }: { - spender: { - address: string; - protocol?: { - id: string; - name: string; - logoUri: string; - }; - }; - token: TransactionToken; - transaction: Action; - updateTransaction: (update: ActionUpdate) => Promise; + actionId: string; + approval: TokenApproval & { token: ERC20Token }; + isEditable: boolean; }) { const { t } = useTranslation(); + const { action } = useApproveAction(actionId); + const { request } = useConnectionContext(); const [showCustomSpendLimit, setShowCustomSpendLimit] = useState(false); const [customSpendLimit, setCustomSpendLimit] = useState({ limitType: Limit.DEFAULT, @@ -59,52 +50,42 @@ export function TokenSpendLimit({ const setSpendLimit = useCallback( (customSpendData: SpendLimit) => { - let limitAmount = ''; + let limitAmount = 0n; if (customSpendData.limitType === Limit.UNLIMITED) { setCustomSpendLimit({ ...customSpendData, value: undefined, }); - limitAmount = `0x${MaxUint256.toString(16)}`; + limitAmount = MaxUint256; } else { setCustomSpendLimit(customSpendData); limitAmount = customSpendData.limitType === Limit.CUSTOM - ? (customSpendData.value ?? 0n).toString() - : (token.amount ?? 0n).toString(); + ? customSpendData.value ?? 0n + : BigInt(approval.value ?? 0n); } - - // create hex string for approval amount - const web3 = new Web3(); - const contract = new web3.eth.Contract(ERC20.abi as any, token.address); - - const hashedCustomSpend = - limitAmount && - contract.methods.approve(spender.address, limitAmount).encodeABI(); - - updateTransaction({ - id: transaction?.actionId, - status: ActionStatus.PENDING, - displayData: { - ...transaction.displayData, - txParams: { - ...transaction.displayData.txParams, - data: hashedCustomSpend, - }, - }, + request({ + method: ExtensionRequest.ACTION_UPDATE_TX_DATA, + params: [actionId, { approvalLimit: `0x${limitAmount.toString(16)}` }], }); }, - [ - token.address, - token.amount, - spender.address, - updateTransaction, - transaction?.actionId, - transaction.displayData, - ] + [actionId, request, approval.value] ); + const isInfinite = customSpendLimit.limitType === Limit.UNLIMITED; + const diffItemValue = isInfinite + ? null + : new TokenUnit( + customSpendLimit.limitType === Limit.DEFAULT + ? typeof approval.value === 'string' + ? BigInt(approval.value) + : 0n + : customSpendLimit.value ?? '0', + approval.token.decimals, + '' + ); + return ( <> @@ -118,9 +99,9 @@ export function TokenSpendLimit({ setShowCustomSpendLimit(false)} /> @@ -129,7 +110,7 @@ export function TokenSpendLimit({ - {token.amount && ( + {isEditable && approval.value && (