From 3557334c6c563463e7a33bbac27c792b7db59077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 5 Sep 2024 15:53:40 +0200 Subject: [PATCH] refactor: bitcoin send transaction (#18) --- .releaserc.js | 15 + jest.config.js | 1 + package.json | 14 +- .../DAppConnectionController.ts | 8 +- .../connections/dAppConnection/models.ts | 5 +- .../connections/dAppConnection/registry.ts | 2 - .../ExtensionConnectionController.ts | 27 +- .../middlewares/ActiveNetworkMiddleware.ts | 35 + .../DAppRequestHandlerMiddleware.ts | 57 +- .../ExtensionRequestHandlerMiddleware.ts | 66 +- .../middlewares/RPCCallsMiddleware.ts | 2 +- .../middlewares/SiteMetadataMiddleware.ts | 1 + .../connections/middlewares/models.ts | 2 + src/background/connections/models.ts | 31 +- src/background/models.ts | 3 + .../providers/ChainAgnosticProvider.ts | 5 +- .../providers/initializeInpageProvider.ts | 4 + src/background/runtime/BackgroundRuntime.ts | 8 +- .../runtime/openApprovalWindow.test.ts | 26 + src/background/runtime/openApprovalWindow.ts | 2 +- .../services/actions/ActionsService.test.ts | 49 +- .../services/actions/ActionsService.ts | 27 +- src/background/services/actions/models.ts | 9 +- .../services/approvals/ApprovalService.ts | 37 +- .../services/balances/BalancesServiceBTC.ts | 9 +- .../services/bridge/BridgeService.ts | 1 + .../handlers/avalanche_bridgeAsset.test.ts | 2 +- .../services/history/HistoryServiceBTC.ts | 7 +- .../handlers/avalanche_signMessage.test.ts | 2 +- .../messages/handlers/signMessage.test.ts | 2 +- .../services/network/NetworkService.ts | 3 +- .../events/networksUpdatedEventListener.ts | 8 +- .../handlers/wallet_addEthereumChain.test.ts | 865 +++++++----------- .../wallet_switchEthereumChain.test.ts | 52 +- src/background/services/network/models.ts | 8 - .../services/network/utils/getSyncDomain.ts | 7 +- .../networkFee/NetworkFeeService.test.ts | 15 +- .../services/networkFee/NetworkFeeService.ts | 6 +- .../wallet_requestPermissions.test.ts | 40 +- .../avalanche_sendTransaction.test.ts | 2 +- .../handlers/avalanche_sendTransaction.ts | 12 +- .../avalanche_signTransaction.test.ts | 2 +- .../handlers/bitcoin_sendTransaction.test.ts | 478 ---------- .../handlers/bitcoin_sendTransaction.ts | 329 ------- .../eth_sendTransaction.test.ts | 4 +- .../services/web3/handlers/connect.test.ts | 36 +- .../vmModules/ApprovalController.test.ts | 266 ++++++ .../vmModules/ApprovalController.ts | 205 +++++ .../vmModules/ModuleManager.test.ts | 49 +- src/background/vmModules/ModuleManager.ts | 34 +- .../buildBtcSendTransactionAction.test.ts | 85 ++ .../helpers/buildBtcSendTransactionAction.ts | 24 + .../vmModules/mocks/avm.manifest.json | 42 - src/background/vmModules/mocks/avm.ts | 62 -- .../vmModules/mocks/coreEth.manifest.json | 39 - src/background/vmModules/mocks/coreEth.ts | 61 -- .../vmModules/mocks/evm.manifest.json | 39 - src/background/vmModules/mocks/evm.ts | 60 -- .../vmModules/mocks/pvm.manifest.json | 42 - src/background/vmModules/mocks/pvm.ts | 60 -- src/background/vmModules/models.ts | 11 + .../common/approval/TransactionDetailItem.tsx | 137 +++ src/contexts/AccountsProvider.tsx | 7 +- src/contexts/BalancesProvider.tsx | 34 + src/contexts/ConnectionProvider.tsx | 18 +- src/contexts/NetworkProvider.tsx | 26 +- .../utils/connectionResponseMapper.ts | 4 +- src/hooks/useApproveAction.ts | 5 + src/localization/locales/en/translation.json | 2 - src/pages/ApproveAction/BitcoinSignTx.tsx | 393 -------- .../ApproveAction/GenericApprovalScreen.tsx | 182 ++++ .../components/DeviceApproval.tsx | 64 ++ .../ApproveAction/hooks/useFeeCustomizer.tsx | 228 +++++ src/pages/Bridge/hooks/useBtcBridge.test.ts | 23 +- src/pages/Bridge/hooks/useBtcBridge.ts | 33 +- src/pages/Send/hooks/useSend/useBTCSend.ts | 21 +- src/popup/ApprovalRoutes.tsx | 14 +- src/types/globals.d.ts | 1 + src/utils/actions/getUpdatedActionData.ts | 17 + webpack.inpage.js | 10 +- yarn.lock | 152 +-- 81 files changed, 2238 insertions(+), 2538 deletions(-) create mode 100644 src/background/connections/middlewares/ActiveNetworkMiddleware.ts create mode 100644 src/background/runtime/openApprovalWindow.test.ts delete mode 100644 src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts delete mode 100644 src/background/services/wallet/handlers/bitcoin_sendTransaction.ts create mode 100644 src/background/vmModules/ApprovalController.test.ts create mode 100644 src/background/vmModules/ApprovalController.ts create mode 100644 src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts create mode 100644 src/background/vmModules/helpers/buildBtcSendTransactionAction.ts delete mode 100644 src/background/vmModules/mocks/avm.manifest.json delete mode 100644 src/background/vmModules/mocks/avm.ts delete mode 100644 src/background/vmModules/mocks/coreEth.manifest.json delete mode 100644 src/background/vmModules/mocks/coreEth.ts delete mode 100644 src/background/vmModules/mocks/evm.manifest.json delete mode 100644 src/background/vmModules/mocks/evm.ts delete mode 100644 src/background/vmModules/mocks/pvm.manifest.json delete mode 100644 src/background/vmModules/mocks/pvm.ts create mode 100644 src/components/common/approval/TransactionDetailItem.tsx delete mode 100644 src/pages/ApproveAction/BitcoinSignTx.tsx create mode 100644 src/pages/ApproveAction/GenericApprovalScreen.tsx create mode 100644 src/pages/ApproveAction/components/DeviceApproval.tsx create mode 100644 src/pages/ApproveAction/hooks/useFeeCustomizer.tsx create mode 100644 src/utils/actions/getUpdatedActionData.ts diff --git a/.releaserc.js b/.releaserc.js index 161cfd446..7d3e17b6f 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -64,6 +64,21 @@ const releaseReplaceSetting = [ ], countMatches: true, }, + { + files: ['dist/js/inpage.js'], + from: 'CORE_EXTENSION_VERSION', + // Replace CORE_EXTENSION_VERSION string to the next release number in the inpage.js file + to: `<%= _.replace(nextRelease.version, /[^0-9.]/g, '') %>`, + results: [ + { + file: 'dist/js/inpage.js', + hasChanged: true, + numMatches: 1, + numReplacements: 1, + }, + ], + countMatches: true, + }, ], }, ]; diff --git a/jest.config.js b/jest.config.js index 5b4359fc1..c6bb254c9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,5 +21,6 @@ module.exports = { EVM_PROVIDER_INFO_ICON: 'EVM_PROVIDER_INFO_ICON', EVM_PROVIDER_INFO_DESCRIPTION: 'EVM_PROVIDER_INFO_DESCRIPTION', EVM_PROVIDER_INFO_RDNS: 'EVM_PROVIDER_INFO_RDNS', + CORE_EXTENSION_VERSION: 'CORE_EXTENSION_VERSION', }, }; diff --git a/package.json b/package.json index 8ed8f8166..744fec9f6 100644 --- a/package.json +++ b/package.json @@ -24,23 +24,23 @@ }, "dependencies": { "@avalabs/avalanchejs": "4.0.5", + "@avalabs/bitcoin-module": "0.0.0-feat-add-core-version-to-evm-p-20240903160850", "@avalabs/bridge-unified": "2.1.0", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.4", "@avalabs/core-covalent-sdk": "3.1.0-alpha.4", "@avalabs/core-etherscan-sdk": "3.1.0-alpha.4", + "@avalabs/core-k2-components": "4.18.0-alpha.47", "@avalabs/core-snowtrace-sdk": "3.1.0-alpha.4", "@avalabs/core-token-prices-sdk": "3.1.0-alpha.4", "@avalabs/core-utils-sdk": "3.1.0-alpha.4", "@avalabs/core-wallets-sdk": "3.1.0-alpha.4", + "@avalabs/evm-module": "0.0.0-feat-add-core-version-to-evm-p-20240903160850", "@avalabs/glacier-sdk": "3.1.0-alpha.4", "@avalabs/hw-app-avalanche": "0.14.1", - "@avalabs/core-k2-components": "4.18.0-alpha.47", "@avalabs/types": "3.1.0-alpha.3", - "@avalabs/vm-module-types": "0.3.0", - "@avalabs/bitcoin-module": "0.3.0", - "@avalabs/evm-module": "0.3.0", + "@avalabs/vm-module-types": "0.0.0-feat-add-core-version-to-evm-p-20240903160850", "@blockaid/client": "0.10.0", "@coinbase/cbpay-js": "1.6.0", "@cubist-labs/cubesigner-sdk": "0.3.28", @@ -55,6 +55,7 @@ "@ledgerhq/hw-app-eth": "6.36.1", "@ledgerhq/hw-transport-webusb": "6.28.6", "@metamask/eth-sig-util": "4.0.1", + "@metamask/rpc-errors": "6.3.0", "@noble/hashes": "1.3.2", "@openzeppelin/contracts": "4.9.6", "@sentry/browser": "7.66.0", @@ -244,7 +245,10 @@ "@avalabs/bitcoin-module>@avalabs/core-wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false, "@avalabs/bitcoin-module>@avalabs/core-wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false, "@avalabs/bitcoin-module>@avalabs/core-wallets-sdk>hdkey>secp256k1": false, - "@avalabs/evm-module": false + "@avalabs/evm-module": false, + "@avalabs/bitcoin-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false, + "@avalabs/bitcoin-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false, + "@avalabs/bitcoin-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>hdkey>secp256k1": false } } } diff --git a/src/background/connections/dAppConnection/DAppConnectionController.ts b/src/background/connections/dAppConnection/DAppConnectionController.ts index 79a30232f..7b2a4a49c 100644 --- a/src/background/connections/dAppConnection/DAppConnectionController.ts +++ b/src/background/connections/dAppConnection/DAppConnectionController.ts @@ -36,6 +36,8 @@ import { import sentryCaptureException, { SentryExceptionTypes, } from '@src/monitoring/sentryCaptureException'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; +import { ActiveNetworkMiddleware } from '../middlewares/ActiveNetworkMiddleware'; /** * This needs to be a controller per dApp, to separate messages @@ -55,7 +57,8 @@ export class DAppConnectionController implements ConnectionController { private permissionsService: PermissionsService, private accountsService: AccountsService, private networkService: NetworkService, - private lockService: LockService + private lockService: LockService, + private moduleManager: ModuleManager ) { this.onRequest = this.onRequest.bind(this); this.disconnect = this.disconnect.bind(this); @@ -79,7 +82,8 @@ export class DAppConnectionController implements ConnectionController { this.accountsService, this.lockService ), - DAppRequestHandlerMiddleware(this.handlers, this.networkService), + ActiveNetworkMiddleware(this.networkService), + DAppRequestHandlerMiddleware(this.handlers, this.moduleManager), LoggerMiddleware(SideToLog.RESPONSE) ); diff --git a/src/background/connections/dAppConnection/models.ts b/src/background/connections/dAppConnection/models.ts index 76315496c..31159ac08 100644 --- a/src/background/connections/dAppConnection/models.ts +++ b/src/background/connections/dAppConnection/models.ts @@ -1,4 +1,5 @@ import { Maybe } from '@avalabs/core-utils-sdk'; +import { RpcResponse } from '@avalabs/vm-module-types'; import { DomainMetadata } from '@src/background/models'; import { EthereumProviderError } from 'eth-rpc-errors'; import { SerializedEthereumRpcError } from 'eth-rpc-errors/dist/classes'; @@ -71,6 +72,7 @@ export interface JsonRpcRequest { readonly id: string; readonly method: 'provider_request'; readonly params: JsonRpcRequestParams; + readonly context?: { tabId?: number } & Record; } interface JsonRpcRequestPayloadBase { @@ -100,4 +102,5 @@ export interface JsonRpcFailure { } export declare type JsonRpcResponse = | JsonRpcSuccess - | JsonRpcFailure; + | JsonRpcFailure + | RpcResponse; diff --git a/src/background/connections/dAppConnection/registry.ts b/src/background/connections/dAppConnection/registry.ts index c87ccbafc..683420b9a 100644 --- a/src/background/connections/dAppConnection/registry.ts +++ b/src/background/connections/dAppConnection/registry.ts @@ -28,7 +28,6 @@ import { registry } from 'tsyringe'; import { AvalancheGetAccountPubKeyHandler } from '@src/background/services/accounts/handlers/avalanche_getAccountPubKey'; import { AvalancheSendTransactionHandler } from '@src/background/services/wallet/handlers/avalanche_sendTransaction'; import { AvalancheGetAddressesInRangeHandler } from '@src/background/services/accounts/handlers/avalanche_getAddressesInRange'; -import { BitcoinSendTransactionHandler } from '@src/background/services/wallet/handlers/bitcoin_sendTransaction'; import { AvalancheSignTransactionHandler } from '@src/background/services/wallet/handlers/avalanche_signTransaction'; import { AvalancheSignMessageHandler } from '@src/background/services/messages/handlers/avalanche_signMessage'; @@ -54,7 +53,6 @@ import { AvalancheSignMessageHandler } from '@src/background/services/messages/h { token: 'DAppRequestHandler', useToken: AvalancheGetAccountPubKeyHandler }, { token: 'DAppRequestHandler', useToken: AvalancheSendTransactionHandler }, { token: 'DAppRequestHandler', useToken: AvalancheSignTransactionHandler }, - { token: 'DAppRequestHandler', useToken: BitcoinSendTransactionHandler }, { token: 'DAppRequestHandler', useToken: AvalancheSignMessageHandler }, { token: 'DAppRequestHandler', diff --git a/src/background/connections/extensionConnection/ExtensionConnectionController.ts b/src/background/connections/extensionConnection/ExtensionConnectionController.ts index 7b710bd6e..3ad348896 100644 --- a/src/background/connections/extensionConnection/ExtensionConnectionController.ts +++ b/src/background/connections/extensionConnection/ExtensionConnectionController.ts @@ -1,5 +1,5 @@ import { injectable, injectAll, injectAllWithTransform } from 'tsyringe'; -import { Runtime } from 'webextension-polyfill'; +import { runtime, Runtime } from 'webextension-polyfill'; import { DEFERRED_RESPONSE, Pipeline } from '../middlewares/models'; import { ExtensionRequestHandlerMiddleware } from '../middlewares/ExtensionRequestHandlerMiddleware'; import { @@ -30,6 +30,9 @@ import sentryCaptureException, { } from '@src/monitoring/sentryCaptureException'; import { DappHandlerToExtensionHandlerTransformer } from './DappHandlerToExtensionHandlerTransformer'; +import { NetworkService } from '@src/background/services/network/NetworkService'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; +import { ActiveNetworkMiddleware } from '../middlewares/ActiveNetworkMiddleware'; @injectable() export class ExtensionConnectionController implements ConnectionController { @@ -49,7 +52,9 @@ export class ExtensionConnectionController implements ConnectionController { DappHandlerToExtensionHandlerTransformer ) private dappHandlers: ExtensionRequestHandler[], - @injectAll('DAppEventEmitter') private dappEmitters: DAppEventEmitter[] + @injectAll('DAppEventEmitter') private dappEmitters: DAppEventEmitter[], + private networkService: NetworkService, + private moduleManager: ModuleManager ) { this.onMessage = this.onMessage.bind(this); this.disconnect = this.disconnect.bind(this); @@ -60,10 +65,11 @@ export class ExtensionConnectionController implements ConnectionController { this.connection = connection; this.pipeline = RequestProcessorPipeline( - ExtensionRequestHandlerMiddleware([ - ...this.handlers, - ...this.dappHandlers, - ]) + ActiveNetworkMiddleware(this.networkService), + ExtensionRequestHandlerMiddleware( + [...this.handlers, ...this.dappHandlers], + this.moduleManager + ) ); connectionLog('Extension Provider'); @@ -107,6 +113,15 @@ export class ExtensionConnectionController implements ConnectionController { // always start with authenticated false, middlewares take care of context updates authenticated: false, request: deserializedRequest, + // Extension does not connect through ChainAgnosticProvider, + // therefore its requests do not have domainMetadata populated. + domainMetadata: { + domain: runtime.id, + url: runtime.getURL(''), + tabId: deserializedRequest.params.request.tabId, + icon: runtime.getManifest().icons?.['192'], + name: runtime.getManifest().name, + }, }) ); diff --git a/src/background/connections/middlewares/ActiveNetworkMiddleware.ts b/src/background/connections/middlewares/ActiveNetworkMiddleware.ts new file mode 100644 index 000000000..464c81c8a --- /dev/null +++ b/src/background/connections/middlewares/ActiveNetworkMiddleware.ts @@ -0,0 +1,35 @@ +import { NetworkService } from '@src/background/services/network/NetworkService'; + +import { JsonRpcRequest, JsonRpcResponse } from '../dAppConnection/models'; + +import { Middleware } from './models'; +import { + ExtensionConnectionMessage, + ExtensionConnectionMessageResponse, +} from '../models'; + +export function ActiveNetworkMiddleware( + networkService: NetworkService +): Middleware< + JsonRpcRequest | ExtensionConnectionMessage, + JsonRpcResponse | ExtensionConnectionMessageResponse +> { + return async (context, next, error) => { + const { scope } = context.request.params; + + if (scope) { + const network = await networkService.getNetwork( + context.request.params.scope + ); + + if (!network) { + error(new Error(`Unrecognized network: ${scope}`)); + return; + } + + context.network = network; + } + + next(); + }; +} diff --git a/src/background/connections/middlewares/DAppRequestHandlerMiddleware.ts b/src/background/connections/middlewares/DAppRequestHandlerMiddleware.ts index bcfedda0c..5b07fceaa 100644 --- a/src/background/connections/middlewares/DAppRequestHandlerMiddleware.ts +++ b/src/background/connections/middlewares/DAppRequestHandlerMiddleware.ts @@ -2,7 +2,6 @@ import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/model import { Middleware } from './models'; import { resolve } from '@src/utils/promiseResolver'; import { engine } from '@src/utils/jsonRpcEngine'; -import { NetworkService } from '@src/background/services/network/NetworkService'; import { DAppRequestHandler } from '../dAppConnection/DAppRequestHandler'; import { ethErrors } from 'eth-rpc-errors'; import { @@ -11,10 +10,11 @@ import { JsonRpcRequestParams, JsonRpcResponse, } from '../dAppConnection/models'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; export function DAppRequestHandlerMiddleware( handlers: DAppRequestHandler[], - networkService: NetworkService + moduleManager: ModuleManager ): Middleware> { const handlerMap = handlers.reduce((acc, handler) => { for (const method of handler.methods) { @@ -27,6 +27,15 @@ export function DAppRequestHandlerMiddleware( const handler = handlerMap.get(context.request.params.request.method); // Call correct handler method based on authentication status let promise: Promise>; + + if (!context.domainMetadata) { + context.response = { + error: ethErrors.rpc.invalidRequest('Unknown request domain'), + }; + + return next(); + } + if (handler) { const params: JsonRpcRequestParams = { ...context.request.params, @@ -39,20 +48,44 @@ export function DAppRequestHandlerMiddleware( ? handler.handleAuthenticated(params) : handler.handleUnauthenticated(params); } else { - const activeNetwork = await networkService.getNetwork( - context.request.params.scope + const [module] = await resolve( + moduleManager.loadModule( + context.request.params.scope, + context.request.params.request.method + ) ); - if (!activeNetwork) { + if (!context.network) { promise = Promise.reject(ethErrors.provider.disconnected()); } else { - promise = engine(activeNetwork).then((e) => - e.handle({ - ...context.request.params.request, - id: crypto.randomUUID(), - jsonrpc: '2.0', - }) - ); + if (module) { + promise = module.onRpcRequest( + { + chainId: context.network.caipId, + dappInfo: { + icon: context.domainMetadata.icon ?? '', + name: context.domainMetadata.name ?? '', + url: context.domainMetadata.url ?? '', + }, + requestId: context.request.id, + sessionId: context.request.params.sessionId, + method: context.request.params.request.method, + params: context.request.params.request.params, + // Do not pass context from unknown sources. + // This field is for our internal use only (only used with extension's direct connection) + context: undefined, + }, + context.network + ); + } else { + promise = engine(context.network).then((e) => + e.handle({ + ...context.request.params.request, + id: crypto.randomUUID(), + jsonrpc: '2.0', + }) + ); + } } } diff --git a/src/background/connections/middlewares/ExtensionRequestHandlerMiddleware.ts b/src/background/connections/middlewares/ExtensionRequestHandlerMiddleware.ts index 1cb9eeee5..a61de8a1d 100644 --- a/src/background/connections/middlewares/ExtensionRequestHandlerMiddleware.ts +++ b/src/background/connections/middlewares/ExtensionRequestHandlerMiddleware.ts @@ -1,4 +1,4 @@ -import { Middleware } from './models'; +import { Context, Middleware } from './models'; import { resolve } from '@src/utils/promiseResolver'; import { ExtensionConnectionMessage, @@ -6,9 +6,13 @@ import { ExtensionRequestHandler, } from '../models'; import * as Sentry from '@sentry/browser'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; +import { Module } from '@avalabs/vm-module-types'; +import { runtime } from 'webextension-polyfill'; export function ExtensionRequestHandlerMiddleware( - handlers: ExtensionRequestHandler[] + handlers: ExtensionRequestHandler[], + moduleManager: ModuleManager ): Middleware< ExtensionConnectionMessage, ExtensionConnectionMessageResponse @@ -23,17 +27,28 @@ export function ExtensionRequestHandlerMiddleware( return async (context, next, onError) => { const method = context.request.params.request.method; const handler = handlerMap.get(method); + const [module] = handler + ? [null] + : await resolve( + moduleManager.loadModule( + context.request.params.scope, + context.request.params.request.method + ) + ); - if (!handler) { - onError(new Error('no handler for this request found')); + if (!handler && !module) { + onError( + new Error( + 'Unable to handle request: ' + context.request.params.request.method + ) + ); return; } + const sentryTracker = Sentry.startTransaction({ name: `Handler: ${method}`, }); - const promise = handler.handle({ - ...context.request.params, - }); + const promise = handleRequest(handler ?? module, context); context.response = await resolve(promise).then(([result, error]) => { error && console.error(error); @@ -50,3 +65,40 @@ export function ExtensionRequestHandlerMiddleware( next(); }; } + +const handleRequest = async ( + handlerOrModule: ExtensionRequestHandler | Module, + context: Context +) => { + if ('handle' in handlerOrModule) { + return handlerOrModule.handle({ + ...context.request.params, + }); + } + + if (!context.network) { + throw new Error('Unrecognized network: ' + context.request.params.scope); + } + + const response = await handlerOrModule.onRpcRequest( + { + chainId: context.network.caipId, + dappInfo: { + icon: runtime.getManifest().icons?.['192'] ?? '', + name: runtime.getManifest().name, + url: runtime.getURL(''), + }, + requestId: context.request.id, + sessionId: context.request.params.sessionId, + method: context.request.params.request.method, + params: context.request.params.request.params, + context: context.request.context, + }, + context.network + ); + + return { + ...context.request.params.request, + ...response, + }; +}; diff --git a/src/background/connections/middlewares/RPCCallsMiddleware.ts b/src/background/connections/middlewares/RPCCallsMiddleware.ts index e4c2aa69c..0a928e8e5 100644 --- a/src/background/connections/middlewares/RPCCallsMiddleware.ts +++ b/src/background/connections/middlewares/RPCCallsMiddleware.ts @@ -10,7 +10,7 @@ export function RPCCallsMiddleware( const network = await networkService.getNetwork( context.request.params.scope ); - const { method } = context.request; + const { method } = context.request.params.request; const declineMethodsPattern = /(^eth_|_watchAsset$)/; if ( network && diff --git a/src/background/connections/middlewares/SiteMetadataMiddleware.ts b/src/background/connections/middlewares/SiteMetadataMiddleware.ts index badd02b1a..a8fd48f9a 100644 --- a/src/background/connections/middlewares/SiteMetadataMiddleware.ts +++ b/src/background/connections/middlewares/SiteMetadataMiddleware.ts @@ -31,6 +31,7 @@ export function SiteMetadataMiddleware( ? new URL(connection.sender?.url || '').hostname : 'unknown', tabId: connection.sender?.tab?.id, + url: connection.sender?.url, }; return (context, next, error) => { diff --git a/src/background/connections/middlewares/models.ts b/src/background/connections/middlewares/models.ts index a9d309651..ebc3e532f 100644 --- a/src/background/connections/middlewares/models.ts +++ b/src/background/connections/middlewares/models.ts @@ -1,4 +1,5 @@ import { DomainMetadata } from '@src/background/models'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; export type Next = () => Promise | void; export type ErrorCallback = (error: Error) => void; @@ -8,6 +9,7 @@ export const DEFERRED_RESPONSE: unique symbol = Symbol(); export type Context = { request: RequestType; domainMetadata?: DomainMetadata; + network?: NetworkWithCaipId; authenticated: boolean; response?: ResponseType | typeof DEFERRED_RESPONSE; }; diff --git a/src/background/connections/models.ts b/src/background/connections/models.ts index a651df818..e2ddcdb2b 100644 --- a/src/background/connections/models.ts +++ b/src/background/connections/models.ts @@ -1,6 +1,8 @@ /* eslint-disable no-prototype-builtins */ import { Runtime } from 'webextension-polyfill'; +import { RpcMethod } from '@avalabs/vm-module-types'; + import { ArrayElement } from '../models'; import { ExtensionRequest } from './extensionConnection/models'; import { @@ -14,12 +16,12 @@ import { SerializedEthereumRpcError } from 'eth-rpc-errors/dist/classes'; import { DAppRequestHandler } from './dAppConnection/DAppRequestHandler'; export interface ExtensionConnectionMessage< - Method extends ExtensionRequest | DAppProviderRequest = any, + Method extends ExtensionRequest | DAppProviderRequest | RpcMethod = any, Params = any > extends JsonRpcRequest {} export type ExtensionConnectionMessageResponse< - Method extends ExtensionRequest | DAppProviderRequest = any, + Method extends ExtensionRequest | DAppProviderRequest | RpcMethod = any, Result = any, Params = any > = ExtensionConnectionMessage['params']['request'] & @@ -68,7 +70,7 @@ export function isConnectionResponse( * string */ export interface ExtensionRequestHandler< - Method extends ExtensionRequest | DAppProviderRequest, + Method extends ExtensionRequest | DAppProviderRequest | RpcMethod, Result, Params = undefined > { @@ -90,7 +92,13 @@ type ExtractHandlerTypes = Type extends ExtensionRequestHandler< Params: P; Result: R; } - : never; + : { + Method: RpcMethod; + Params: Type; + Result: string; + }; + +type ModuleRequestPayload = Record; /** * The `Handler` type argument is required and must be a reference to a class @@ -98,17 +106,20 @@ type ExtractHandlerTypes = Type extends ExtensionRequestHandler< */ export type RequestHandlerType = < // Reference to a class that implements ExtensionRequestHandler. - Handler extends + HandlerOrKnownParams extends | ExtensionRequestHandler - | DAppRequestHandler, + | DAppRequestHandler + | ModuleRequestPayload, // The following type arguments should NOT be provided, they are inferred. Method extends | ExtensionRequest - | DAppProviderRequest = ExtractHandlerTypes['Method'], - Result = Exclude['Result'], symbol>, - Params = ExtractHandlerTypes['Params'] + | DAppProviderRequest + | RpcMethod = ExtractHandlerTypes['Method'], + Result = Exclude['Result'], symbol>, + Params = ExtractHandlerTypes['Params'] >( - message: Omit, 'id'> + message: Omit, 'id'>, + context?: Record ) => Promise; interface ConnectionEventEmitter { diff --git a/src/background/models.ts b/src/background/models.ts index 8f0be0091..8a230869b 100644 --- a/src/background/models.ts +++ b/src/background/models.ts @@ -8,6 +8,7 @@ export interface DomainMetadata { name?: string; icon?: string; tabId?: number; + url?: string; } export interface EthCall { @@ -77,3 +78,5 @@ export type Never = { }; export type ArrayElement = A extends readonly (infer T)[] ? T : never; + +export const ACTION_HANDLED_BY_MODULE = '__handled.via.vm.modules__'; diff --git a/src/background/providers/ChainAgnosticProvider.ts b/src/background/providers/ChainAgnosticProvider.ts index 3df1b647b..546f7272d 100644 --- a/src/background/providers/ChainAgnosticProvider.ts +++ b/src/background/providers/ChainAgnosticProvider.ts @@ -7,6 +7,7 @@ import { import { PartialBy } from '../models'; import { ethErrors, serializeError } from 'eth-rpc-errors'; import AbstractConnection from '../utils/messaging/AbstractConnection'; +import { chainIdToCaip } from '../../utils/caipConversion'; import { ChainId } from '@avalabs/core-chains-sdk'; import RequestRatelimiter from './utils/RequestRatelimiter'; import { @@ -69,9 +70,9 @@ export class ChainAgnosticProvider extends EventEmitter { method: 'provider_request', jsonrpc: '2.0', params: { - scope: `eip155:${ + scope: chainIdToCaip( chainId ? parseInt(chainId) : ChainId.AVALANCHE_MAINNET_ID - }`, + ), sessionId, request: { params: [], diff --git a/src/background/providers/initializeInpageProvider.ts b/src/background/providers/initializeInpageProvider.ts index 61d6e5e18..7cc6c8355 100644 --- a/src/background/providers/initializeInpageProvider.ts +++ b/src/background/providers/initializeInpageProvider.ts @@ -27,6 +27,10 @@ export function initializeProvider( const evmProvider = new Proxy( new EVMProvider({ maxListeners, + // Core Web needs to know which extension version it's working with + // For local (dev) builds, CORE_EXTENSION_VERSION is 0.0.0 + // For release builds (alpha or production), it's replaced by semantic-release to the actual version number + walletVersion: CORE_EXTENSION_VERSION, info: { name: EVM_PROVIDER_INFO_NAME, uuid: EVM_PROVIDER_INFO_UUID, diff --git a/src/background/runtime/BackgroundRuntime.ts b/src/background/runtime/BackgroundRuntime.ts index a4a5fd7af..6ad03eaaa 100644 --- a/src/background/runtime/BackgroundRuntime.ts +++ b/src/background/runtime/BackgroundRuntime.ts @@ -5,7 +5,7 @@ import { singleton } from 'tsyringe'; import { LockService } from '@src/background/services/lock/LockService'; import { OnboardingService } from '@src/background/services/onboarding/OnboardingService'; import { BridgeService } from '@src/background/services/bridge/BridgeService'; -import ModuleManager from '../vmModules/ModuleManager'; +import { ModuleManager } from '../vmModules/ModuleManager'; @singleton() export class BackgroundRuntime { @@ -13,7 +13,8 @@ export class BackgroundRuntime { private connectionService: ConnectionService, private lockService: LockService, private onboardingService: OnboardingService, - private bridgeService: BridgeService + private bridgeService: BridgeService, + private moduleManager: ModuleManager ) {} activate() { @@ -21,12 +22,11 @@ export class BackgroundRuntime { this.registerInpageScript(); this.addContextMenus(); - ModuleManager.init(); - // Activate services which need to run all the or are required for bootstraping the wallet state this.connectionService.activate(); this.lockService.activate(); this.onboardingService.activate(); + this.moduleManager.activate(); } private onInstalled() { diff --git a/src/background/runtime/openApprovalWindow.test.ts b/src/background/runtime/openApprovalWindow.test.ts new file mode 100644 index 000000000..fec5a2eaa --- /dev/null +++ b/src/background/runtime/openApprovalWindow.test.ts @@ -0,0 +1,26 @@ +import { container } from 'tsyringe'; +import { openApprovalWindow } from './openApprovalWindow'; + +describe('src/background/runtime/openApprovalWindow', () => { + beforeEach(() => { + jest.spyOn(container, 'resolve'); + }); + + it('requests approval via ApprovalService', async () => { + const requestApproval = jest.fn(); + + jest + .mocked(container.resolve) + .mockReturnValueOnce({ requestApproval } as any); + + openApprovalWindow({ id: '123' } as any, 'approval/screen'); + + expect(requestApproval).toHaveBeenCalledWith( + { + id: '123', + actionId: crypto.randomUUID(), // this is mocked + }, + 'approval/screen' + ); + }); +}); diff --git a/src/background/runtime/openApprovalWindow.ts b/src/background/runtime/openApprovalWindow.ts index 1c3d9effa..62d0ab58b 100644 --- a/src/background/runtime/openApprovalWindow.ts +++ b/src/background/runtime/openApprovalWindow.ts @@ -8,7 +8,7 @@ export const openApprovalWindow = async (action: Action, url: string) => { // using direct injection instead of the constructor to prevent circular dependencies const approvalService = container.resolve(ApprovalService); - approvalService.requestApproval( + return approvalService.requestApproval( { ...action, actionId, diff --git a/src/background/services/actions/ActionsService.test.ts b/src/background/services/actions/ActionsService.test.ts index 522dfb180..81c6f4ba5 100644 --- a/src/background/services/actions/ActionsService.test.ts +++ b/src/background/services/actions/ActionsService.test.ts @@ -11,6 +11,8 @@ import { ACTIONS_STORAGE_KEY, } from './models'; import { filterStaleActions } from './utils'; +import { ApprovalController } from '@src/background/vmModules/ApprovalController'; +import { ACTION_HANDLED_BY_MODULE } from '@src/background/models'; jest.mock('../storage/StorageService'); jest.mock('../lock/LockService'); @@ -52,15 +54,23 @@ describe('background/services/actions/ActionsService.ts', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { displayData, ...mockActionWithoutDisplaydata } = mockAction; + let approvalController: jest.Mocked; + beforeEach(() => { jest.resetAllMocks(); // jest is having issues mocking non static getters (lockService as any).locked = false; + approvalController = { + onApproved: jest.fn(), + onRejected: jest.fn(), + } as unknown as jest.Mocked; + actionsService = new ActionsService( [handlerWithCallback, handlerWithoutCallback], storageService, - lockService + lockService, + approvalController ); (filterStaleActions as jest.Mock).mockImplementation((a) => a); }); @@ -257,6 +267,25 @@ describe('background/services/actions/ActionsService.ts', () => { }); describe('ActionStatus.SUBMITTING', () => { + it('calls ApprovalController when action originates from vm module', async () => { + const action = { + ...mockAction, + method: 'method-with-no-handler', + [ACTION_HANDLED_BY_MODULE]: true, + }; + (storageService.load as jest.Mock).mockResolvedValue({ + 1: action, + }); + jest.spyOn(actionsService, 'removeAction'); + await actionsService.updateAction({ + status: ActionStatus.SUBMITTING, + id: 1, + }); + + expect(approvalController.onApproved).toHaveBeenCalledWith(action); + expect(actionsService.removeAction).toHaveBeenCalledWith(1); + }); + it('emits error when handler not compatible or missing', async () => { const eventListener = jest.fn(); actionsService.addListener( @@ -393,6 +422,24 @@ describe('background/services/actions/ActionsService.ts', () => { }); describe('ActionStatus.ERROR_USER_CANCELED', () => { + it('calls ApprovalController when action originates from vm module', async () => { + const action = { + ...mockAction, + method: 'method-with-no-handler', + [ACTION_HANDLED_BY_MODULE]: true, + }; + (storageService.load as jest.Mock).mockResolvedValue({ + 1: action, + }); + jest.spyOn(actionsService, 'removeAction'); + await actionsService.updateAction({ + status: ActionStatus.ERROR_USER_CANCELED, + id: 1, + }); + + expect(approvalController.onRejected).toHaveBeenCalledWith(action); + expect(actionsService.removeAction).toHaveBeenCalledWith(1); + }); it('emits error when user rejects', async () => { const eventListener = jest.fn(); actionsService.addListener( diff --git a/src/background/services/actions/ActionsService.ts b/src/background/services/actions/ActionsService.ts index 11a75027b..2317462c0 100644 --- a/src/background/services/actions/ActionsService.ts +++ b/src/background/services/actions/ActionsService.ts @@ -15,6 +15,10 @@ import { DAppRequestHandler } from '@src/background/connections/dAppConnection/D import { OnStorageReady } from '@src/background/runtime/lifecycleCallbacks'; import { LockService } from '../lock/LockService'; import { filterStaleActions } from './utils'; +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'; @singleton() export class ActionsService implements OnStorageReady { @@ -24,7 +28,8 @@ export class ActionsService implements OnStorageReady { @injectAll('DAppRequestHandler') private dAppRequestHandlers: DAppRequestHandler[], private storageService: StorageService, - private lockService: LockService + private lockService: LockService, + private approvalController: ApprovalController ) {} async onStorageReady() { @@ -124,6 +129,7 @@ export class ActionsService implements OnStorageReady { result, error, displayData, + signingData, tabId, }: ActionUpdate) { const currentPendingActions = await this.getActions(); @@ -132,9 +138,14 @@ export class ActionsService implements OnStorageReady { return; } - if (status === ActionStatus.SUBMITTING) { + const isHandledByModule = pendingMessage[ACTION_HANDLED_BY_MODULE]; + + if (status === ActionStatus.SUBMITTING && isHandledByModule) { + this.approvalController.onApproved(pendingMessage); + this.removeAction(id); + } else if (status === ActionStatus.SUBMITTING) { const handler = this.dAppRequestHandlers.find((h) => - h.methods.includes(pendingMessage.method) + h.methods.includes(pendingMessage.method as DAppProviderRequest) ); if (!handler || !handler.onActionApproved) { @@ -167,6 +178,12 @@ export class ActionsService implements OnStorageReady { ); } else if (status === ActionStatus.COMPLETED) { await this.emitResult(id, pendingMessage, true, result ?? true); + } else if ( + status === ActionStatus.ERROR_USER_CANCELED && + isHandledByModule + ) { + this.approvalController.onRejected(pendingMessage); + this.removeAction(id); } else if (status === ActionStatus.ERROR_USER_CANCELED) { await this.emitResult( id, @@ -183,6 +200,10 @@ export class ActionsService implements OnStorageReady { ...pendingMessage.displayData, ...displayData, }, + signingData: getUpdatedSigningData( + pendingMessage.signingData, + signingData + ), status, result, error, diff --git a/src/background/services/actions/models.ts b/src/background/services/actions/models.ts index 6e7c9a62c..4b1c9c05a 100644 --- a/src/background/services/actions/models.ts +++ b/src/background/services/actions/models.ts @@ -1,7 +1,9 @@ +import { DappInfo, RpcMethod, SigningData } from '@avalabs/vm-module-types'; import { DAppProviderRequest, JsonRpcRequestPayload, } from '@src/background/connections/dAppConnection/models'; +import { ACTION_HANDLED_BY_MODULE } from '@src/background/models'; export enum ActionStatus { // user has been shown the UI and we are waiting on approval @@ -14,10 +16,14 @@ export enum ActionStatus { ERROR_USER_CANCELED = 'error-user-canceled', } export type Action = JsonRpcRequestPayload< - DAppProviderRequest, + DAppProviderRequest | RpcMethod, Params > & { scope: string; + context?: Record; + signingData?: SigningData; + dappInfo?: DappInfo; + [ACTION_HANDLED_BY_MODULE]?: boolean; time?: number; status?: ActionStatus; result?: any; @@ -38,6 +44,7 @@ export interface ActionUpdate { id: any; status: ActionStatus; displayData?: DisplayData; + signingData?: SigningData; result?: any; error?: string; tabId?: number; diff --git a/src/background/services/approvals/ApprovalService.ts b/src/background/services/approvals/ApprovalService.ts index f23e8fe2e..e1f653846 100644 --- a/src/background/services/approvals/ApprovalService.ts +++ b/src/background/services/approvals/ApprovalService.ts @@ -14,27 +14,38 @@ export class ApprovalService { constructor(private actionsService: ActionsService) {} - async requestApproval(action: Action, route: string) { - const isInAppRequest = action.site?.domain === browser.runtime.id; + #isInAppRequest(action: Action): boolean { + if (action.site?.domain === browser.runtime.id) { + return true; + } + + if (action.dappInfo) { + const vmModuleDappUrl = new URL(action.dappInfo.url); + return vmModuleDappUrl.hostname === browser.runtime.id; + } + + return false; + } + async requestApproval(action: Action, route: string): Promise { const url = `${route}?actionId=${action.actionId}`; - if (isInAppRequest) { + if (this.#isInAppRequest(action)) { this.#eventEmitter.emit(ApprovalEvent.ApprovalRequested, { action, url, }); - await this.actionsService.addAction(action); - } else { - // By having this extension window render here, we are popping the extension window before we send the completed request - // allowing the locked service to prompt the password input first, saving the previous request to be completed once logged in. - const windowData = await openExtensionNewWindow(url); - - await this.actionsService.addAction({ - ...action, - popupWindowId: windowData.id, - }); + return this.actionsService.addAction(action); } + + // By having this extension window render here, we are popping the extension window before we send the completed request + // allowing the locked service to prompt the password input first, saving the previous request to be completed once logged in. + const windowData = await openExtensionNewWindow(url); + + return this.actionsService.addAction({ + ...action, + popupWindowId: windowData.id, + }); } addListener(event: string, callback: (data) => void) { diff --git a/src/background/services/balances/BalancesServiceBTC.ts b/src/background/services/balances/BalancesServiceBTC.ts index 6b327d885..6e9d65486 100644 --- a/src/background/services/balances/BalancesServiceBTC.ts +++ b/src/background/services/balances/BalancesServiceBTC.ts @@ -7,13 +7,16 @@ import * as Sentry from '@sentry/browser'; import { TokensPriceShortData } from '../tokens/models'; import { BitcoinProvider } from '@avalabs/core-wallets-sdk'; import { getPriceChangeValues } from './utils/getPriceChangeValues'; -import ModuleManager from '@src/background/vmModules/ModuleManager'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; import { NetworkWithCaipId } from '../network/models'; import { TokenWithBalanceBTC } from '@avalabs/vm-module-types'; @singleton() export class BalancesServiceBTC { - constructor(private settingsService: SettingsService) {} + constructor( + private settingsService: SettingsService, + private moduleManager: ModuleManager + ) {} getServiceForProvider(provider: any) { if (provider instanceof BitcoinProvider) return this; @@ -31,7 +34,7 @@ export class BalancesServiceBTC { await this.settingsService.getSettings() ).currency.toLowerCase(); - const module = (await ModuleManager.loadModuleByNetwork( + const module = (await this.moduleManager.loadModuleByNetwork( network )) as BitcoinModule; const addresses = accounts diff --git a/src/background/services/bridge/BridgeService.ts b/src/background/services/bridge/BridgeService.ts index ec119e479..1001ff315 100644 --- a/src/background/services/bridge/BridgeService.ts +++ b/src/background/services/bridge/BridgeService.ts @@ -63,6 +63,7 @@ export class BridgeService implements OnLock, OnStorageReady { this.networkService.developerModeChanged.add(() => { this.updateBridgeConfig(); }); + this.updateBridgeConfig(); } async onStorageReady(): Promise { diff --git a/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts b/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts index 790f683e9..bf95696d2 100644 --- a/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts +++ b/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts @@ -168,7 +168,7 @@ describe('background/services/bridge/handlers/avalanche_bridgeAsset', () => { waitForTx: jest.fn().mockResolvedValue(btcResult), }); balanceAggregatorServiceMock.getBalancesForNetworks.mockResolvedValue({}); - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); jest.mocked(getAssets).mockReturnValue({ BTC: btcAsset, WETH: evmAsset, diff --git a/src/background/services/history/HistoryServiceBTC.ts b/src/background/services/history/HistoryServiceBTC.ts index 22f264615..efebf387e 100644 --- a/src/background/services/history/HistoryServiceBTC.ts +++ b/src/background/services/history/HistoryServiceBTC.ts @@ -3,7 +3,7 @@ import { singleton } from 'tsyringe'; import { AccountsService } from '../accounts/AccountsService'; import { HistoryServiceBridgeHelper } from './HistoryServiceBridgeHelper'; import { TransactionType, TxHistoryItem } from './models'; -import ModuleManager from '@src/background/vmModules/ModuleManager'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; import { NetworkWithCaipId } from '../network/models'; import sentryCaptureException, { SentryExceptionTypes, @@ -13,7 +13,8 @@ import sentryCaptureException, { export class HistoryServiceBTC { constructor( private accountsService: AccountsService, - private bridgeHistoryHelperService: HistoryServiceBridgeHelper + private bridgeHistoryHelperService: HistoryServiceBridgeHelper, + private moduleManager: ModuleManager ) {} async getHistory(network: NetworkWithCaipId): Promise { @@ -27,7 +28,7 @@ export class HistoryServiceBTC { } try { - const module = await ModuleManager.loadModuleByNetwork(network); + const module = await this.moduleManager.loadModuleByNetwork(network); const { transactions } = await module.getTransactionHistory({ address, network, diff --git a/src/background/services/messages/handlers/avalanche_signMessage.test.ts b/src/background/services/messages/handlers/avalanche_signMessage.test.ts index dcf33e540..1f31c7d89 100644 --- a/src/background/services/messages/handlers/avalanche_signMessage.test.ts +++ b/src/background/services/messages/handlers/avalanche_signMessage.test.ts @@ -30,7 +30,7 @@ describe('avalanche_signMessage', function () { }; beforeEach(() => { - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); }); it('returns error when no message', async () => { diff --git a/src/background/services/messages/handlers/signMessage.test.ts b/src/background/services/messages/handlers/signMessage.test.ts index 3809db96d..ed454c53b 100644 --- a/src/background/services/messages/handlers/signMessage.test.ts +++ b/src/background/services/messages/handlers/signMessage.test.ts @@ -55,7 +55,7 @@ describe('src/background/services/messages/handlers/signMessage.ts', () => { getNetwork: () => activeNetworkMock, } as any; - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); (paramsToMessageParams as jest.Mock).mockReturnValue(displayDataMock); }); diff --git a/src/background/services/network/NetworkService.ts b/src/background/services/network/NetworkService.ts index 8680a17cb..876104c0b 100644 --- a/src/background/services/network/NetworkService.ts +++ b/src/background/services/network/NetworkService.ts @@ -14,7 +14,6 @@ import { CustomNetworkPayload, ChainList, Network, - SYNCED_DOMAINS, ChainListWithCaipIds, NetworkWithCaipId, } from './models'; @@ -210,7 +209,7 @@ export class NetworkService implements OnLock, OnStorageReady { const scope = this.#dappScopes[getSyncDomain(domain)]; const storedNetwork = scope ? await this.getNetwork(scope) : null; - const isSynced = SYNCED_DOMAINS.includes(domain); + const isSynced = isSyncDomain(domain); // Synchronized dApps can handle our fake chain IDs const isActiveEvmBased = this.uiActiveNetwork?.vmName === NetworkVMType.EVM; const canFallbackToActive = isActiveEvmBased || isSynced; diff --git a/src/background/services/network/events/networksUpdatedEventListener.ts b/src/background/services/network/events/networksUpdatedEventListener.ts index 76b7d7982..017e7d119 100644 --- a/src/background/services/network/events/networksUpdatedEventListener.ts +++ b/src/background/services/network/events/networksUpdatedEventListener.ts @@ -1,12 +1,12 @@ import { ExtensionConnectionEvent } from '@src/background/connections/models'; -import { Network, NetworkEvents } from '../models'; +import { NetworkEvents, NetworkWithCaipId } from '../models'; export function networksUpdatedEventListener( evt: ExtensionConnectionEvent<{ - networks: Network[]; - activeNetwork?: Network; + networks: NetworkWithCaipId[]; + activeNetwork?: NetworkWithCaipId; favoriteNetworks: number[]; - customNetworks: Record; + customNetworks: Record; }> ) { return evt.name === NetworkEvents.NETWORKS_UPDATED_EVENT; diff --git a/src/background/services/network/handlers/wallet_addEthereumChain.test.ts b/src/background/services/network/handlers/wallet_addEthereumChain.test.ts index 23cbc659d..f6fb0219c 100644 --- a/src/background/services/network/handlers/wallet_addEthereumChain.test.ts +++ b/src/background/services/network/handlers/wallet_addEthereumChain.test.ts @@ -1,21 +1,18 @@ import { Network, NetworkVMType } from '@avalabs/core-chains-sdk'; import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { openExtensionNewWindow } from '@src/utils/extensionUtils'; import { ethErrors } from 'eth-rpc-errors'; -import { container } from 'tsyringe'; -import { ActionsService } from '../../actions/ActionsService'; import { Action, ActionStatus } from '../../actions/models'; import { NetworkService } from '../NetworkService'; import { WalletAddEthereumChainHandler } from './wallet_addEthereumChain'; import { buildRpcCall } from '@src/tests/test-utils'; import { isCoreWeb } from '../../network/utils/isCoreWeb'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; +import { decorateWithCaipId } from '@src/utils/caipConversion'; jest.mock('../NetworkService'); jest.mock('../../network/utils/isCoreWeb'); -jest.mock('@src/utils/extensionUtils', () => ({ - openExtensionNewWindow: jest.fn(), -})); +jest.mock('@src/background/runtime/openApprovalWindow'); const mockActiveNetwork: Network = { chainName: 'Avalanche (C-Chain)', @@ -39,10 +36,6 @@ const mockActiveNetwork: Network = { describe('background/services/network/handlers/wallet_addEthereumChain.ts', () => { let mockNetworkService: NetworkService; let handler: WalletAddEthereumChainHandler; - const actionsServiceMock = { - addAction: jest.fn(), - }; - container.registerInstance(ActionsService, actionsServiceMock as any); beforeEach(() => { jest.resetAllMocks(); @@ -59,7 +52,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }), } as any; handler = new WalletAddEthereumChainHandler(mockNetworkService); - (openExtensionNewWindow as jest.Mock).mockReturnValue({ id: 123 }); + (openApprovalWindow as jest.Mock).mockReturnValue({ id: 123 }); (crypto.randomUUID as jest.Mock).mockReturnValue('uuid'); }); @@ -105,40 +98,13 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }, ], }; - openExtensionNewWindow; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'network/switch?actionId=uuid' + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'network/switch' ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - actionId: 'uuid', - scope: 'eip155:43113', - displayData: { - network: { - caipId: 'eip155:43113', - chainId: 43113, - chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - explorerUrl: 'https://snowtrace.io/', - logoUri: '', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', - logoUri: '', - }, - isTestnet: false, - }, - }, - popupWindowId: 123, - }); expect(result).toEqual({ ...request, @@ -162,8 +128,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); expect(result).toEqual({ ...request, @@ -188,8 +153,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); expect(result).toEqual({ ...request, @@ -214,8 +178,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); expect(result).toEqual({ ...request, @@ -241,8 +204,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); expect(result).toEqual({ ...request, @@ -268,8 +230,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = }; const result = await handler.handleUnauthenticated(buildRpcCall(request)); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); expect(result).toEqual({ ...request, @@ -310,8 +271,7 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = 'https://api.avax.network/ext/bc/C/rpc' ); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); + expect(openApprovalWindow).not.toHaveBeenCalled(); }); it('opens approval dialog', async () => { @@ -336,40 +296,11 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = result: DEFERRED_RESPONSE, }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'networks/add-popup?actionId=uuid' + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'networks/add-popup' ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - actionId: 'uuid', - scope: 'eip155:43113', - displayData: { - network: { - caipId: 'eip155:43112', - chainId: 43112, - chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - logoUri: 'logo.png', - explorerUrl: 'https://snowtrace.io/', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', - logoUri: 'logo.png', - }, - isTestnet: false, - }, - options: { - requiresGlacierApiKey: false, - }, - }, - popupWindowId: 123, - }); }); describe('when glacier API key is required', () => { @@ -396,499 +327,401 @@ describe('background/services/network/handlers/wallet_addEthereumChain.ts', () = result: DEFERRED_RESPONSE, }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'networks/add-popup?actionId=uuid' + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'networks/add-popup' ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - actionId: 'uuid', - displayData: { - network: { - caipId: 'eip155:43112', - chainId: 43112, + }); + + it('works for authenticated', async () => { + const request = { + id: '1234', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa868', // 43112 chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - logoUri: 'logo.png', - explorerUrl: 'https://snowtrace.io/', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', - logoUri: 'logo.png', - }, - isTestnet: false, - }, - options: { - requiresGlacierApiKey: true, + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], }, - }, - popupWindowId: 123, - scope: 'eip155:43113', - }); - }); - }); + ], + }; + const result = await handler.handleAuthenticated(buildRpcCall(request)); - it('works for authenticated', async () => { - const request = { - id: '1234', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa868', // 43112 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], - }, - ], - }; - const result = await handler.handleAuthenticated(buildRpcCall(request)); + expect(result).toEqual({ + ...request, + result: DEFERRED_RESPONSE, + }); - expect(result).toEqual({ - ...request, - result: DEFERRED_RESPONSE, + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'networks/add-popup' + ); }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'networks/add-popup?actionId=uuid' - ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - actionId: 'uuid', - scope: 'eip155:43113', - displayData: { - network: { - caipId: 'eip155:43112', - chainId: 43112, - chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - logoUri: 'logo.png', - explorerUrl: 'https://snowtrace.io/', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', - logoUri: 'logo.png', + it('handles non standard isTestnet values', async () => { + const request = { + id: '1234', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa868', // 43112 + chainName: 'Avalanche', + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], + isTestnet: 'ofc', }, - isTestnet: false, - }, - options: { - requiresGlacierApiKey: false, - }, - }, - popupWindowId: 123, - }); - }); - - it('handles non standard isTestnet values', async () => { - const request = { - id: '1234', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa868', // 43112 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], - isTestnet: 'ofc', - }, - ], - }; + ], + }; - const result = await handler.handleAuthenticated(buildRpcCall(request)); + const result = await handler.handleAuthenticated(buildRpcCall(request)); - expect(result).toEqual({ - ...request, - result: DEFERRED_RESPONSE, - }); + expect(result).toEqual({ + ...request, + result: DEFERRED_RESPONSE, + }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'networks/add-popup?actionId=uuid' - ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - actionId: 'uuid', - displayData: expect.objectContaining({ - network: expect.objectContaining({ - isTestnet: true, - }), - }), - scope: 'eip155:43113', - popupWindowId: 123, + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'networks/add-popup' + ); }); - }); - it('does not opens approval dialog and switch to a known network if the request is from core web', async () => { - jest.mocked(isCoreWeb).mockResolvedValue(true); + it('does not opens approval dialog and switch to a known network if the request is from core web', async () => { + jest.mocked(isCoreWeb).mockResolvedValue(true); - const request = { - id: '852', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa869', // 43113 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], + const request = { + id: '852', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa869', // 43113 + chainName: 'Avalanche', + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], + }, + ], + site: { + domain: 'core.app', + name: 'Core', + tabId: 123, }, - ], - site: { - domain: 'core.app', - name: 'Core', - tabId: 123, - }, - }; + }; - const result = await handler.handleAuthenticated(buildRpcCall(request)); + const result = await handler.handleAuthenticated(buildRpcCall(request)); - expect(result).toEqual({ - ...request, - result: null, - }); + expect(result).toEqual({ + ...request, + result: null, + }); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); - expect(mockNetworkService.setNetwork).toHaveBeenCalledTimes(1); - expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( - 'core.app', - 'eip155:43113' - ); - }); + expect(openApprovalWindow).not.toHaveBeenCalled(); + expect(mockNetworkService.setNetwork).toHaveBeenCalledTimes(1); + expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( + 'core.app', + 'eip155:43113' + ); + }); - it('does not opens approval dialog and add and switch to a new network if the request is from a core domain', async () => { - jest.mocked(isCoreWeb).mockResolvedValue(true); + it('does not opens approval dialog and add and switch to a new network if the request is from a core domain', async () => { + jest.mocked(isCoreWeb).mockResolvedValue(true); - const request = { - id: '852', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa868', // 43112 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], + const request = { + id: '852', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa868', // 43112 + chainName: 'Avalanche', + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], + }, + ], + site: { + domain: 'core.app', + name: 'Core', + tabId: 123, }, - ], - site: { - domain: 'core.app', - name: 'Core', - tabId: 123, - }, - }; + }; - const result = await handler.handleAuthenticated(buildRpcCall(request)); + const result = await handler.handleAuthenticated(buildRpcCall(request)); - expect(result).toEqual({ - ...request, - result: null, - }); + expect(result).toEqual({ + ...request, + result: null, + }); - expect(openExtensionNewWindow).not.toHaveBeenCalled(); - expect(actionsServiceMock.addAction).not.toHaveBeenCalled(); - expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledTimes(1); - expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledWith({ - caipId: 'eip155:43112', - chainId: 43112, - chainName: 'Avalanche', - explorerUrl: 'https://snowtrace.io/', - isTestnet: false, - logoUri: 'logo.png', - networkToken: { - decimals: 18, - description: '', + expect(openApprovalWindow).not.toHaveBeenCalled(); + expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledTimes(1); + expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledWith({ + caipId: 'eip155:43112', + chainId: 43112, + chainName: 'Avalanche', + explorerUrl: 'https://snowtrace.io/', + isTestnet: false, logoUri: 'logo.png', - name: 'AVAX', - symbol: 'AVAX', - }, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - vmName: 'EVM', - }); - }); - - it('adds testnet networks', async () => { - const request = { - id: '1234', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa868', // 43112 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], - isTestnet: true, + networkToken: { + decimals: 18, + description: '', + logoUri: 'logo.png', + name: 'AVAX', + symbol: 'AVAX', }, - ], - }; - const result = await handler.handleAuthenticated(buildRpcCall(request)); - - expect(result).toEqual({ - ...request, - result: DEFERRED_RESPONSE, + primaryColor: 'black', + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + vmName: 'EVM', + }); }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'networks/add-popup?actionId=uuid' - ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...request, - scope: 'eip155:43113', - actionId: 'uuid', - displayData: { - network: { - caipId: 'eip155:43112', - chainId: 43112, - chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - primaryColor: 'black', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - logoUri: 'logo.png', - explorerUrl: 'https://snowtrace.io/', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', - logoUri: 'logo.png', + it('adds testnet networks', async () => { + const request = { + id: '1234', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa868', // 43112 + chainName: 'Avalanche', + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], + isTestnet: true, }, - isTestnet: true, - }, - options: { - requiresGlacierApiKey: false, - }, - }, - popupWindowId: 123, + ], + }; + const result = await handler.handleAuthenticated(buildRpcCall(request)); + + expect(result).toEqual({ + ...request, + result: DEFERRED_RESPONSE, + }); + + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'networks/add-popup' + ); }); - }); - describe('onActionApproved', () => { - const mockPendingAction: Action = { - id: 'uuid', - method: DAppProviderRequest.WALLET_ADD_CHAIN, - params: [ - { - chainId: '0xa868', // 43112 - chainName: 'Avalanche', - nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, - rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], - blockExplorerUrls: ['https://snowtrace.io/'], - iconUrls: ['logo.png'], + describe('onActionApproved', () => { + const mockPendingAction: Action = { + id: 'uuid', + method: DAppProviderRequest.WALLET_ADD_CHAIN, + params: [ + { + chainId: '0xa868', // 43112 + chainName: 'Avalanche', + nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 }, + rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'], + blockExplorerUrls: ['https://snowtrace.io/'], + iconUrls: ['logo.png'], + }, + ], + tabId: undefined, + site: { + domain: 'core.app', }, - ], - tabId: undefined, - site: { - domain: 'core.app', - }, - actionId: 'uuid', - displayData: { - network: { - chainId: 43112, - chainName: 'Avalanche', - vmName: NetworkVMType.EVM, - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - logoUri: 'logo.png', - explorerUrl: 'https://snowtrace.io/', - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'AVAX', + actionId: 'uuid', + displayData: { + network: decorateWithCaipId({ + chainId: 43112, + chainName: 'Avalanche', + vmName: NetworkVMType.EVM, + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', logoUri: 'logo.png', - }, + explorerUrl: 'https://snowtrace.io/', + networkToken: { + symbol: 'AVAX', + decimals: 18, + description: '', + name: 'AVAX', + logoUri: 'logo.png', + }, + }), }, - }, - time: 123123, - status: ActionStatus.SUBMITTING, - } as any; + time: 123123, + status: ActionStatus.SUBMITTING, + } as any; - describe('when glacier API key is provided', () => { - const preppedAction = { - ...mockPendingAction, - displayData: { - network: { - ...mockPendingAction.displayData.network, - customRpcHeaders: { - 'X-Glacier-Api-Key': 'test-1234', + describe('when glacier API key is provided', () => { + const preppedAction = { + ...mockPendingAction, + displayData: { + network: { + ...mockPendingAction.displayData.network, + customRpcHeaders: { + 'X-Glacier-Api-Key': 'test-1234', + }, + }, + options: { + requiresGlacierApiKey: true, }, }, - options: { - requiresGlacierApiKey: true, - }, - }, - }; + }; + + it('saves the API key header in network config overrides', async () => { + ( + mockNetworkService.updateNetworkOverrides as jest.Mock + ).mockResolvedValue(undefined); + + await handler.onActionApproved( + preppedAction, + undefined, + () => {}, + () => {} + ); + + expect( + mockNetworkService.updateNetworkOverrides + ).toHaveBeenCalledWith( + expect.objectContaining({ + customRpcHeaders: { + 'X-Glacier-Api-Key': 'test-1234', + }, + }) + ); + + expect( + mockNetworkService.updateNetworkOverrides + ).toHaveBeenCalledWith( + expect.not.objectContaining({ + rpcUrl: expect.any(String), // RPC is not supposed to be passed here + }) + ); + }); + }); - it('saves the API key header in network config overrides', async () => { - ( - mockNetworkService.updateNetworkOverrides as jest.Mock - ).mockResolvedValue(undefined); + it('saves custom network on approval', async () => { + (mockNetworkService.saveCustomNetwork as jest.Mock).mockResolvedValue( + undefined + ); + + const successHandler = jest.fn(); + const errorHandler = jest.fn(); await handler.onActionApproved( - preppedAction, + mockPendingAction, undefined, - () => {}, - () => {} - ); - - expect(mockNetworkService.updateNetworkOverrides).toHaveBeenCalledWith( - expect.objectContaining({ - customRpcHeaders: { - 'X-Glacier-Api-Key': 'test-1234', - }, - }) + successHandler, + errorHandler ); - expect(mockNetworkService.updateNetworkOverrides).toHaveBeenCalledWith( - expect.not.objectContaining({ - rpcUrl: expect.any(String), // RPC is not supposed to be passed here - }) + expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( + mockPendingAction.site?.domain, + mockPendingAction.displayData.network.caipId ); + expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledTimes(1); + expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledWith({ + ...mockPendingAction.displayData.network, + }); + + expect(successHandler).toHaveBeenCalledTimes(1); + expect(successHandler).toHaveBeenCalledWith(null); + expect(errorHandler).not.toHaveBeenCalled(); }); - }); - it('saves custom network on approval', async () => { - (mockNetworkService.saveCustomNetwork as jest.Mock).mockResolvedValue( - undefined - ); + it('calls error when saving custom network fails', async () => { + (mockNetworkService.saveCustomNetwork as jest.Mock).mockRejectedValue( + new Error('some serious error') + ); - const successHandler = jest.fn(); - const errorHandler = jest.fn(); + const successHandler = jest.fn(); + const errorHandler = jest.fn(); - await handler.onActionApproved( - mockPendingAction, - undefined, - successHandler, - errorHandler - ); + await handler.onActionApproved( + mockPendingAction, + undefined, + successHandler, + errorHandler + ); - expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( - mockPendingAction.site?.domain, - mockPendingAction.displayData.network.caipId - ); - expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledTimes(1); - expect(mockNetworkService.saveCustomNetwork).toHaveBeenCalledWith({ - ...mockPendingAction.displayData.network, + expect(errorHandler).toHaveBeenCalledTimes(1); + expect(errorHandler).toHaveBeenCalledWith( + new Error('some serious error') + ); + expect(successHandler).not.toHaveBeenCalled(); }); - expect(successHandler).toHaveBeenCalledTimes(1); - expect(successHandler).toHaveBeenCalledWith(null); - expect(errorHandler).not.toHaveBeenCalled(); - }); - - it('calls error when saving custom network fails', async () => { - (mockNetworkService.saveCustomNetwork as jest.Mock).mockRejectedValue( - new Error('some serious error') - ); - - const successHandler = jest.fn(); - const errorHandler = jest.fn(); - - await handler.onActionApproved( - mockPendingAction, - undefined, - successHandler, - errorHandler - ); - - expect(errorHandler).toHaveBeenCalledTimes(1); - expect(errorHandler).toHaveBeenCalledWith( - new Error('some serious error') - ); - expect(successHandler).not.toHaveBeenCalled(); - }); - - it('switches network if network already added', async () => { - jest.mocked(mockNetworkService.setNetwork).mockResolvedValue(undefined); + it('switches network if network already added', async () => { + jest.mocked(mockNetworkService.setNetwork).mockResolvedValue(undefined); - const successHandler = jest.fn(); - const errorHandler = jest.fn(); + const successHandler = jest.fn(); + const errorHandler = jest.fn(); - const network = { - ...mockPendingAction.displayData.network, - chainId: 43113, - caipId: 'eip155:43113', - }; + const network = { + ...mockPendingAction.displayData.network, + chainId: 43113, + }; - await handler.onActionApproved( - { - ...mockPendingAction, - displayData: { - network, - options: { - requiresGlacierApiKey: false, + await handler.onActionApproved( + { + ...mockPendingAction, + displayData: { + network, + options: { + requiresGlacierApiKey: false, + }, }, }, - }, - undefined, - successHandler, - errorHandler - ); + undefined, + successHandler, + errorHandler + ); - expect(mockNetworkService.saveCustomNetwork).not.toHaveBeenCalled(); - expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( - mockPendingAction.site?.domain, - network.caipId - ); + expect(mockNetworkService.saveCustomNetwork).not.toHaveBeenCalled(); + expect(mockNetworkService.setNetwork).toHaveBeenCalledWith( + mockPendingAction.site?.domain, + network.caipId + ); - expect(successHandler).toHaveBeenCalledTimes(1); - expect(successHandler).toHaveBeenCalledWith(null); - expect(errorHandler).not.toHaveBeenCalled(); - }); + expect(successHandler).toHaveBeenCalledTimes(1); + expect(successHandler).toHaveBeenCalledWith(null); + expect(errorHandler).not.toHaveBeenCalled(); + }); - it('calls error when switching network fails', async () => { - jest - .mocked(mockNetworkService.setNetwork) - .mockRejectedValue(new Error('some serious error')); + it('calls error when switching network fails', async () => { + jest + .mocked(mockNetworkService.setNetwork) + .mockRejectedValue(new Error('some serious error')); - const successHandler = jest.fn(); - const errorHandler = jest.fn(); + const successHandler = jest.fn(); + const errorHandler = jest.fn(); - await handler.onActionApproved( - { - ...mockPendingAction, - displayData: { - network: { - ...mockPendingAction.displayData.network, - chainId: 43113, - caipId: 'eip155:43113', - }, - options: { - requiresGlacierApiKey: false, + await handler.onActionApproved( + { + ...mockPendingAction, + displayData: { + network: { + ...mockPendingAction.displayData.network, + chainId: 43113, + }, + options: { + requiresGlacierApiKey: false, + }, }, }, - }, - undefined, - successHandler, - errorHandler - ); + undefined, + successHandler, + errorHandler + ); - expect(errorHandler).toHaveBeenCalledTimes(1); - expect(errorHandler).toHaveBeenCalledWith( - new Error('some serious error') - ); - expect(successHandler).not.toHaveBeenCalled(); + expect(errorHandler).toHaveBeenCalledTimes(1); + expect(errorHandler).toHaveBeenCalledWith( + new Error('some serious error') + ); + expect(successHandler).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/src/background/services/network/handlers/wallet_switchEthereumChain.test.ts b/src/background/services/network/handlers/wallet_switchEthereumChain.test.ts index a41729345..8ee2fae45 100644 --- a/src/background/services/network/handlers/wallet_switchEthereumChain.test.ts +++ b/src/background/services/network/handlers/wallet_switchEthereumChain.test.ts @@ -1,18 +1,14 @@ import { WalletSwitchEthereumChainHandler } from './wallet_switchEthereumChain'; -import { openExtensionNewWindow } from './../../../../utils/extensionUtils'; import { buildRpcCall } from './../../../../tests/test-utils'; import { DAppProviderRequest } from './../../../connections/dAppConnection/models'; import { DEFERRED_RESPONSE } from './../../../connections/middlewares/models'; import { Network, NetworkVMType } from '@avalabs/core-chains-sdk'; import { ethErrors } from 'eth-rpc-errors'; -import { container } from 'tsyringe'; -import { ActionsService } from '../../actions/ActionsService'; import { isCoreWeb } from '../../network/utils/isCoreWeb'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -jest.mock('@src/utils/extensionUtils', () => ({ - openExtensionNewWindow: jest.fn(), -})); jest.mock('../../network/utils/isCoreWeb'); +jest.mock('@src/background/runtime/openApprovalWindow'); const mockActiveNetwork: Network = { chainName: 'Avalanche (C-Chain)', @@ -34,10 +30,6 @@ const mockActiveNetwork: Network = { }; describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts', () => { - const actionsServiceMock = { - addAction: jest.fn(), - } as any; - const networkServiceMock = { isValidRPCUrl: jest.fn(), allNetworks: { @@ -64,7 +56,6 @@ describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts setNetwork: () => Promise.resolve(), } as any; - container.registerInstance(ActionsService, actionsServiceMock); let handler: WalletSwitchEthereumChainHandler; const switchChainRequest = { id: '1234', @@ -78,7 +69,6 @@ describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts beforeEach(() => { jest.resetAllMocks(); - (openExtensionNewWindow as jest.Mock).mockReturnValue({ id: 123 }); (networkServiceMock.isValidRPCUrl as jest.Mock).mockResolvedValue(true); handler = new WalletSwitchEthereumChainHandler(networkServiceMock); @@ -107,37 +97,11 @@ describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts result: DEFERRED_RESPONSE, }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - 'network/switch?actionId=uuid' + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1234' }), + 'network/switch' ); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...switchChainRequest, - actionId: 'uuid', - displayData: { - network: { - chainId: 43113, - chainName: 'Avalanche (C-Chain)', - id: 43113, - logoUri: 'chain-logo.png', - vmName: NetworkVMType.EVM, - primaryColor: 'violet', - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - explorerUrl: 'https://explorer.url', - subnetExplorerUriId: 'c-chain', - tokens: [], - networkToken: { - symbol: 'AVAX', - decimals: 18, - description: '', - name: 'Avalanche', - logoUri: 'token-logo.png', - }, - }, - }, - popupWindowId: 123, - }); }); it('does not open approval dialog because the request comes from core web', async () => { @@ -152,7 +116,7 @@ describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts result: null, }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(0); + expect(openApprovalWindow).toHaveBeenCalledTimes(0); }); it('throws error when the requested network is not found', async () => { @@ -178,7 +142,7 @@ describe('src/background/services/network/handlers/wallet_switchEthereumChain.ts }), }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(0); + expect(openApprovalWindow).toHaveBeenCalledTimes(0); }); }); }); diff --git a/src/background/services/network/models.ts b/src/background/services/network/models.ts index d89ba4dcf..33c50d200 100644 --- a/src/background/services/network/models.ts +++ b/src/background/services/network/models.ts @@ -1,6 +1,5 @@ import { Network as _Network } from '@avalabs/core-chains-sdk'; import { EnsureDefined, PartialBy } from '@src/background/models'; -import { runtime } from 'webextension-polyfill'; export enum NetworkEvents { NETWORK_UPDATE_EVENT = 'network-updated', @@ -12,13 +11,6 @@ export const NETWORK_STORAGE_KEY = 'NETWORK_STORAGE_KEY'; export const NETWORK_LIST_STORAGE_KEY = 'NETWORK_LIST_STORAGE_KEY'; export const NETWORK_OVERRIDES_STORAGE_KEY = 'NETWORK_OVERRIDES_STORAGE_KEY'; -export const SYNCED_DOMAINS = [ - 'core-web.pages.dev', - 'core.app', - 'test.core.app', - runtime.id, -]; - export interface NetworkStorage { favoriteNetworks: number[]; customNetworks: Record; diff --git a/src/background/services/network/utils/getSyncDomain.ts b/src/background/services/network/utils/getSyncDomain.ts index dc9f29968..a2ff89b01 100644 --- a/src/background/services/network/utils/getSyncDomain.ts +++ b/src/background/services/network/utils/getSyncDomain.ts @@ -1,6 +1,11 @@ import { runtime } from 'webextension-polyfill'; -import { SYNCED_DOMAINS } from '../models'; +const SYNCED_DOMAINS = [ + 'core-web.pages.dev', + 'core.app', + 'test.core.app', + runtime.id, +]; export const isSyncDomain = (domain: string) => { return SYNCED_DOMAINS.some((syncDomain) => { diff --git a/src/background/services/networkFee/NetworkFeeService.test.ts b/src/background/services/networkFee/NetworkFeeService.test.ts index 2405ff7a4..aaf9cb0d7 100644 --- a/src/background/services/networkFee/NetworkFeeService.test.ts +++ b/src/background/services/networkFee/NetworkFeeService.test.ts @@ -2,7 +2,6 @@ import { NetworkVMType } from '@avalabs/core-chains-sdk'; import { NetworkFeeService } from './NetworkFeeService'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; import { NetworkWithCaipId } from '../network/models'; -import ModuleManager from '@src/background/vmModules/ModuleManager'; import { serializeToJSON } from '@src/background/serialization/serialize'; jest.mock('@src/utils/network/getProviderForNetwork'); @@ -38,15 +37,17 @@ describe('src/background/services/networkFee/NetworkFeeService', () => { high: { maxFeePerGas: 5n }, }); - jest - .spyOn(ModuleManager, 'loadModuleByNetwork') - .mockResolvedValueOnce({ getNetworkFee } as any); + const moduleManager = { + loadModuleByNetwork: jest + .fn() + .mockResolvedValue({ getNetworkFee } as any), + } as any; - const service = new NetworkFeeService(); + const service = new NetworkFeeService(moduleManager); const result = await service.getNetworkFee(btc); - expect(ModuleManager.loadModuleByNetwork).toHaveBeenCalledWith(btc); + expect(moduleManager.loadModuleByNetwork).toHaveBeenCalledWith(btc); expect(getNetworkFee).toHaveBeenCalledWith(btc); // Jest has issues with objects containing BigInts, so we make it easier by using our own serializer @@ -66,7 +67,7 @@ describe('src/background/services/networkFee/NetworkFeeService', () => { provider.getFeeData.mockResolvedValueOnce({ maxFeePerGas }); - const service = new NetworkFeeService(); + const service = new NetworkFeeService({} as any); expect(await service.getNetworkFee(evm)).toEqual({ displayDecimals: 9, // use Gwei to display amount diff --git a/src/background/services/networkFee/NetworkFeeService.ts b/src/background/services/networkFee/NetworkFeeService.ts index 97fd5b0c1..09ca37f59 100644 --- a/src/background/services/networkFee/NetworkFeeService.ts +++ b/src/background/services/networkFee/NetworkFeeService.ts @@ -5,7 +5,7 @@ import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork' import { singleton } from 'tsyringe'; import { FeeRate, NetworkFee, TransactionPriority } from './models'; import { NetworkWithCaipId } from '../network/models'; -import ModuleManager from '@src/background/vmModules/ModuleManager'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; const EVM_BASE_TIP = BigInt(5e8); // 0.5 Gwei const EVM_TIP_MODIFIERS: Record = { @@ -16,7 +16,7 @@ const EVM_TIP_MODIFIERS: Record = { @singleton() export class NetworkFeeService { - constructor() {} + constructor(private moduleManager: ModuleManager) {} async getNetworkFee(network: NetworkWithCaipId): Promise { if (network.vmName === NetworkVMType.EVM) { @@ -26,7 +26,7 @@ export class NetworkFeeService { provider as JsonRpcBatchInternal ); } else if (network.vmName === NetworkVMType.BITCOIN) { - const module = await ModuleManager.loadModuleByNetwork(network); + const module = await this.moduleManager.loadModuleByNetwork(network); const { low, medium, high, isFixedFee } = await module.getNetworkFee( network ); diff --git a/src/background/services/permissions/handlers/wallet_requestPermissions.test.ts b/src/background/services/permissions/handlers/wallet_requestPermissions.test.ts index 70f142e68..bf3fd83cb 100644 --- a/src/background/services/permissions/handlers/wallet_requestPermissions.test.ts +++ b/src/background/services/permissions/handlers/wallet_requestPermissions.test.ts @@ -1,28 +1,19 @@ import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { openExtensionNewWindow } from '@src/utils/extensionUtils'; import { ethErrors } from 'eth-rpc-errors'; -import { container } from 'tsyringe'; import { AccountsService } from '../../accounts/AccountsService'; import { AccountType } from '../../accounts/models'; -import { ActionsService } from '../../actions/ActionsService'; import { Action, ActionStatus } from '../../actions/models'; import { PermissionsService } from '../../permissions/PermissionsService'; import { WalletRequestPermissionsHandler } from './wallet_requestPermissions'; import { getPermissionsConvertedToMetaMaskStructure } from '../utils/getPermissionsConvertedToMetaMaskStructure'; import { buildRpcCall } from '@src/tests/test-utils'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -jest.mock('@src/utils/extensionUtils', () => ({ - openExtensionNewWindow: jest.fn().mockReturnValue({ id: 321 }), -})); - +jest.mock('@src/background/runtime/openApprovalWindow'); jest.mock('../utils/getPermissionsConvertedToMetaMaskStructure'); describe('background/services/permissions/handlers/wallet_requestPermissions.ts', () => { - beforeEach(() => { - container.clearInstances(); - }); - describe('handleAuthenticated', () => { it('calls handle authenticated', async () => { const handler = new WalletRequestPermissionsHandler( @@ -33,11 +24,6 @@ describe('background/services/permissions/handlers/wallet_requestPermissions.ts' handler, 'handleUnauthenticated' ); - - const actionsServiceMock = { - addAction: jest.fn(), - }; - container.registerInstance(ActionsService, actionsServiceMock as any); const mockRequest = { id: '1234', method: DAppProviderRequest.WALLET_PERMISSIONS, @@ -61,10 +47,6 @@ describe('background/services/permissions/handlers/wallet_requestPermissions.ts' {} as PermissionsService, {} as AccountsService ); - const actionsServiceMock = { - addAction: jest.fn(), - }; - container.registerInstance(ActionsService, actionsServiceMock as any); const mockRequest = { id: '4321', @@ -84,20 +66,10 @@ describe('background/services/permissions/handlers/wallet_requestPermissions.ts' ...mockRequest, result: DEFERRED_RESPONSE, }); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...mockRequest, - actionId: '00000000-0000-0000-0000-000000000000', - displayData: { - domainIcon: 'icon.svg', - domainName: 'Example dapp', - domainUrl: 'example.com', - }, - popupWindowId: 321, - }); - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - `permissions?actionId=00000000-0000-0000-0000-000000000000` + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '4321' }), + `permissions` ); }); }); diff --git a/src/background/services/wallet/handlers/avalanche_sendTransaction.test.ts b/src/background/services/wallet/handlers/avalanche_sendTransaction.test.ts index 4492f5f40..df98bca96 100644 --- a/src/background/services/wallet/handlers/avalanche_sendTransaction.test.ts +++ b/src/background/services/wallet/handlers/avalanche_sendTransaction.test.ts @@ -132,7 +132,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts', issueTxHexMock.mockResolvedValue({ txID: 1 }); getAvalanceProviderXPMock.mockResolvedValue(providerMock); getAddressesMock.mockReturnValue([]); - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); (accountsServiceMock as any).activeAccount = activeAccountMock; (Avalanche.getVmByChainAlias as jest.Mock).mockReturnValue(AVM); (Avalanche.getUtxosByTxFromGlacier as jest.Mock).mockReturnValue(utxosMock); diff --git a/src/background/services/wallet/handlers/avalanche_sendTransaction.ts b/src/background/services/wallet/handlers/avalanche_sendTransaction.ts index d9715ec13..9c306d141 100644 --- a/src/background/services/wallet/handlers/avalanche_sendTransaction.ts +++ b/src/background/services/wallet/handlers/avalanche_sendTransaction.ts @@ -161,7 +161,11 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler< }; } - const actionData = { + const actionData: Action<{ + unsignedTxJson: string; + txData: Avalanche.Tx; + vm: VM; + }> = { ...request, scope, displayData: { @@ -217,7 +221,11 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler< } onActionApproved = async ( - pendingAction: Action, + pendingAction: Action<{ + unsignedTxJson: string; + txData: Avalanche.Tx; + vm: VM; + }>, result, onSuccess, onError, diff --git a/src/background/services/wallet/handlers/avalanche_signTransaction.test.ts b/src/background/services/wallet/handlers/avalanche_signTransaction.test.ts index e3f6e480c..7491feb98 100644 --- a/src/background/services/wallet/handlers/avalanche_signTransaction.test.ts +++ b/src/background/services/wallet/handlers/avalanche_signTransaction.test.ts @@ -104,7 +104,7 @@ describe('src/background/services/wallet/handlers/avalanche_signTransaction', () (utils.getManagerForVM as jest.Mock).mockReturnValue(codecManagerMock); txMock.getSigIndices.mockReturnValue([]); unsignedTxMock.toJSON.mockReturnValue(unsignedTxJson); - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); (Avalanche.getUtxosByTxFromGlacier as jest.Mock).mockReturnValue(utxosMock); (getProvidedUtxos as jest.Mock).mockReturnValue(utxosMock); }); diff --git a/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts b/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts deleted file mode 100644 index c88ad0469..000000000 --- a/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; -import { ethErrors } from 'eth-rpc-errors'; -import { BitcoinSendTransactionHandler } from '@src/background/services/wallet/handlers/bitcoin_sendTransaction'; -import { isBtcAddressInNetwork } from '@src/utils/isBtcAddressInNetwork'; -import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { Action } from '@src/background/services/actions/models'; -import { AccountType } from '../../accounts/models'; -import { ChainId, NetworkVMType } from '@avalabs/core-chains-sdk'; -import { createTransferTx } from '@avalabs/core-wallets-sdk'; - -import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -import { buildRpcCall } from '@src/tests/test-utils'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; -import { measureDuration } from '@src/utils/measureDuration'; - -jest.mock('@avalabs/core-wallets-sdk'); -jest.mock('@src/utils/isBtcAddressInNetwork'); -jest.mock('@src/background/runtime/openApprovalWindow'); -jest.mock('@src/utils/network/getProviderForNetwork'); -jest.mock('@src/utils/measureDuration', () => { - const measureDurationMock = { - start: jest.fn(), - end: jest.fn(), - }; - return { - measureDuration: () => measureDurationMock, - }; -}); - -describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', () => { - const request = { - id: '123', - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: ['address', '1', 10], - site: { - tabId: 1, - } as any, - }; - const frontendTabId = 987; - - const isMainnet = jest.fn(); - const signMock = jest.fn(); - const sendTransactionMock = jest.fn(); - const getBalancesForNetworksMock = jest.fn(); - const captureEventMock = jest.fn(); - - const getBitcoinNetworkMock = jest.fn(); - const activeAccountMock = { - addressBTC: 'btc1', - }; - - const walletServiceMock = { - sign: signMock, - }; - - const networkServiceMock = { - isMainnet: isMainnet, - getBitcoinNetwork: getBitcoinNetworkMock, - sendTransaction: sendTransactionMock, - }; - const accountsServiceMock = {}; - const balanceAggregatorServiceMock = { - getBalancesForNetworks: getBalancesForNetworksMock, - }; - const analyticsServiceMock = { - captureEncryptedEvent: captureEventMock, - }; - - beforeEach(() => { - jest.resetAllMocks(); - isMainnet.mockReturnValue(true); - jest.mocked(isBtcAddressInNetwork).mockReturnValue(true); - getBitcoinNetworkMock.mockResolvedValue({ - vmName: NetworkVMType.BITCOIN, - }); - jest.mocked(getProviderForNetwork).mockReturnValue({ - getScriptsForUtxos: jest.fn().mockResolvedValue([]), - getNetwork: jest.fn(), - waitForTx: jest.fn().mockRejectedValue(new Error()), - } as any); - jest - .mocked(createTransferTx) - .mockReturnValue({ fee: 5, inputs: [], outputs: [] }); - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); - getBitcoinNetworkMock.mockResolvedValue({ - vmName: NetworkVMType.BITCOIN, - rpcUrl: 'RPCURL', - }); - getBalancesForNetworksMock.mockResolvedValue({ - [ChainId.BITCOIN_TESTNET]: { - btc1: { - BTC: {}, - }, - }, - [ChainId.BITCOIN]: { - btc1: { - BTC: {}, - }, - }, - }); - (accountsServiceMock as any).activeAccount = activeAccountMock; - }); - - describe('handleUnauthenticated', () => { - it('returns error for unauthorized requests', async () => { - const handler = new BitcoinSendTransactionHandler( - {} as any, - {} as any, - {} as any, - {} as any, - {} as any - ); - const result = await handler.handleUnauthenticated(buildRpcCall(request)); - - expect(result).toEqual({ - ...request, - error: ethErrors.provider.unauthorized(), - }); - }); - }); - - describe('handleAuthenticated', () => { - const handler = new BitcoinSendTransactionHandler( - {} as any, - networkServiceMock as any, - { - activeAccount: { - addressC: 'abcd1234', - addressBTC: activeAccountMock.addressBTC, - type: AccountType.PRIMARY, - }, - } as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - it('returns error if the active account is imported via WalletConnect', async () => { - jest.mocked(isBtcAddressInNetwork).mockReturnValueOnce(true); - const sendHandler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - { - activeAccount: { - addressC: 'abcd1234', - addressBTC: activeAccountMock.addressBTC, - type: AccountType.WALLET_CONNECT, - }, - } as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - const result = await sendHandler.handleAuthenticated( - buildRpcCall(request) - ); - - expect(result).toEqual({ - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'The active account does not support BTC transactions', - }), - }); - }); - - it('returns error if the active account has no BTC address', async () => { - jest.mocked(isBtcAddressInNetwork).mockReturnValueOnce(true); - const sendHandler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - { - activeAccount: { - addressC: 'abcd1234', - }, - } as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - const result = await sendHandler.handleAuthenticated( - buildRpcCall(request) - ); - - expect(result).toEqual({ - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'The active account does not support BTC transactions', - }), - }); - }); - - it('returns error if address is not provided', async () => { - const req = { - ...request, - params: [undefined, '1', 1], - }; - const result = await handler.handleAuthenticated(buildRpcCall(req)); - - expect(result).toEqual({ - ...req, - error: ethErrors.rpc.invalidParams({ - message: 'Missing address', - }), - }); - }); - - it('returns error if amount is not provided', async () => { - const req = { - ...request, - params: ['tb1qdx76h4su9wavjjpzxqd4ar5ydcy2e05tvp7d6j', undefined, 1], - }; - const result = await handler.handleAuthenticated(buildRpcCall(req)); - - expect(result).toEqual({ - ...req, - error: ethErrors.rpc.invalidParams({ - message: 'Missing amount', - }), - }); - }); - - it('returns error if fee rate is not provided', async () => { - const req = { - ...request, - params: ['btc', '1', undefined], - }; - const result = await handler.handleAuthenticated(buildRpcCall(req)); - - expect(result).toEqual({ - ...req, - error: ethErrors.rpc.invalidParams({ - message: 'Missing fee rate', - }), - }); - }); - - it('returns error if address is invalid', async () => { - (isBtcAddressInNetwork as jest.Mock).mockReturnValueOnce(false); - const result = await handler.handleAuthenticated(buildRpcCall(request)); - expect(result).toEqual({ - ...request, - error: ethErrors.rpc.invalidParams({ - message: 'Not a valid address.', - }), - }); - }); - - it('returns error if there is no active account', async () => { - (isBtcAddressInNetwork as jest.Mock).mockReturnValueOnce(true); - const sendHandler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - {} as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - const result = await sendHandler.handleAuthenticated({ request } as any); - expect(result).toEqual({ - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'No active account found', - }), - }); - }); - - it('returns error if unable to construct a transaction', async () => { - jest.mocked(createTransferTx).mockImplementationOnce(() => { - throw new Error('hmm'); - }); - - const result = await handler.handleAuthenticated(buildRpcCall(request)); - expect(result).toEqual({ - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'Unable to construct the transaction.', - }), - }); - }); - - it('opens the approval window and returns deferred response', async () => { - const sendHandler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - accountsServiceMock as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - const result = await sendHandler.handleAuthenticated( - buildRpcCall(request) - ); - - expect(openApprovalWindow).toHaveBeenCalledWith( - expect.objectContaining({ - ...request, - displayData: { - address: 'address', - amount: 1, - balance: {}, - feeRate: 10, - from: 'btc1', - sendFee: 5, - }, - }), - 'approve/bitcoinSignTx' - ); - - expect(result).toEqual({ - ...request, - result: DEFERRED_RESPONSE, - }); - }); - - it('works even if active network is not bitcoin', async () => { - const sendHandler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - { - ...networkServiceMock, - activeNetwork: { - vmName: NetworkVMType.EVM, - }, - } as any, - accountsServiceMock as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - const result = await sendHandler.handleAuthenticated( - buildRpcCall(request) - ); - - expect(result).toEqual({ - ...request, - result: DEFERRED_RESPONSE, - }); - }); - }); - - describe('onActionApproved', () => { - const pendingActionMock = { - displayData: { - address: 'address', - amount: 1, - balance: {}, - feeRate: 10, - from: 'btc1', - sendFee: 5, - }, - site: { - domain: 'core.app', - }, - } as unknown as Action; - - it('returns error when signing fails', async () => { - const handler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - accountsServiceMock as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - getBitcoinNetworkMock.mockResolvedValue({ - chainId: ChainId.BITCOIN_TESTNET, - vmName: NetworkVMType.BITCOIN, - }); - - const onSuccessMock = jest.fn(); - const onErrorMock = jest.fn(); - signMock.mockRejectedValueOnce(new Error('sign failed')); - - await handler.onActionApproved( - pendingActionMock, - {}, - onSuccessMock, - onErrorMock, - frontendTabId - ); - - expect(onErrorMock).toHaveBeenCalledWith(expect.any(Error)); - expect(onErrorMock).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'sign failed', - }) - ); - }); - - it('returns success when successfull', async () => { - const handler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - accountsServiceMock as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - - getBitcoinNetworkMock.mockResolvedValue({ - chainId: ChainId.BITCOIN_TESTNET, - vmName: NetworkVMType.BITCOIN, - }); - - const onSuccessMock = jest.fn(); - const onErrorMock = jest.fn(); - - signMock.mockResolvedValue({ signedTx: 'resultHash' }); - sendTransactionMock.mockReturnValueOnce('resultHash'); - await handler.onActionApproved( - pendingActionMock, - {}, - onSuccessMock, - onErrorMock, - frontendTabId - ); - expect(signMock).toHaveBeenCalledWith( - { - inputs: [], - outputs: [], - }, - { - chainId: ChainId.BITCOIN_TESTNET, - vmName: NetworkVMType.BITCOIN, - }, - frontendTabId - ); - expect(onSuccessMock).toHaveBeenCalledWith('resultHash'); - }); - - it('reports time to confirmation event', async () => { - const handler = new BitcoinSendTransactionHandler( - walletServiceMock as any, - networkServiceMock as any, - accountsServiceMock as any, - balanceAggregatorServiceMock as any, - analyticsServiceMock as any - ); - const durationMock = measureDuration(); - jest.mocked(durationMock.end).mockReturnValue(1000); - jest.mocked(getProviderForNetwork).mockReturnValue({ - getScriptsForUtxos: jest.fn().mockResolvedValue([]), - getNetwork: jest.fn(), - waitForTx: jest.fn().mockResolvedValue({}), - } as any); - - getBitcoinNetworkMock.mockResolvedValue({ - chainId: ChainId.BITCOIN_TESTNET, - vmName: NetworkVMType.BITCOIN, - rpcUrl: 'RPCURL', - }); - - const onSuccessMock = jest.fn(); - const onErrorMock = jest.fn(); - - signMock.mockResolvedValue({ signedTx: 'resultHash' }); - sendTransactionMock.mockReturnValueOnce('resultHash'); - await handler.onActionApproved( - pendingActionMock, - {}, - onSuccessMock, - onErrorMock, - frontendTabId - ); - - expect(analyticsServiceMock.captureEncryptedEvent).toHaveBeenCalledTimes( - 1 - ); - expect(analyticsServiceMock.captureEncryptedEvent).toHaveBeenCalledWith({ - name: 'TransactionTimeToConfirmation', - properties: { - chainId: 4503599627370475, - duration: 1000, - site: 'core.app', - txType: 'send', - rpcUrl: 'RPCURL', - }, - windowId: undefined, - }); - }); - }); -}); diff --git a/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts b/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts deleted file mode 100644 index 67092d0d0..000000000 --- a/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { injectable } from 'tsyringe'; -import { WalletService } from '../WalletService'; -import { - DAppProviderRequest, - JsonRpcRequestParams, -} from '@src/background/connections/dAppConnection/models'; -import { DAppRequestHandler } from '@src/background/connections/dAppConnection/DAppRequestHandler'; -import { Action } from '../../actions/models'; -import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { NetworkService } from '@src/background/services/network/NetworkService'; -import { AnalyticsServicePosthog } from '@src/background/services/analytics/AnalyticsServicePosthog'; -import { ethErrors } from 'eth-rpc-errors'; -import { - DisplayData_BitcoinSendTx, - TxDisplayOptions, -} from '@src/background/services/wallet/handlers/models'; -import { AccountsService } from '@src/background/services/accounts/AccountsService'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { BalanceAggregatorService } from '@src/background/services/balances/BalanceAggregatorService'; -import { Account, WalletConnectAccount } from '../../accounts/models'; -import { BitcoinProvider } from '@avalabs/core-wallets-sdk'; -import { BtcSendOptions } from '@src/pages/Send/models'; -import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; -import { EnsureDefined } from '@src/background/models'; -import { isWalletConnectAccount } from '../../accounts/utils/typeGuards'; -import { - buildBtcTx, - getBtcInputUtxos, - validateBtcSend, -} from '@src/utils/send/btcSendUtils'; -import { SendErrorMessage } from '@src/utils/send/models'; -import { resolve } from '@avalabs/core-utils-sdk'; - -import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -import { runtime } from 'webextension-polyfill'; -import { TokenWithBalanceBTC } from '@avalabs/vm-module-types'; -import { measureDuration } from '@src/utils/measureDuration'; -import { noop } from '@src/utils/noop'; - -type BitcoinTxParams = [ - address: string, - amount: string, - feeRate: number, - displayOptions?: TxDisplayOptions -]; - -@injectable() -export class BitcoinSendTransactionHandler extends DAppRequestHandler< - BitcoinTxParams, - string -> { - methods = [DAppProviderRequest.BITCOIN_SEND_TRANSACTION]; - - constructor( - private walletService: WalletService, - private networkService: NetworkService, - private accountService: AccountsService, - private balanceAggregatorService: BalanceAggregatorService, - private analyticsServicePosthog: AnalyticsServicePosthog - ) { - super(); - } - - #getRpcErrorMessage = (error: SendErrorMessage) => { - switch (error) { - case SendErrorMessage.ADDRESS_REQUIRED: - return 'Missing address'; - - case SendErrorMessage.AMOUNT_REQUIRED: - return 'Missing amount'; - - case SendErrorMessage.INVALID_NETWORK_FEE: - return 'Missing fee rate'; - - case SendErrorMessage.INVALID_ADDRESS: - return 'Not a valid address.'; - - default: - return 'Unable to construct the transaction.'; - } - }; - - #getBalance = async ( - account: EnsureDefined - ): Promise => { - const btcChainID = this.networkService.isMainnet() - ? ChainId.BITCOIN - : ChainId.BITCOIN_TESTNET; - - // Refresh UTXOs before to ensure that UTXOs is updated - const balances = await this.balanceAggregatorService.getBalancesForNetworks( - [btcChainID], - [account] - ); - - const balance = balances[btcChainID]?.[account.addressBTC]?.['BTC']; - - if (balance) { - return balance as TokenWithBalanceBTC; - } - }; - - #isSupportedAccount( - account?: Account - ): account is EnsureDefined< - Exclude, - 'addressBTC' - > { - if (!account) { - return false; - } - - if (isWalletConnectAccount(account)) { - return false; - } - - return Boolean(account.addressBTC); - } - - handleAuthenticated = async ({ - request, - scope, - }: JsonRpcRequestParams) => { - if (!this.accountService.activeAccount) { - return { - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'No active account found', - }), - }; - } - - if (!this.#isSupportedAccount(this.accountService.activeAccount)) { - return { - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'The active account does not support BTC transactions', - }), - }; - } - - const [address, amount, feeRate, displayOptions] = (request.params ?? - []) as BitcoinTxParams; - const isMainnet = this.networkService.isMainnet(); - const token = await this.#getBalance( - this.accountService.activeAccount as EnsureDefined - ); - - // Only the extension UI is allowed to suggest custom display options - if (displayOptions && request.site?.domain !== runtime.id) { - return { - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'Unauthorized use of display options', - }), - }; - } - - if (!token) { - return { - ...request, - error: ethErrors.rpc.internal({ - message: 'Unknown balance for BTC', - }), - }; - } - - const [network, networkError] = await resolve( - this.networkService.getBitcoinNetwork() - ); - if (networkError || !network) { - return { - ...request, - error: ethErrors.rpc.internal({ - message: 'Bitcoin network not found', - }), - }; - } - const provider = getProviderForNetwork(network) as BitcoinProvider; - const utxos = await getBtcInputUtxos(provider, token, feeRate); - - const from = this.accountService.activeAccount.addressBTC; - const validationError = validateBtcSend( - from, - { - address, - amount: Number(amount), - feeRate, - token, - } as BtcSendOptions, - utxos, - isMainnet - ); - - if (validationError) { - return { - ...request, - error: ethErrors.rpc.invalidParams({ - message: this.#getRpcErrorMessage(validationError), - }), - }; - } - - try { - const { fee: sendFee } = await buildBtcTx( - this.accountService.activeAccount.addressBTC, - provider, - { - amount: Number(amount), - feeRate, - address, - token, - } - ); - - const displayData: DisplayData_BitcoinSendTx = { - from: this.accountService.activeAccount.addressBTC, - address, - amount: Number(amount), - sendFee, - feeRate, - balance: token, - displayOptions, - }; - - const actionData = { - ...request, - scope, - displayData, - }; - - await openApprovalWindow(actionData, `approve/bitcoinSignTx`); - } catch (err) { - return { - ...request, - error: ethErrors.rpc.invalidRequest({ - message: 'Unable to construct the transaction.', - }), - }; - } - - return { - ...request, - result: DEFERRED_RESPONSE, - }; - }; - - handleUnauthenticated = ({ request }) => { - return { - ...request, - error: ethErrors.provider.unauthorized(), - }; - }; - - onActionApproved = async ( - pendingAction: Action, - _result, - onSuccess, - onError, - frontendTabId?: number - ) => { - const measurement = measureDuration(); - measurement.start(); - try { - const { address, amount, from, feeRate, balance } = - pendingAction.displayData; - const btcChainID = this.networkService.isMainnet() - ? ChainId.BITCOIN - : ChainId.BITCOIN_TESTNET; - - const [network, networkError] = await resolve( - this.networkService.getBitcoinNetwork() - ); - if (networkError || !network) { - throw new Error('Bitcoin network not found'); - } - - const provider = getProviderForNetwork(network) as BitcoinProvider; - - const { inputs, outputs } = await buildBtcTx(from, provider, { - amount, - address, - token: balance, - feeRate, - }); - - if (!inputs || !outputs) { - throw new Error('Unable to create transaction'); - } - - const result = await this.walletService.sign( - { inputs, outputs }, - network, - frontendTabId - ); - - const hash = await this.networkService.sendTransaction(result, network); - - // Refresh UTXOs - if (this.#isSupportedAccount(this.accountService.activeAccount)) { - this.#getBalance(this.accountService.activeAccount); - } - - onSuccess(hash); - - provider - .waitForTx(hash) - .then(() => { - const duration = measurement.end(); - this.analyticsServicePosthog.captureEncryptedEvent({ - name: 'TransactionTimeToConfirmation', - windowId: crypto.randomUUID(), - properties: { - duration, - txType: 'send', - chainId: btcChainID, - site: pendingAction.site?.domain, - rpcUrl: network.rpcUrl, - }, - }); - }) - .catch(noop); - } catch (e) { - // clean up pending measurement - measurement.end(); - onError(e); - } - }; -} 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 index 6dae038b4..424307e9e 100644 --- a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts +++ b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts @@ -149,7 +149,7 @@ const displayValuesMock: TransactionDisplayValues = { describe('background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts', () => { const networkService = new NetworkService({} as any, {} as any); - const networkFeeService = new NetworkFeeService(); + const networkFeeService = new NetworkFeeService({} as any); const balanceAggregatorService = new BalanceAggregatorService( {} as any, {} as any, @@ -237,7 +237,7 @@ describe('background/services/wallet/handlers/eth_sendTransaction/eth_sendTransa .captureEncryptedEvent.mockResolvedValue(); jest.mocked(isBitcoinNetwork).mockReturnValue(false); (encryptAnalyticsData as jest.Mock).mockResolvedValue(mockedEncryptResult); - jest.mocked(openApprovalWindow).mockResolvedValue(undefined); + jest.mocked(openApprovalWindow).mockResolvedValue({} as any); jest.mocked(txToCustomEvmTx).mockReturnValue({ maxFeePerGas: '0x54', maxPriorityFeePerGas: 1n, diff --git a/src/background/services/web3/handlers/connect.test.ts b/src/background/services/web3/handlers/connect.test.ts index 10368c434..9b174c99d 100644 --- a/src/background/services/web3/handlers/connect.test.ts +++ b/src/background/services/web3/handlers/connect.test.ts @@ -1,25 +1,17 @@ import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; -import { openExtensionNewWindow } from '@src/utils/extensionUtils'; import { ethErrors } from 'eth-rpc-errors'; -import { container } from 'tsyringe'; import { AccountsService } from '../../accounts/AccountsService'; import { AccountType } from '../../accounts/models'; -import { ActionsService } from '../../actions/ActionsService'; import { Action, ActionStatus } from '../../actions/models'; import { PermissionsService } from '../../permissions/PermissionsService'; import { ConnectRequestHandler } from './connect'; import { buildRpcCall } from '@src/tests/test-utils'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; -jest.mock('@src/utils/extensionUtils', () => ({ - openExtensionNewWindow: jest.fn().mockReturnValue({ id: 123 }), -})); +jest.mock('@src/background/runtime/openApprovalWindow'); describe('background/services/web3/handlers/connect.ts', () => { - beforeEach(() => { - container.clearInstances(); - }); - describe('handleAuthenticated', () => { it('returns error when no active account available', async () => { const handler = new ConnectRequestHandler( @@ -91,11 +83,6 @@ describe('background/services/web3/handlers/connect.ts', () => { {} as PermissionsService ); - const actionsServiceMock = { - addAction: jest.fn(), - }; - container.registerInstance(ActionsService, actionsServiceMock as any); - const mockRequest = { id: '1235', method: DAppProviderRequest.CONNECT_METHOD, @@ -115,21 +102,10 @@ describe('background/services/web3/handlers/connect.ts', () => { result: DEFERRED_RESPONSE, }); - expect(actionsServiceMock.addAction).toHaveBeenCalledTimes(1); - expect(actionsServiceMock.addAction).toHaveBeenCalledWith({ - ...mockRequest, - actionId: '00000000-0000-0000-0000-000000000000', - displayData: { - domainIcon: 'icon.svg', - domainName: 'Example dapp', - domainUrl: 'example.com', - }, - popupWindowId: 123, - }); - - expect(openExtensionNewWindow).toHaveBeenCalledTimes(1); - expect(openExtensionNewWindow).toHaveBeenCalledWith( - `permissions?actionId=00000000-0000-0000-0000-000000000000` + expect(openApprovalWindow).toHaveBeenCalledTimes(1); + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ id: '1235' }), + 'permissions' ); }); }); diff --git a/src/background/vmModules/ApprovalController.test.ts b/src/background/vmModules/ApprovalController.test.ts new file mode 100644 index 000000000..fbc7b5b5b --- /dev/null +++ b/src/background/vmModules/ApprovalController.test.ts @@ -0,0 +1,266 @@ +import { ChainId } from '@avalabs/core-chains-sdk'; +import { errorCodes, 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'; + +import { WalletService } from '../services/wallet/WalletService'; +import { NetworkService } from '../services/network/NetworkService'; +import { openApprovalWindow } from '../runtime/openApprovalWindow'; + +import { ApprovalParamsWithContext } from './models'; +import { buildBtcSendTransactionAction } from './helpers/buildBtcSendTransactionAction'; +import { ApprovalController } from './ApprovalController'; + +jest.mock('tsyringe', () => { + return { + ...jest.requireActual('tsyringe'), + container: { + resolve: jest.fn(), + }, + }; +}); +jest.mock('../runtime/openApprovalWindow'); +jest.mock('@src/utils/network/getProviderForNetwork'); +jest.mock('@src/utils/send/btcSendUtils'); + +const btcNetwork = { + chainId: ChainId.BITCOIN_TESTNET, + rpcUrl: '', +} as any; + +const dappInfo: DappInfo = { + icon: 'icon', + name: 'name', + url: 'https://extension.url', +}; + +describe('src/background/vmModules/ApprovalController', () => { + describe('requestApproval()', () => { + 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); + }); + + it('returns error if network cannot be resolved', async () => { + expect( + await controller.requestApproval({ + request: { + chainId: 'abcd-1234', + }, + } as any) + ).toEqual({ + 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), + 'approve/generic' + ); + }); + + it('returns error when user cancels the transaction', async () => { + const promise = controller.requestApproval(approvalParams); + + await new Promise(process.nextTick); + + const action = { + ...buildBtcSendTransactionAction(approvalParams), + actionId: crypto.randomUUID(), + }; + + controller.onRejected(action); + + expect(await promise).toEqual({ + error: providerErrors.userRejectedRequest(), + }); + }); + + it('returns error if transaction cannot be signed', async () => { + const signingError = new Error('Invalid transaction payload'); + walletService.sign.mockRejectedValueOnce(signingError); + + const promise = controller.requestApproval(approvalParams); + const action = { + ...buildBtcSendTransactionAction(approvalParams), + actionId: crypto.randomUUID(), + }; + + // Await network resolution + await new Promise(process.nextTick); + + controller.onApproved(action); + + // Wait for transaction to be constructed + await new Promise(process.nextTick); + + expect(walletService.sign).toHaveBeenCalledWith( + { + inputs: btcTx.inputs, + outputs: btcTx.outputs, + }, + btcNetwork + ); + + expect(await promise).toEqual({ + error: rpcErrors.internal({ + message: 'Unable to sign the message', + data: { + originalError: signingError, + }, + }), + }); + }); + + it('signs the transaction on user approval', async () => { + const signedTx = '0x1234'; + + walletService.sign.mockResolvedValueOnce({ + signedTx, + }); + + const promise = controller.requestApproval(approvalParams); + const action = { + ...buildBtcSendTransactionAction(approvalParams), + actionId: crypto.randomUUID(), + }; + + // Await network resolution + await new Promise(process.nextTick); + + 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( + { + inputs: btcTx.inputs, + outputs: btcTx.outputs, + }, + btcNetwork + ); + + expect(await promise).toEqual({ signedData: signedTx }); + }); + }); + }); +}); diff --git a/src/background/vmModules/ApprovalController.ts b/src/background/vmModules/ApprovalController.ts new file mode 100644 index 000000000..15dcbce75 --- /dev/null +++ b/src/background/vmModules/ApprovalController.ts @@ -0,0 +1,205 @@ +import { singleton } from 'tsyringe'; +import { + ApprovalParams, + ApprovalResponse, + ApprovalController as IApprovalController, + RpcError, + RpcMethod, +} 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 { WalletService } from '../services/wallet/WalletService'; +import { Action } 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'; + +@singleton() +export class ApprovalController implements IApprovalController { + #walletService: WalletService; + #networkService: NetworkService; + + #requests = new Map< + string, + { + params: ApprovalParams; + network: NetworkWithCaipId; + resolve: (response: ApprovalResponse) => void; + } + >(); + + constructor(walletService: WalletService, networkService: NetworkService) { + this.#walletService = walletService; + this.#networkService = networkService; + } + + onTransactionConfirmed = () => { + // Transaction Confirmed. Show a toast? Trigger browser notification?', + }; + + onTransactionReverted = () => { + // Transaction Reverted. Show a toast? Trigger browser notification?', + }; + + onRejected = async (action: Action) => { + if (!action.actionId) { + return; + } + + const request = this.#requests.get(action.actionId); + + if (!request) { + return; + } + + const { resolve } = request; + + resolve({ + error: providerErrors.userRejectedRequest(), + }); + this.#requests.delete(action.actionId); + }; + + onApproved = async (action: Action) => { + if (!action.actionId) { + return; + } + + const request = this.#requests.get(action.actionId); + + if (!request) { + return; + } + + const { params, network, resolve } = request; + + try { + const { signedTx: signedData, txHash } = await this.#handleApproval( + params, + action, + network + ); + + if (signedData) { + resolve({ signedData }); + } else if (txHash) { + resolve({ txHash }); + } else { + resolve({ + error: rpcErrors.internal({ + message: 'Unsupported signing result type', + }), + }); + } + } catch (err) { + resolve({ + error: rpcErrors.internal({ + message: 'Unable to sign the message', + data: { + originalError: err, + }, + }), + }); + } finally { + this.#requests.delete(action.actionId); + } + }; + + /** + * This method should never throw. Instead, return an { error } object. + */ + requestApproval = async ( + params: ApprovalParamsWithContext + ): Promise => { + const network = await this.#networkService.getNetwork( + params.request.chainId + ); + if (!network) { + return { + error: rpcErrors.invalidRequest({ + message: 'Unsupported network', + data: params.request.chainId, + }), + }; + } + + const [preparedAction, actionError] = this.#try(() => + this.#buildAction(params) + ); + if (actionError) return { error: actionError }; + + const action = (await openApprovalWindow( + preparedAction, + 'approve/generic' + )) as EnsureDefined; + + return new Promise((resolve) => { + this.#requests.set(action.actionId, { + params, + network, + resolve, + }); + }); + }; + + #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]; + } + } + + #handleApproval = async ( + params: ApprovalParams, + action: Action, + network: NetworkWithCaipId + ) => { + 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 (!inputs || !outputs) { + throw new Error('Unable to construct BTC transaction'); + } + + return await this.#walletService.sign({ inputs, outputs }, network); + } + + 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, + }); + }; +} diff --git a/src/background/vmModules/ModuleManager.test.ts b/src/background/vmModules/ModuleManager.test.ts index b37892cc4..ba04b5246 100644 --- a/src/background/vmModules/ModuleManager.test.ts +++ b/src/background/vmModules/ModuleManager.test.ts @@ -1,13 +1,23 @@ import { NetworkVMType } from '@avalabs/core-chains-sdk'; -import ModuleManager from './ModuleManager'; +import { ModuleManager } from './ModuleManager'; import { VMModuleError } from './models'; +import { ApprovalController } from './ApprovalController'; describe('ModuleManager', () => { + let manager: ModuleManager; + let controller: ApprovalController; + + beforeEach(() => { + controller = { + requestApproval: jest.fn(), + } as any; + manager = new ModuleManager(controller); + }); describe('when not initialized', () => { it('should throw not initialized error', async () => { try { - await ModuleManager.loadModule('eip155:123', 'eth_randomMethod'); + await manager.loadModule('eip155:123', 'eth_randomMethod'); } catch (e: any) { expect(e.data.reason).toBe(VMModuleError.ModulesNotInitialized); } @@ -16,44 +26,21 @@ describe('ModuleManager', () => { describe('when initialized', () => { beforeEach(async () => { - await ModuleManager.init(); + await manager.activate(); }); it('should load the correct modules', async () => { const params = [ - { - chainId: 'eip155:1', - method: 'eth_randomMethod', - name: NetworkVMType.EVM, - }, { chainId: 'bip122:000000000019d6689c085ae165831e93', method: 'bitcoin_sendTransaction', name: NetworkVMType.BITCOIN, }, - { - chainId: 'avax:2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM', - method: 'avalanche_randomMethod', - name: NetworkVMType.AVM, - }, - { - chainId: 'avax:11111111111111111111111111111111LpoYY', - method: 'avalanche_randomMethod', - name: NetworkVMType.PVM, - }, - { - chainId: 'eip2256:1', - method: 'eth_randomMethod', - name: NetworkVMType.CoreEth, - }, ]; await Promise.all( params.map(async (param) => { - const module = await ModuleManager.loadModule( - param.chainId, - param.method - ); + const module = await manager.loadModule(param.chainId, param.method); expect(module?.getManifest()?.name.toLowerCase()).toContain( param.name.toLowerCase() ); @@ -63,7 +50,7 @@ describe('ModuleManager', () => { it('should have thrown with incorrect chainId', async () => { try { - await ModuleManager.loadModule('eip155:123', 'eth_randomMethod'); + await manager.loadModule('eip155:123', 'eth_randomMethod'); } catch (e: any) { expect(e.data.reason).toBe(VMModuleError.UnsupportedChain); } @@ -71,15 +58,15 @@ describe('ModuleManager', () => { it('should have thrown with incorrect method', async () => { try { - await ModuleManager.loadModule('eip155:1', 'evth_randomMethod'); + await manager.loadModule('eip155:1', 'evth_randomMethod'); } catch (e: any) { - expect(e.data.reason).toBe(VMModuleError.UnsupportedMethod); + expect(e.data.reason).toBe(VMModuleError.UnsupportedChain); } }); it('should have thrown with incorrect namespace', async () => { try { - await ModuleManager.loadModule('avalanche:1', 'eth_method'); + await manager.loadModule('avalanche:1', 'eth_method'); } catch (e: any) { expect(e.data.reason).toBe(VMModuleError.UnsupportedNamespace); } diff --git a/src/background/vmModules/ModuleManager.ts b/src/background/vmModules/ModuleManager.ts index d37c3db6b..4461c9de9 100644 --- a/src/background/vmModules/ModuleManager.ts +++ b/src/background/vmModules/ModuleManager.ts @@ -1,24 +1,24 @@ import { Environment, Module } from '@avalabs/vm-module-types'; import { BitcoinModule } from '@avalabs/bitcoin-module'; import { ethErrors } from 'eth-rpc-errors'; +import { singleton } from 'tsyringe'; import { assertPresent } from '@src/utils/assertions'; import { isDevelopment } from '@src/utils/environment'; import { NetworkWithCaipId } from '../services/network/models'; -import { AVMModule } from './mocks/avm'; -import { EVMModule } from './mocks/evm'; -import { PVMModule } from './mocks/pvm'; -import { CoreEthModule } from './mocks/coreEth'; import { VMModuleError } from './models'; +import { ApprovalController } from './ApprovalController'; // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md // Syntax for namespace is defined in CAIP-2 const NAMESPACE_REGEX = new RegExp('^[-a-z0-9]{3,8}$'); -class ModuleManager { +@singleton() +export class ModuleManager { #_modules: Module[] | undefined; + #approvalController: ApprovalController; get #modules(): Module[] { assertPresent(this.#_modules, VMModuleError.ModulesNotInitialized); @@ -30,7 +30,11 @@ class ModuleManager { this.#_modules = modules; } - async init(): Promise { + constructor(controller: ApprovalController) { + this.#approvalController = controller; + } + + async activate(): Promise { if (this.#_modules !== undefined) return; const environment = isDevelopment() @@ -38,24 +42,10 @@ class ModuleManager { : Environment.PRODUCTION; this.#modules = [ - new EVMModule(), new BitcoinModule({ environment, - approvalController: { - requestApproval: () => { - throw new Error('not implemented'); - }, - onTransactionConfirmed: () => { - throw new Error('not implemented'); - }, - onTransactionReverted: () => { - throw new Error('not implemented'); - }, - }, + approvalController: this.#approvalController, }), - new AVMModule(), - new CoreEthModule(), - new PVMModule(), ]; } @@ -137,5 +127,3 @@ class ModuleManager { }); } } - -export default new ModuleManager(); diff --git a/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts b/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts new file mode 100644 index 000000000..3197c74ec --- /dev/null +++ b/src/background/vmModules/helpers/buildBtcSendTransactionAction.test.ts @@ -0,0 +1,85 @@ +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 new file mode 100644 index 000000000..4ddd4d967 --- /dev/null +++ b/src/background/vmModules/helpers/buildBtcSendTransactionAction.ts @@ -0,0 +1,24 @@ +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/background/vmModules/mocks/avm.manifest.json b/src/background/vmModules/mocks/avm.manifest.json deleted file mode 100644 index 22923ac74..000000000 --- a/src/background/vmModules/mocks/avm.manifest.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "AVM", - "description": "", - "version": "0.0.1", - "sources": { - "module": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/bundle.js", - "packageName": "@avalabs/avm-module", - "registry": "https://registry.npmjs.org" - } - } - }, - "provider": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/provider.js", - "packageName": "@avalabs/avm-module", - "registry": "https://registry.npmjs.org" - } - } - } - }, - "network": { - "chainIds": [ - "avax:2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", - "avax:2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm" - ], - "namespaces": ["avax"] - }, - "cointype": "60", - "permissions": { - "rpc": { - "dapps": true, - "methods": ["avalanche_sendTransaction", "avalanche_*"] - } - }, - "manifestVersion": "0.0" -} diff --git a/src/background/vmModules/mocks/avm.ts b/src/background/vmModules/mocks/avm.ts deleted file mode 100644 index c62af070d..000000000 --- a/src/background/vmModules/mocks/avm.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - Manifest, - Module, - parseManifest, - GetTransactionHistory, - NetworkFees, - NetworkContractToken, - RpcRequest, - TransactionHistoryResponse, - GetBalancesResponse, - Network, - RpcResponse, -} from '@avalabs/vm-module-types'; -import { ethErrors } from 'eth-rpc-errors'; - -import manifest from './avm.manifest.json'; - -export class AVMModule implements Module { - getManifest(): Manifest | undefined { - const result = parseManifest(manifest); - return result.success ? result.data : undefined; - } - - getBalances(): Promise { - return Promise.resolve({}); - } - - getTransactionHistory( - _: GetTransactionHistory // eslint-disable-line @typescript-eslint/no-unused-vars - ): Promise { - return Promise.resolve({ transactions: [], nextPageToken: '' }); - } - - getNetworkFee(): Promise { - return Promise.resolve({ - low: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - medium: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - high: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - baseFee: 0n, - isFixedFee: false, - }); - } - - async getAddress() { - return {}; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getTokens(_: Network): Promise { - return Promise.resolve([]); - } - - async onRpcRequest(request: RpcRequest): Promise { - return { - error: ethErrors.rpc.methodNotSupported({ - data: `Method ${request.method} not supported`, - }) as any, // TODO: fix it - }; - } - - getProvider; -} diff --git a/src/background/vmModules/mocks/coreEth.manifest.json b/src/background/vmModules/mocks/coreEth.manifest.json deleted file mode 100644 index 2e571a1e7..000000000 --- a/src/background/vmModules/mocks/coreEth.manifest.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "CoreEth", - "description": "", - "version": "0.0.1", - "sources": { - "module": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/bundle.js", - "packageName": "@avalabs/coreEth-module", - "registry": "https://registry.npmjs.org" - } - } - }, - "provider": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/provider.js", - "packageName": "@avalabs/coreEth-module", - "registry": "https://registry.npmjs.org" - } - } - } - }, - "network": { - "chainIds": ["eip2256:1"], - "namespaces": ["eip2256"] - }, - "cointype": "60", - "permissions": { - "rpc": { - "dapps": true, - "methods": ["eth_sendTransaction", "eth_*"] - } - }, - "manifestVersion": "0.0" -} diff --git a/src/background/vmModules/mocks/coreEth.ts b/src/background/vmModules/mocks/coreEth.ts deleted file mode 100644 index 5b0bf91ca..000000000 --- a/src/background/vmModules/mocks/coreEth.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - GetTransactionHistory, - Manifest, - Module, - TransactionHistoryResponse, - RpcRequest, - parseManifest, - NetworkContractToken, - Network, - NetworkFees, - GetBalancesResponse, - RpcResponse, -} from '@avalabs/vm-module-types'; -import { ethErrors } from 'eth-rpc-errors'; - -import manifest from './coreEth.manifest.json'; - -export class CoreEthModule implements Module { - getManifest(): Manifest | undefined { - const result = parseManifest(manifest); - return result.success ? result.data : undefined; - } - - getBalances(): Promise { - return Promise.resolve({}); - } - - getTransactionHistory( - _: GetTransactionHistory // eslint-disable-line @typescript-eslint/no-unused-vars - ): Promise { - return Promise.resolve({ transactions: [], nextPageToken: '' }); - } - - getNetworkFee(): Promise { - return Promise.resolve({ - low: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - medium: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - high: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - baseFee: 0n, - isFixedFee: false, - }); - } - - async getAddress() { - return {}; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getTokens(_: Network): Promise { - return Promise.resolve([]); - } - - async onRpcRequest(request: RpcRequest): Promise { - return { - error: ethErrors.rpc.methodNotSupported({ - data: `Method ${request.method} not supported`, - }) as any, // TODO: fix it - }; - } - getProvider; -} diff --git a/src/background/vmModules/mocks/evm.manifest.json b/src/background/vmModules/mocks/evm.manifest.json deleted file mode 100644 index effaee31c..000000000 --- a/src/background/vmModules/mocks/evm.manifest.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "EVM", - "description": "", - "version": "0.0.1", - "sources": { - "module": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/bundle.js", - "packageName": "@avalabs/evm-module", - "registry": "https://registry.npmjs.org" - } - } - }, - "provider": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/provider.js", - "packageName": "@avalabs/evm-module", - "registry": "https://registry.npmjs.org" - } - } - } - }, - "network": { - "chainIds": ["eip155:1", "eip155:43114", "eip155:43113"], - "namespaces": ["eip155"] - }, - "cointype": "60", - "permissions": { - "rpc": { - "dapps": true, - "methods": ["eth_sendTransaction", "eth_*"] - } - }, - "manifestVersion": "0.0" -} diff --git a/src/background/vmModules/mocks/evm.ts b/src/background/vmModules/mocks/evm.ts deleted file mode 100644 index d0ce6c242..000000000 --- a/src/background/vmModules/mocks/evm.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - Module, - parseManifest, - GetTransactionHistory, - RpcRequest, - TransactionHistoryResponse, - Network, - GetBalancesResponse, - NetworkFees, - NetworkContractToken, - RpcResponse, -} from '@avalabs/vm-module-types'; -import { ethErrors } from 'eth-rpc-errors'; - -import manifest from './evm.manifest.json'; - -export class EVMModule implements Module { - getManifest() { - const result = parseManifest(manifest); - return result.success ? result.data : undefined; - } - - getBalances(): Promise { - return Promise.resolve({}); - } - - getTransactionHistory( - _: GetTransactionHistory // eslint-disable-line @typescript-eslint/no-unused-vars - ): Promise { - return Promise.resolve({ transactions: [], nextPageToken: '' }); - } - - getNetworkFee(): Promise { - return Promise.resolve({ - low: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - medium: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - high: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - baseFee: 0n, - isFixedFee: false, - }); - } - - async getAddress() { - return {}; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getTokens(_: Network): Promise { - return Promise.resolve([]); - } - - async onRpcRequest(request: RpcRequest): Promise { - return { - error: ethErrors.rpc.methodNotSupported({ - data: `Method ${request.method} not supported`, - }) as any, // TODO: fix it - }; - } - getProvider; -} diff --git a/src/background/vmModules/mocks/pvm.manifest.json b/src/background/vmModules/mocks/pvm.manifest.json deleted file mode 100644 index 12dcee044..000000000 --- a/src/background/vmModules/mocks/pvm.manifest.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "PVM", - "description": "", - "version": "0.0.1", - "sources": { - "module": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/bundle.js", - "packageName": "@avalabs/pvm-module", - "registry": "https://registry.npmjs.org" - } - } - }, - "provider": { - "checksum": "", - "location": { - "npm": { - "filePath": "dist/provider.js", - "packageName": "@avalabs/pvm-module", - "registry": "https://registry.npmjs.org" - } - } - } - }, - "network": { - "chainIds": [ - "avax:11111111111111111111111111111111LpoYY", - "avax:fuji-11111111111111111111111111111111LpoYY" - ], - "namespaces": ["avax"] - }, - "cointype": "60", - "permissions": { - "rpc": { - "dapps": true, - "methods": ["avalanche_sendTransaction", "avalanche_*"] - } - }, - "manifestVersion": "0.0" -} diff --git a/src/background/vmModules/mocks/pvm.ts b/src/background/vmModules/mocks/pvm.ts deleted file mode 100644 index 6bee26586..000000000 --- a/src/background/vmModules/mocks/pvm.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - GetBalancesResponse, - GetTransactionHistory, - Manifest, - Module, - Network, - NetworkContractToken, - NetworkFees, - RpcRequest, - RpcResponse, - parseManifest, -} from '@avalabs/vm-module-types'; -import { ethErrors } from 'eth-rpc-errors'; - -import manifest from './pvm.manifest.json'; - -export class PVMModule implements Module { - getManifest(): Manifest | undefined { - const result = parseManifest(manifest); - return result.success ? result.data : undefined; - } - - getBalances(): Promise { - return Promise.resolve({}); - } - - getTransactionHistory( - _: GetTransactionHistory // eslint-disable-line @typescript-eslint/no-unused-vars - ) { - return Promise.resolve({ transactions: [], nextPageToken: '' }); - } - - getNetworkFee(): Promise { - return Promise.resolve({ - low: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - medium: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - high: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n }, - baseFee: 0n, - isFixedFee: false, - }); - } - - async getAddress() { - return {}; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getTokens(_: Network): Promise { - return Promise.resolve([]); - } - - async onRpcRequest(request: RpcRequest): Promise { - return { - error: ethErrors.rpc.methodNotSupported({ - data: `Method ${request.method} not supported`, - }) as any, // TODO: fix it - }; - } - getProvider; -} diff --git a/src/background/vmModules/models.ts b/src/background/vmModules/models.ts index d179c7052..7e30aaabe 100644 --- a/src/background/vmModules/models.ts +++ b/src/background/vmModules/models.ts @@ -1,6 +1,17 @@ +import { ApprovalParams, RpcRequest } from '@avalabs/vm-module-types'; + export enum VMModuleError { UnsupportedChain = 'unsupported-chain', UnsupportedMethod = 'unsupported-method', UnsupportedNamespace = 'unsupported-namespace', ModulesNotInitialized = 'modules-not-initialized', } + +type RpcRequestWithExtensionContext = RpcRequest & { + context?: RpcRequest['context'] & { + tabId?: number; + }; +}; +export interface ApprovalParamsWithContext extends ApprovalParams { + request: RpcRequestWithExtensionContext; +} diff --git a/src/components/common/approval/TransactionDetailItem.tsx b/src/components/common/approval/TransactionDetailItem.tsx new file mode 100644 index 000000000..5fca72dde --- /dev/null +++ b/src/components/common/approval/TransactionDetailItem.tsx @@ -0,0 +1,137 @@ +import { + Link, + LinkIcon, + Stack, + Tooltip, + Typography, +} from '@avalabs/core-k2-components'; +import { + type AddressItem, + type CurrencyItem, + type DetailItem, + type LinkItem, + type TextItem, + DetailItemType, +} from '@avalabs/vm-module-types'; +import { TokenUnit } from '@avalabs/core-utils-sdk'; + +import { AccountDetails } from '@src/pages/SignTransaction/components/ApprovalTxDetails'; +import { useSettingsContext } from '@src/contexts/SettingsProvider'; + +import { TxDetailsRow } from './TxDetailsRow'; +import { useBalancesContext } from '@src/contexts/BalancesProvider'; +import { runtime } from 'webextension-polyfill'; + +export const TransactionDetailItem = ({ item }: { item: DetailItem }) => { + if (typeof item === 'string') { + return ; + } + + switch (item.type) { + case DetailItemType.TEXT: + return ; + + case DetailItemType.ADDRESS: + return ; + + case DetailItemType.LINK: + return ; + + case DetailItemType.CURRENCY: + return ; + + default: + return null; + } +}; + +const PlainTextInfo = ({ item }: { item: string }) => ( + {item} +); + +const TextInfo = ({ item }: { item: TextItem }) => ( + + {item.value} + +); + +const LinkInfo = ({ item }: { item: LinkItem }) => { + const url = new URL(item.value.url); + + const isLinkToExtensionItself = url.hostname === runtime.id; + + // Do not link to ourselves + if (isLinkToExtensionItself) { + return null; + } + + return ( + + + + + + + + {url.hostname} + + + + + ); +}; + +const AddressInfo = ({ item }: { item: AddressItem }) => ( + +); + +const CurrencyInfo = ({ item }: { item: CurrencyItem }) => { + const { currencyFormatter } = useSettingsContext(); + const { getTokenPrice } = useBalancesContext(); + const token = new TokenUnit(item.value, item.maxDecimals, item.symbol); + const tokenPrice = getTokenPrice(item.symbol); + + return ( + + + + {token.toDisplay()} {token.getSymbol()} + + {tokenPrice ? ( + + {currencyFormatter( + tokenPrice * token.toDisplay({ asNumber: true }) + )} + + ) : null} + + + ); +}; diff --git a/src/contexts/AccountsProvider.tsx b/src/contexts/AccountsProvider.tsx index fc5c093bb..af7af1404 100644 --- a/src/contexts/AccountsProvider.tsx +++ b/src/contexts/AccountsProvider.tsx @@ -20,6 +20,7 @@ import { SelectAccountHandler } from '@src/background/services/accounts/handlers import { RenameAccountHandler } from '@src/background/services/accounts/handlers/renameAccount'; import { AddAccountHandler } from '@src/background/services/accounts/handlers/addAccount'; import { DeleteAccountHandler } from '@src/background/services/accounts/handlers/deleteAccounts'; +import getAllAddressesForAccount from '@src/utils/getAllAddressesForAccount'; const AccountsContext = createContext<{ accounts: Accounts; @@ -76,8 +77,10 @@ export function AccountsContextProvider({ children }: { children: any }) { const getAccount = useCallback( (address: string) => - allAccounts.find( - (acc) => acc.addressC.toLowerCase() === address.toLowerCase() + allAccounts.find((acc) => + getAllAddressesForAccount(acc) + .map((addy) => addy?.toLowerCase()) + .includes(address.toLowerCase()) ), [allAccounts] ); diff --git a/src/contexts/BalancesProvider.tsx b/src/contexts/BalancesProvider.tsx index cdb5da012..af3b73f41 100644 --- a/src/contexts/BalancesProvider.tsx +++ b/src/contexts/BalancesProvider.tsx @@ -5,6 +5,7 @@ import { NftTokenWithBalance, TokenType, TotalPriceChange, + getTokenPrice as getTokenPriceFromBalance, } from '@src/background/services/balances/models'; import { GetBalancesHandler } from '@src/background/services/balances/handlers/getBalances'; import { GetNftBalancesHandler } from '@src/background/services/balances/handlers/getNftBalances'; @@ -35,6 +36,8 @@ import { getSmallImageForNFT } from '@src/background/services/balances/nft/utils import { parseRawAttributesString } from '@src/utils/nfts/metadataParser'; import { TokensPriceShortData } from '@src/background/services/tokens/models'; import { calculateTotalBalance } from '@src/utils/calculateTotalBalance'; +import { Network } from '@src/background/services/network/models'; +import { getAddressForChain } from '@src/utils/getAddressForChain'; interface NftState { loading: boolean; @@ -80,6 +83,7 @@ const BalancesContext = createContext<{ pageToken?: NftPageTokens, callback?: () => void ) => void; + getTokenPrice(addressOrSymbol: string): number | undefined; updateBalanceOnAllNetworks: (accounts: Account[]) => Promise; registerSubscriber: () => void; unregisterSubscriber: () => void; @@ -94,6 +98,9 @@ const BalancesContext = createContext<{ }>({ tokens: { loading: true }, nfts: { loading: false }, + getTokenPrice() { + return undefined; + }, async refreshNftMetadata() {}, // eslint-disable-line @typescript-eslint/no-empty-function async updateBalanceOnAllNetworks() {}, // eslint-disable-line @typescript-eslint/no-empty-function registerSubscriber() {}, // eslint-disable-line @typescript-eslint/no-empty-function @@ -328,11 +335,38 @@ export function BalancesProvider({ children }: { children: any }) { [getAccount, favoriteNetworks, network, tokens.balances] ); + const getTokenPrice = useCallback( + (addressOrSymbol: string, lookupNetwork?: Network) => { + if (!activeAccount) { + return; + } + + const chainId = (lookupNetwork ?? network)?.chainId; + + if (!chainId) { + return; + } + + const addressForChain = getAddressForChain(chainId, activeAccount); + + if (!addressForChain) { + return; + } + + const token = + tokens.balances?.[chainId]?.[addressForChain]?.[addressOrSymbol]; + + return getTokenPriceFromBalance(token); + }, + [tokens.balances, activeAccount, network] + ); + return ( ( - (results) => { - return results.error ? Promise.reject(results.error) : results.result; + return activeEngine( + { + ...message, + tabId, + }, + scope ?? '', + { + ...context, + tabId, } - ); + ).then((results) => { + return results.error ? Promise.reject(results.error) : results.result; + }); }, [] ); diff --git a/src/contexts/NetworkProvider.tsx b/src/contexts/NetworkProvider.tsx index 314376781..bdf06bbc3 100644 --- a/src/contexts/NetworkProvider.tsx +++ b/src/contexts/NetworkProvider.tsx @@ -27,6 +27,7 @@ import { CustomNetworkPayload, Network, NetworkOverrides, + NetworkWithCaipId, } from '@src/background/services/network/models'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; import { isNetworkUpdatedEvent } from '@src/background/services/network/events/isNetworkUpdatedEvent'; @@ -36,22 +37,22 @@ import { getNetworkCaipId } from '@src/utils/caipConversion'; import { networkChanged } from './NetworkProvider/networkChanges'; const NetworkContext = createContext<{ - network?: Network | undefined; + network?: NetworkWithCaipId | undefined; setNetwork(network: Network): void; - networks: Network[]; + networks: NetworkWithCaipId[]; setDeveloperMode(status: boolean): void; saveCustomNetwork(network: CustomNetworkPayload): Promise; updateDefaultNetwork(network: NetworkOverrides): Promise; removeCustomNetwork(chainId: number): Promise; isDeveloperMode: boolean; - favoriteNetworks: Network[]; + favoriteNetworks: NetworkWithCaipId[]; addFavoriteNetwork(chainId: number): void; removeFavoriteNetwork(chainId: number): void; isFavoriteNetwork(chainId: number): boolean; - customNetworks: Network[]; + customNetworks: NetworkWithCaipId[]; isCustomNetwork(chainId: number): boolean; isChainIdExist(chainId: number): boolean; - getNetwork(chainId: number): Network | undefined; + getNetwork(chainId: number | string): NetworkWithCaipId | undefined; avalancheProvider?: JsonRpcBatchInternal; ethereumProvider?: JsonRpcBatchInternal; bitcoinProvider?: BitcoinProvider; @@ -63,8 +64,8 @@ const NetworkContext = createContext<{ * event. Thus updating all instances of the network provider and everything stays in sync. */ export function NetworkContextProvider({ children }: { children: any }) { - const [network, setNetwork] = useState(); - const [networks, setNetworks] = useState([]); + const [network, setNetwork] = useState(); + const [networks, setNetworks] = useState([]); const [customNetworks, setCustomNetworks] = useState([]); const [favoriteNetworks, setFavoriteNetworks] = useState([]); const { request, events } = useConnectionContext(); @@ -103,12 +104,11 @@ export function NetworkContextProvider({ children }: { children: any }) { ); const getNetwork = useCallback( - (lookupChainId: number) => { - if (isNaN(lookupChainId)) { - return; - } - - return networks.find(({ chainId }) => chainId === lookupChainId); + (lookupChainId: number | string) => { + return networks.find( + ({ chainId, caipId }) => + chainId === lookupChainId || caipId === lookupChainId + ); }, [networks] ); diff --git a/src/contexts/utils/connectionResponseMapper.ts b/src/contexts/utils/connectionResponseMapper.ts index 47d8a471b..eac2bfb56 100644 --- a/src/contexts/utils/connectionResponseMapper.ts +++ b/src/contexts/utils/connectionResponseMapper.ts @@ -65,7 +65,8 @@ export function requestEngine( }); return async ( request: PartialBy, 'params'>, - scope: string + scope: string, + context: { tabId?: number } & Record ) => { const id = `${request.method}-${Math.floor(Math.random() * 10000000)}`; @@ -82,6 +83,7 @@ export function requestEngine( ...request, }, }, + context, }; const response = connectionRequest(requestWithId); isDevelopment() && diff --git a/src/hooks/useApproveAction.ts b/src/hooks/useApproveAction.ts index e4910dee2..dbd426aff 100644 --- a/src/hooks/useApproveAction.ts +++ b/src/hooks/useApproveAction.ts @@ -11,6 +11,7 @@ import { useIsSpecificContextContainer, } from './useIsSpecificContextContainer'; import { useApprovalsContext } from '@src/contexts/ApprovalsProvider'; +import { getUpdatedSigningData } from '@src/utils/actions/getUpdatedActionData'; export function useApproveAction(actionId: string) { const { request } = useConnectionContext(); @@ -39,6 +40,10 @@ export function useApproveAction(actionId: string) { ...prevActionData.displayData, ...params.displayData, }, + signingData: getUpdatedSigningData( + prevActionData.signingData, + params.signingData + ), }; }); diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 1de3954fa..37ae43366 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -76,7 +76,6 @@ "App": "App", "Application": "Application", "Approve": "Approve", - "Approve BTC Send": "Approve BTC Send", "Approve Export": "Approve Export", "Approve Import": "Approve Import", "Approve Transaction": "Approve Transaction", @@ -646,7 +645,6 @@ "Receive assets by clicking the button below": "Receive assets by clicking the button below", "Received": "Received", "Recents": "Recents", - "Recipient": "Recipient", "Recipients": "Recipients", "Reconnect": "Reconnect", "Recovery Methods": "Recovery Methods", diff --git a/src/pages/ApproveAction/BitcoinSignTx.tsx b/src/pages/ApproveAction/BitcoinSignTx.tsx deleted file mode 100644 index fa67c5a64..000000000 --- a/src/pages/ApproveAction/BitcoinSignTx.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import { ActionStatus } from '@src/background/services/actions/models'; -import { useApproveAction } from '@src/hooks/useApproveAction'; -import { useGetRequestId } from '@src/hooks/useGetRequestId'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { LoadingOverlay } from '../../components/common/LoadingOverlay'; -import { satoshiToBtc } from '@avalabs/core-bridge-sdk'; -import { useTranslation } from 'react-i18next'; -import { bigIntToString } from '@avalabs/core-utils-sdk'; -import { BITCOIN_NETWORK, ChainId } from '@avalabs/core-chains-sdk'; -import { useNativeTokenPrice } from '@src/hooks/useTokenPrice'; -import { DisplayData_BitcoinSendTx } from '@src/background/services/wallet/handlers/models'; -import { LedgerAppType } from '@src/contexts/LedgerProvider'; -import { - Box, - Button, - Divider, - Scrollbars, - Skeleton, - Stack, - Typography, -} from '@avalabs/core-k2-components'; -import { useLedgerDisconnectedDialog } from '@src/pages/SignTransaction/hooks/useLedgerDisconnectedDialog'; -import { LedgerApprovalOverlay } from '@src/pages/SignTransaction/components/LedgerApprovalOverlay'; -import useIsUsingLedgerWallet from '@src/hooks/useIsUsingLedgerWallet'; -import useIsUsingKeystoneWallet from '@src/hooks/useIsUsingKeystoneWallet'; -import { KeystoneApprovalOverlay } from '../SignTransaction/components/KeystoneApprovalOverlay'; -import { - ApprovalSection, - ApprovalSectionBody, - ApprovalSectionHeader, -} from '@src/components/common/approval/ApprovalSection'; -import { TxDetailsRow } from '@src/components/common/approval/TxDetailsRow'; -import { - TransactionTokenCard, - TransactionTokenCardVariant, -} from '../SignTransaction/components/TransactionTokenCard'; -import { TokenIcon } from '@src/components/common/TokenIcon'; -import { TransactionToken } from '@src/background/services/wallet/handlers/eth_sendTransaction/models'; -import { AccountDetails } from '../SignTransaction/components/ApprovalTxDetails'; -import { CustomFees, GasFeeModifier } from '@src/components/common/CustomFees'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { SendErrorMessage } from '@src/utils/send/models'; -import { buildBtcTx } from '@src/utils/send/btcSendUtils'; -import { getSendErrorMessage } from '../Send/utils/sendErrorMessages'; -import { NetworkFee } from '@src/background/services/networkFee/models'; -import { getNetworkCaipId } from '@src/utils/caipConversion'; - -export function BitcoinSignTx() { - const { t } = useTranslation(); - const { network, networks, bitcoinProvider } = useNetworkContext(); - const { getNetworkFee } = useNetworkFeeContext(); - const requestId = useGetRequestId(); - const tokenPrice = useNativeTokenPrice(BITCOIN_NETWORK); - const { action, updateAction, cancelHandler } = - useApproveAction(requestId); - const isUsingLedgerWallet = useIsUsingLedgerWallet(); - const isUsingKeystoneWallet = useIsUsingKeystoneWallet(); - const [error, setError] = useState(); - const [isCalculatingFee, setIsCalculatingFee] = useState(false); - - const { displayData } = action ?? {}; - const [networkFee, setNetworkFee] = useState(); - - useEffect(() => { - let isMounted = true; - - if (!network) { - return; - } - // If the request comes from a dApp, a different network may be active, - // so we need to fetch current fees for Bitcoin specifically. - getNetworkFee(getNetworkCaipId(network)).then((fee) => { - if (isMounted) { - setNetworkFee(fee); - } - }); - - return () => { - isMounted = false; - }; - }, [getNetworkFee, network]); - - const btcNetwork = useMemo(() => { - const networkID = network?.isTestnet - ? ChainId.BITCOIN_TESTNET - : ChainId.BITCOIN; - - const foundNetwork = networks.filter( - (networkItem) => networkItem.chainId === networkID - ); - - return foundNetwork[0]; - }, [network, networks]); - - const btcAmountDisplay = useMemo( - () => (displayData ? satoshiToBtc(displayData.amount).toFixed(8) : '-'), - [displayData] - ); - - const sendFeeDisplay = useMemo(() => { - return displayData?.sendFee - ? satoshiToBtc(displayData.sendFee).toFixed(8) - : '-'; - }, [displayData]); - - const renderDeviceApproval = () => { - if (action?.status !== ActionStatus.SUBMITTING) { - return null; - } - - if (isUsingLedgerWallet) { - return ( - - ); - } - - if (isUsingKeystoneWallet) { - return ; - } - }; - - const [gasFeeModifier, setGasFeeModifier] = useState( - GasFeeModifier.NORMAL - ); - const [customFeeRate, setCustomFeeRate] = useState(displayData?.feeRate ?? 0); - - const setCustomFee = useCallback( - (values: { maxFeePerGas: bigint; feeType: GasFeeModifier }) => { - const newFeeRate = Number(values.maxFeePerGas); - - setCustomFeeRate(newFeeRate); - setGasFeeModifier(values.feeType); - }, - [] - ); - - useEffect( - () => { - if (!customFeeRate || !action || !displayData || !bitcoinProvider) { - return; - } - - let isMounted = true; - - setIsCalculatingFee(true); - buildBtcTx(displayData.from, bitcoinProvider, { - amount: displayData.amount, - address: displayData.address, - token: displayData.balance, - feeRate: customFeeRate, - }) - .then((tx) => { - if (!isMounted) { - return; - } - - if (displayData.amount > 0 && !tx.psbt) { - setError(SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE); - } else if (tx.psbt) { - setError(undefined); - } - - const newData: DisplayData_BitcoinSendTx = { - balance: displayData.balance, - from: displayData.from, - amount: displayData.amount, - address: displayData.address, - feeRate: customFeeRate, - sendFee: tx.fee, - }; - - // Only update if the action wasn't already submitted/cancelled - if (action.status === ActionStatus.PENDING) { - updateAction({ - id: action.actionId, - status: ActionStatus.PENDING, - displayData: newData, - }); - } - }) - .catch((err) => { - console.error(err); - setError(SendErrorMessage.UNKNOWN_ERROR); - }) - .finally(() => { - setIsCalculatingFee(false); - }); - - return () => { - isMounted = false; - }; - }, - // Keeping displayData out of here, as the only way it updates is through this UI, - // and having it here would result in render loops. - // eslint-disable-next-line - [ - action?.actionId, - action?.status, - error, - customFeeRate, - updateAction, - bitcoinProvider, - ] - ); - - const handleRejection = useCallback(() => { - cancelHandler(); - }, [cancelHandler]); - - const signTx = useCallback(() => { - updateAction( - { - status: ActionStatus.SUBMITTING, - id: requestId, - }, - isUsingLedgerWallet || isUsingKeystoneWallet - ); - }, [requestId, updateAction, isUsingLedgerWallet, isUsingKeystoneWallet]); - - // Make the user switch to the correct app or close the window - useLedgerDisconnectedDialog(handleRejection, LedgerAppType.BITCOIN); - - const transactionToken: TransactionToken | null = useMemo(() => { - if (!displayData?.balance) { - return null; - } - - const { decimals, symbol, name, logoUri } = displayData.balance; - - return { - address: '', - decimals, - symbol, - name, - logoUri, - amount: BigInt(displayData.amount), - usdValue: - Number( - bigIntToString(BigInt(displayData.amount.toString()), decimals) - ) * tokenPrice, - usdPrice: tokenPrice, - }; - }, [displayData?.balance, displayData?.amount, tokenPrice]); - - if (!action || !displayData) { - return ; - } - - return ( - <> - - {/* Header */} - - - {displayData.displayOptions?.customApprovalScreenTitle || - t('Approve BTC Send')} - - - {/* Transaction Details */} - - - - - - - - - - - - - - - - - - {t('Send')} - - {transactionToken && ( - <> - - - - - - )} - - - - - {networkFee ? ( - - ) : ( - - - - - )} - {error && ( - - {getSendErrorMessage(error)} - - )} - - - {/* Action Buttons */} - - - - - - {renderDeviceApproval()} - - ); -} diff --git a/src/pages/ApproveAction/GenericApprovalScreen.tsx b/src/pages/ApproveAction/GenericApprovalScreen.tsx new file mode 100644 index 000000000..4d401b63d --- /dev/null +++ b/src/pages/ApproveAction/GenericApprovalScreen.tsx @@ -0,0 +1,182 @@ +import { ActionStatus } from '@src/background/services/actions/models'; +import { useApproveAction } from '@src/hooks/useApproveAction'; +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 { + Box, + Button, + Scrollbars, + Stack, + Typography, +} from '@avalabs/core-k2-components'; +import { useLedgerDisconnectedDialog } from '@src/pages/SignTransaction/hooks/useLedgerDisconnectedDialog'; +import useIsUsingLedgerWallet from '@src/hooks/useIsUsingLedgerWallet'; +import useIsUsingKeystoneWallet from '@src/hooks/useIsUsingKeystoneWallet'; +import { + ApprovalSection, + ApprovalSectionBody, + ApprovalSectionHeader, +} from '@src/components/common/approval/ApprovalSection'; +import { getSendErrorMessage } from '../Send/utils/sendErrorMessages'; +import { TransactionDetailItem } from '@src/components/common/approval/TransactionDetailItem'; +import { useFeeCustomizer } from './hooks/useFeeCustomizer'; +import { DeviceApproval } from './components/DeviceApproval'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; + +export function GenericApprovalScreen() { + const { t } = useTranslation(); + const requestId = useGetRequestId(); + const { action, updateAction, cancelHandler } = + useApproveAction(requestId); + const isUsingLedgerWallet = useIsUsingLedgerWallet(); + const isUsingKeystoneWallet = useIsUsingKeystoneWallet(); + const [network, setNetwork] = useState(); + const { getNetwork } = useNetworkContext(); + const { isCalculatingFee, feeError, renderFeeWidget } = useFeeCustomizer({ + actionId: requestId, + network, + }); + + const { displayData, context } = action ?? {}; + + useEffect(() => { + if (!action?.scope) { + return; + } + + setNetwork(getNetwork(action.scope)); + }, [getNetwork, action?.scope]); + + const handleRejection = useCallback(() => { + cancelHandler(); + }, [cancelHandler]); + + const signTx = useCallback(() => { + updateAction( + { + status: ActionStatus.SUBMITTING, + id: requestId, + }, + isUsingLedgerWallet || isUsingKeystoneWallet + ); + }, [requestId, updateAction, isUsingLedgerWallet, isUsingKeystoneWallet]); + + // Make the user switch to the correct app or close the window + useLedgerDisconnectedDialog(handleRejection, LedgerAppType.BITCOIN); + + if (!action || !displayData) { + return ; + } + + return ( + + + {/* Header */} + + + {context?.customApprovalScreenTitle || displayData.title} + + + + + + + {displayData.details.map((section, sectionIndex) => ( + + {section.title && ( + + )} + + {section.items.map((item, index) => ( + + ))} + + + ))} + + {displayData.networkFeeSelector && renderFeeWidget()} + + + {feeError && ( + + {getSendErrorMessage(feeError)} + + )} + + {/* Action Buttons */} + + + + + + + ); +} diff --git a/src/pages/ApproveAction/components/DeviceApproval.tsx b/src/pages/ApproveAction/components/DeviceApproval.tsx new file mode 100644 index 000000000..586ba7f12 --- /dev/null +++ b/src/pages/ApproveAction/components/DeviceApproval.tsx @@ -0,0 +1,64 @@ +import { satoshiToBtc } from '@avalabs/core-bridge-sdk'; +import { RpcMethod, SigningData } from '@avalabs/vm-module-types'; + +import { Action, ActionStatus } from '@src/background/services/actions/models'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; +import useIsUsingKeystoneWallet from '@src/hooks/useIsUsingKeystoneWallet'; +import useIsUsingLedgerWallet from '@src/hooks/useIsUsingLedgerWallet'; + +import { LedgerApprovalOverlay } from '@src/pages/SignTransaction/components/LedgerApprovalOverlay'; +import { KeystoneApprovalOverlay } from '@src/pages/SignTransaction/components/KeystoneApprovalOverlay'; + +const getTxInfoForLedger = ( + signingData: SigningData, + network: NetworkWithCaipId +) => { + if (signingData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { + return { + amount: satoshiToBtc(signingData.data.amount).toFixed(8), + fee: satoshiToBtc(signingData.data.fee).toFixed(8), + to: signingData.data.to, + symbol: signingData.data.balance.symbol, + feeSymbol: network.networkToken.symbol, + }; + } + + throw new Error( + `Getting tx info for ledger not implemented yet for ${signingData?.type}` + ); +}; + +export const DeviceApproval = ({ + action, + network, + handleRejection, +}: { + action: Action; + network?: NetworkWithCaipId; + handleRejection: () => void; +}) => { + const isUsingLedgerWallet = useIsUsingLedgerWallet(); + const isUsingKeystoneWallet = useIsUsingKeystoneWallet(); + + if (!action || !network || !action.signingData) { + return null; + } + + if (action.status !== ActionStatus.SUBMITTING) { + return null; + } + + if (isUsingLedgerWallet) { + return ( + + ); + } + + if (isUsingKeystoneWallet) { + return ; + } + + return null; +}; diff --git a/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx b/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx new file mode 100644 index 000000000..ed0e01f04 --- /dev/null +++ b/src/pages/ApproveAction/hooks/useFeeCustomizer.tsx @@ -0,0 +1,228 @@ +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 { 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'; + +const getInitialFeeRate = (data?: SigningData): bigint => { + if (!data) { + return 0n; + } + + if (data.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { + return BigInt(data.data.feeRate); + } + + return 0n; +}; + +export const useFeeCustomizer = ({ + actionId, + network, +}: { + actionId: string; + network?: NetworkWithCaipId; +}) => { + const { action, updateAction } = useApproveAction(actionId); + const [networkFee, setNetworkFee] = useState(); + + const [feeError, setFeeError] = useState(); + const { getNetworkFee } = useNetworkFeeContext(); + + const [isCalculatingFee, setIsCalculatingFee] = useState(false); + const [gasFeeModifier, setGasFeeModifier] = useState( + GasFeeModifier.NORMAL + ); + + const signingData = useMemo(() => { + if (action?.signingData?.type === RpcMethod.BITCOIN_SEND_TRANSACTION) { + return action.signingData; + } + }, [action]); + + const [maxFeePerGas, setMaxFeePerGas] = useState( + getInitialFeeRate(signingData) + ); + + const setCustomFee = useCallback( + (values: { maxFeePerGas: bigint; feeType: GasFeeModifier }) => { + setMaxFeePerGas(values.maxFeePerGas); + setGasFeeModifier(values.feeType); + }, + [] + ); + + useEffect(() => { + let isMounted = true; + + if (!network) { + return; + } + // If the request comes from a dApp, a different network may be active, + // so we need to fetch current fees for Bitcoin specifically. + getNetworkFee(network.caipId).then((fee) => { + if (isMounted) { + setNetworkFee(fee); + } + }); + + return () => { + isMounted = false; + }; + }, [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) { + 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, + }); + }) + .catch((err) => { + console.error(err); + setFeeError(err); + }) + .finally(() => { + 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}`); + } + }, []); + + const renderFeeWidget = useCallback(() => { + if (!networkFee || !signingData) { + return ( + + + + + ); + } + + const { feeRate, limit } = getFeeInfo(signingData); + + return ( + + ); + }, [ + gasFeeModifier, + getFeeInfo, + network, + networkFee, + setCustomFee, + signingData, + ]); + + return { + isCalculatingFee, + renderFeeWidget, + feeError, + }; +}; diff --git a/src/pages/Bridge/hooks/useBtcBridge.test.ts b/src/pages/Bridge/hooks/useBtcBridge.test.ts index ca7ba3691..7d4f5a641 100644 --- a/src/pages/Bridge/hooks/useBtcBridge.test.ts +++ b/src/pages/Bridge/hooks/useBtcBridge.test.ts @@ -206,15 +206,20 @@ describe('src/pages/Bridge/hooks/useBtcBridge', () => { await act(async () => { const hash = await hook.current.transfer(); - expect(requestFn).toHaveBeenCalledWith({ - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: [ - 'bridge-btc-address', - String(btcToSatoshi(amount)), - Number(highFee), - { customApprovalScreenTitle: 'Confirm Bridge' }, - ], - }); + expect(requestFn).toHaveBeenCalledWith( + { + method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, + params: { + from: 'user-btc-address', + to: 'bridge-btc-address', + amount: btcToSatoshi(amount), + feeRate: Number(highFee), + }, + }, + { + customApprovalScreenTitle: 'Confirm Bridge', + } + ); expect(hash).toEqual(fakeHash); }); diff --git a/src/pages/Bridge/hooks/useBtcBridge.ts b/src/pages/Bridge/hooks/useBtcBridge.ts index ebfd5a276..c95d42c8f 100644 --- a/src/pages/Bridge/hooks/useBtcBridge.ts +++ b/src/pages/Bridge/hooks/useBtcBridge.ts @@ -9,6 +9,8 @@ import { useBridgeSDK, } from '@avalabs/core-bridge-sdk'; import { ChainId } from '@avalabs/core-chains-sdk'; +import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; +import { RpcMethod, TokenWithBalanceBTC } from '@avalabs/vm-module-types'; import { BitcoinInputUTXOWithOptionalScript, getMaxTransferAmount, @@ -26,20 +28,16 @@ import { TransactionPriority } from '@src/background/services/networkFee/models' import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; import { getBtcInputUtxos } from '@src/utils/send/btcSendUtils'; -import { BitcoinSendTransactionHandler } from '@src/background/services/wallet/handlers/bitcoin_sendTransaction'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { useTranslation } from 'react-i18next'; -import { TokenWithBalanceBTC } from '@avalabs/vm-module-types'; import { normalizeBalance } from '@src/utils/normalizeBalance'; /** * Hook for Bitcoin to Avalanche transactions */ export function useBtcBridge(amountInBtc: Big): BridgeAdapter { - const { t } = useTranslation(); const { setTransactionDetails, currentBlockchain } = useBridgeSDK(); const isBitcoinBridge = currentBlockchain === Blockchain.BITCOIN; - + const { t } = useTranslation(); const { request } = useConnectionContext(); const { bitcoinProvider, isDeveloperMode } = useNetworkContext(); const { networkFee: currentFeeInfo } = useNetworkFeeContext(); @@ -189,19 +187,18 @@ export function useBtcBridge(amountInBtc: Big): BridgeAdapter { } const symbol = 'BTC'; - const hash = await request< - BitcoinSendTransactionHandler, - DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - string - >({ - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: [ - config.criticalBitcoin.walletAddresses.btc, - String(btcToSatoshi(amountInBtc)), - feeRate, - { customApprovalScreenTitle: t('Confirm Bridge') }, - ], - }); + const hash = await request( + { + method: RpcMethod.BITCOIN_SEND_TRANSACTION, + params: { + from: activeAccount.addressBTC, + to: config.criticalBitcoin.walletAddresses.btc, + feeRate, + amount: btcToSatoshi(amountInBtc), + }, + }, + { customApprovalScreenTitle: t('Confirm Bridge') } + ); setTransactionDetails({ tokenSymbol: symbol, diff --git a/src/pages/Send/hooks/useSend/useBTCSend.ts b/src/pages/Send/hooks/useSend/useBTCSend.ts index 5d6889ed2..5e2296902 100644 --- a/src/pages/Send/hooks/useSend/useBTCSend.ts +++ b/src/pages/Send/hooks/useSend/useBTCSend.ts @@ -4,15 +4,15 @@ import { BitcoinInputUTXO, getMaxTransferAmount, } from '@avalabs/core-wallets-sdk'; +import { RpcMethod } from '@avalabs/vm-module-types'; +import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; import { SendErrorMessage } from '@src/utils/send/models'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { useConnectionContext } from '@src/contexts/ConnectionProvider'; import { getBtcInputUtxos, validateBtcSend, } from '@src/utils/send/btcSendUtils'; -import type { BitcoinSendTransactionHandler } from '@src/background/services/wallet/handlers/bitcoin_sendTransaction'; import { SendAdapterBTC } from './models'; import { BaseSendOptions } from '../../models'; @@ -100,19 +100,20 @@ export const useBtcSend: SendAdapterBTC = ({ const amountBN = stringToBN(amount || '0', nativeToken.decimals); const amountInSatoshis = amountBN.toNumber(); - return await request< - BitcoinSendTransactionHandler, - DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - string - >({ - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: [address, String(amountInSatoshis), Number(maxFee)], + return await request({ + method: RpcMethod.BITCOIN_SEND_TRANSACTION, + params: { + from, + to: address, + amount: amountInSatoshis, + feeRate: Number(maxFee), + }, }); } finally { setIsSending(false); } }, - [maxFee, nativeToken.decimals, request] + [from, maxFee, nativeToken.decimals, request] ); return { diff --git a/src/popup/ApprovalRoutes.tsx b/src/popup/ApprovalRoutes.tsx index f9f776fec..815beb391 100644 --- a/src/popup/ApprovalRoutes.tsx +++ b/src/popup/ApprovalRoutes.tsx @@ -88,9 +88,9 @@ const AvalancheSignTx = lazy(() => { })); }); -const BitcoinSignTx = lazy(() => { - return import('../pages/ApproveAction/BitcoinSignTx').then((m) => ({ - default: m.BitcoinSignTx, +const GenericApprovalScreen = lazy(() => { + return import('../pages/ApproveAction/GenericApprovalScreen').then((m) => ({ + default: m.GenericApprovalScreen, })); }); @@ -121,6 +121,11 @@ export const ApprovalRoutes = (props: SwitchProps) => ( + + + + + @@ -160,9 +165,6 @@ export const ApprovalRoutes = (props: SwitchProps) => ( - - - diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 279bf7705..eef7c7825 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -13,6 +13,7 @@ declare global { const EVM_PROVIDER_INFO_ICON: `data:image/svg+xml;base64,${string}`; const EVM_PROVIDER_INFO_DESCRIPTION: string; const EVM_PROVIDER_INFO_RDNS: string; + const CORE_EXTENSION_VERSION: string; } export {}; diff --git a/src/utils/actions/getUpdatedActionData.ts b/src/utils/actions/getUpdatedActionData.ts new file mode 100644 index 000000000..2596d5111 --- /dev/null +++ b/src/utils/actions/getUpdatedActionData.ts @@ -0,0 +1,17 @@ +import { SigningData } from '@avalabs/vm-module-types'; + +export const getUpdatedSigningData = ( + oldSigningData?: SigningData, + newSigningData?: SigningData +): SigningData | undefined => { + if (!oldSigningData) { + return newSigningData; + } else if (!newSigningData) { + return oldSigningData; + } + + return { + ...oldSigningData, + ...newSigningData, + }; +}; diff --git a/webpack.inpage.js b/webpack.inpage.js index 0fd9e0dfb..2a195cac4 100644 --- a/webpack.inpage.js +++ b/webpack.inpage.js @@ -28,8 +28,10 @@ const prodEvmProviderConfig = { }; module.exports = (env, argv) => { - const evmProviderConfig = - argv.mode === 'production' ? prodEvmProviderConfig : devEvmProviderConfig; + const isDevBuild = argv.mode !== 'production'; + const evmProviderConfig = isDevBuild + ? devEvmProviderConfig + : prodEvmProviderConfig; return { mode: 'development', devtool: 'hidden-source-map', @@ -75,6 +77,10 @@ module.exports = (env, argv) => { new NodePolyfillPlugin(), new DefinePlugin({ ...evmProviderConfig, + // For non-dev builds, it's replaced by actual version number later in the release process + CORE_EXTENSION_VERSION: isDevBuild + ? '"0.0.0"' + : '"CORE_EXTENSION_VERSION"', }), ], }; diff --git a/yarn.lock b/yarn.lock index daafb44f0..78c8942a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,15 +40,15 @@ "@scure/base" "1.1.5" micro-eth-signer "0.7.2" -"@avalabs/bitcoin-module@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@avalabs/bitcoin-module/-/bitcoin-module-0.3.0.tgz#3ecd74fad7e645bceecfa643559fe9cf87d83558" - integrity sha512-q6YPSEcdNAE1F/cmflYIsAmgOrYTEPg+eG2hmO9epwpdmfHNr4C4V5CeWASk/IDVy3pbr+gFX57rZ68F2xMeqw== - dependencies: - "@avalabs/core-coingecko-sdk" "3.1.0-alpha.1" - "@avalabs/core-utils-sdk" "3.1.0-alpha.1" - "@avalabs/core-wallets-sdk" "3.1.0-alpha.1" - "@avalabs/vm-module-types" "0.3.0" +"@avalabs/bitcoin-module@0.0.0-feat-add-core-version-to-evm-p-20240903160850": + version "0.0.0-feat-add-core-version-to-evm-p-20240903160850" + resolved "https://registry.yarnpkg.com/@avalabs/bitcoin-module/-/bitcoin-module-0.0.0-feat-add-core-version-to-evm-p-20240903160850.tgz#52e6cce7a572583c94a65a5c9f51ded5ac19d083" + integrity sha512-SpcQEAZxGEUSC7HYYg2jt+lPNqAG5YBp4UepkO37lpH5MFufIkrPXEsd5yTPgyZpI+yGFVDspKXOkoWKeMmD4Q== + dependencies: + "@avalabs/core-coingecko-sdk" "3.1.0-alpha.5" + "@avalabs/core-utils-sdk" "3.1.0-alpha.5" + "@avalabs/core-wallets-sdk" "3.1.0-alpha.5" + "@avalabs/vm-module-types" "0.0.0-feat-add-core-version-to-evm-p-20240903160850" "@metamask/rpc-errors" "6.3.0" "@zodios/core" "10.9.6" big.js "6.2.1" @@ -74,13 +74,6 @@ "@avalabs/core-utils-sdk" "3.1.0-alpha.4" "@avalabs/core-wallets-sdk" "3.1.0-alpha.4" -"@avalabs/core-chains-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/core-chains-sdk/-/core-chains-sdk-3.1.0-alpha.1.tgz#5fc89d097ca6644e3e79cc5abb93d49731f84c0e" - integrity sha512-JYNmlrqNMoEL/Tgw0MVqYMHTbiq+XXRyHv4QDvuG0al7v1IkHXQ+TAh5rGlSSAXY+39/1SQbVLMRIYpMTUo6Eg== - dependencies: - "@avalabs/core-utils-sdk" "3.1.0-alpha.1" - "@avalabs/core-chains-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" resolved "https://registry.yarnpkg.com/@avalabs/core-chains-sdk/-/core-chains-sdk-3.1.0-alpha.4.tgz#72b71681e8780034585f796aa1a65b0ebfbaaf1d" @@ -88,12 +81,12 @@ dependencies: "@avalabs/core-utils-sdk" "3.1.0-alpha.4" -"@avalabs/core-coingecko-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/core-coingecko-sdk/-/core-coingecko-sdk-3.1.0-alpha.1.tgz#43bc6d8b2d4d0cd8aabc520ff2ca52e565de64d8" - integrity sha512-zgDEm8Zaw3Ka9fF8lHSxSWXepPXzbGqy4fWXHwSTXs5n9Sy5bd03n5ArlTASPhXG4srMXFi2lVt2XdRg6YBfGw== +"@avalabs/core-chains-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/core-chains-sdk/-/core-chains-sdk-3.1.0-alpha.5.tgz#f8b1b00af81577d5c4ade1e8eda56238edb7f7b9" + integrity sha512-oaXbhjmjNy4pEDDX+cLn/e5GXK08V0GjUxdrVefEecRXabow13zaN+NQHPcqQOwKpbOAg8WPNlVKK5hCMmpGOA== dependencies: - "@avalabs/core-utils-sdk" "3.1.0-alpha.1" + "@avalabs/core-utils-sdk" "3.1.0-alpha.5" "@avalabs/core-coingecko-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" @@ -102,6 +95,13 @@ dependencies: "@avalabs/core-utils-sdk" "3.1.0-alpha.4" +"@avalabs/core-coingecko-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/core-coingecko-sdk/-/core-coingecko-sdk-3.1.0-alpha.5.tgz#c6609ee44f60f5372174338ea9c26219e63ed2e9" + integrity sha512-m4eoUSZd/d6qqUtHQiUcRpFi1kWHu5ftbJqBHxddleBt7QzuYBSXEuygzna70Vqo1u8hKK+b6gbtGZQJApOR1Q== + dependencies: + "@avalabs/core-utils-sdk" "3.1.0-alpha.5" + "@avalabs/core-covalent-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" resolved "https://registry.yarnpkg.com/@avalabs/core-covalent-sdk/-/core-covalent-sdk-3.1.0-alpha.4.tgz#5f8b78363edc4211973defe11f812e51f7d800e3" @@ -109,13 +109,6 @@ dependencies: "@avalabs/core-utils-sdk" "3.1.0-alpha.4" -"@avalabs/core-etherscan-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/core-etherscan-sdk/-/core-etherscan-sdk-3.1.0-alpha.1.tgz#df84e4d71863538e008216133f7a57e71ed8b690" - integrity sha512-FfCLwNNck0R0C+73YgT37mOzeOriHeWl2WAvDEpt3etgtkBayxE7aF7DRLajGhvFt5SQ1uiXFA+JGW63uTyZlQ== - dependencies: - "@avalabs/core-utils-sdk" "3.1.0-alpha.1" - "@avalabs/core-etherscan-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" resolved "https://registry.yarnpkg.com/@avalabs/core-etherscan-sdk/-/core-etherscan-sdk-3.1.0-alpha.4.tgz#e9a44a00e45205678d35c0c00498066f9c1fb6e1" @@ -123,6 +116,13 @@ dependencies: "@avalabs/core-utils-sdk" "3.1.0-alpha.4" +"@avalabs/core-etherscan-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/core-etherscan-sdk/-/core-etherscan-sdk-3.1.0-alpha.5.tgz#da2d32fd5cb51106a1c559b3e847e634cf2156b8" + integrity sha512-frJH1WNsEsCB3D1jL88t0ImY6ueWCPpFu8XGKwGjKlzOv1uTLAIYSHKDar1tv/cSDxaKpdjTal3VNW79IPjZCQ== + dependencies: + "@avalabs/core-utils-sdk" "3.1.0-alpha.5" + "@avalabs/core-k2-components@4.18.0-alpha.47": version "4.18.0-alpha.47" resolved "https://registry.yarnpkg.com/@avalabs/core-k2-components/-/core-k2-components-4.18.0-alpha.47.tgz#94d588cf109350fe57d246dbf36bc127a1fc0584" @@ -161,32 +161,32 @@ "@avalabs/core-coingecko-sdk" "3.1.0-alpha.4" "@avalabs/core-utils-sdk" "3.1.0-alpha.4" -"@avalabs/core-utils-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/core-utils-sdk/-/core-utils-sdk-3.1.0-alpha.1.tgz#6c154463561c39d50274073d12502e58638acf97" - integrity sha512-amqIGN+4P5qqIMKze1Jq5pa3GQGMzAvlX4wlNQ7t510A3kMG9FDOZxDNl9PsXk+Ah3843GnFK0o+FbsQcOYVYw== +"@avalabs/core-utils-sdk@3.1.0-alpha.4": + version "3.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@avalabs/core-utils-sdk/-/core-utils-sdk-3.1.0-alpha.4.tgz#360c73f6b9ebb9821c22e75709b8b84701258d8b" + integrity sha512-Is0kF1likZwOLyoS1zBxI++6ItGELxftahMGWXOMj7X3p2jg6QgxHos4RHhgCcjI2j9oxpnMADnOnI6zRr3rKA== dependencies: "@avalabs/avalanchejs" "4.0.5" "@hpke/core" "1.2.5" is-ipfs "6.0.2" -"@avalabs/core-utils-sdk@3.1.0-alpha.4": - version "3.1.0-alpha.4" - resolved "https://registry.yarnpkg.com/@avalabs/core-utils-sdk/-/core-utils-sdk-3.1.0-alpha.4.tgz#360c73f6b9ebb9821c22e75709b8b84701258d8b" - integrity sha512-Is0kF1likZwOLyoS1zBxI++6ItGELxftahMGWXOMj7X3p2jg6QgxHos4RHhgCcjI2j9oxpnMADnOnI6zRr3rKA== +"@avalabs/core-utils-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/core-utils-sdk/-/core-utils-sdk-3.1.0-alpha.5.tgz#f4ce19298cdc264a99503f6889218cdd8d1b4821" + integrity sha512-cylqLT9hDmUhZbPkyBO4mN8ZHZeL2zxrjZ7+yfydot++IP79RNo6nDLU7mkrZgG57Ui1otDJsFAUTq8w8Ml0Gg== dependencies: "@avalabs/avalanchejs" "4.0.5" "@hpke/core" "1.2.5" is-ipfs "6.0.2" -"@avalabs/core-wallets-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/core-wallets-sdk/-/core-wallets-sdk-3.1.0-alpha.1.tgz#015c0a8738e5ac4a1517d2e0bfed17de41f2b257" - integrity sha512-sto78LJoSQQzTJW7JYnIqZkBI7Rftm8m7zivcDJbQbEMFzk9c1+qROvkE8KUgJxHntcVeEzg64e/e0Ar3g+6xQ== +"@avalabs/core-wallets-sdk@3.1.0-alpha.4": + version "3.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@avalabs/core-wallets-sdk/-/core-wallets-sdk-3.1.0-alpha.4.tgz#53b8d76d9b0b5be4873dc8174724a64325b8d6bd" + integrity sha512-hISEAvIPQBHS2o+RIL0kY+eQ/BBhzTFsSjze6HOeH1GBYmyHyA5P4ZJ7pc7+wtRU5kjHsGWx1sDySklbUYZDuQ== dependencies: "@avalabs/avalanchejs" "4.0.5" - "@avalabs/core-chains-sdk" "3.1.0-alpha.1" - "@avalabs/glacier-sdk" "3.1.0-alpha.1" + "@avalabs/core-chains-sdk" "3.1.0-alpha.4" + "@avalabs/glacier-sdk" "3.1.0-alpha.4" "@avalabs/hw-app-avalanche" "0.14.1" "@ledgerhq/hw-app-btc" "10.2.4" "@ledgerhq/hw-app-eth" "6.36.1" @@ -203,14 +203,14 @@ ledger-bitcoin "0.2.3" xss "1.0.14" -"@avalabs/core-wallets-sdk@3.1.0-alpha.4": - version "3.1.0-alpha.4" - resolved "https://registry.yarnpkg.com/@avalabs/core-wallets-sdk/-/core-wallets-sdk-3.1.0-alpha.4.tgz#53b8d76d9b0b5be4873dc8174724a64325b8d6bd" - integrity sha512-hISEAvIPQBHS2o+RIL0kY+eQ/BBhzTFsSjze6HOeH1GBYmyHyA5P4ZJ7pc7+wtRU5kjHsGWx1sDySklbUYZDuQ== +"@avalabs/core-wallets-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/core-wallets-sdk/-/core-wallets-sdk-3.1.0-alpha.5.tgz#d6456fdd0b3874011e79501ef5a5650f4be6124d" + integrity sha512-MIBwJhAf0n9/8gaXZ1ivUtSIV5epbN53/IiATFNZ3PB/TSc062t4NSrExgYX+SWPhZ2BUO0t0J/+lswpv8jAsQ== dependencies: "@avalabs/avalanchejs" "4.0.5" - "@avalabs/core-chains-sdk" "3.1.0-alpha.4" - "@avalabs/glacier-sdk" "3.1.0-alpha.4" + "@avalabs/core-chains-sdk" "3.1.0-alpha.5" + "@avalabs/glacier-sdk" "3.1.0-alpha.5" "@avalabs/hw-app-avalanche" "0.14.1" "@ledgerhq/hw-app-btc" "10.2.4" "@ledgerhq/hw-app-eth" "6.36.1" @@ -227,18 +227,18 @@ ledger-bitcoin "0.2.3" xss "1.0.14" -"@avalabs/evm-module@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@avalabs/evm-module/-/evm-module-0.3.0.tgz#c78debb95fae9e5d5c6e6948ecbe6cae487e6dab" - integrity sha512-JhLvzocCnm7RDgft3ujg0qqZnJADGPQGEpbn1L2YcPvCt+ZLpKpaSQpEpByp8NuBcoUIntGS03QJ0ouVN0Nr+Q== - dependencies: - "@avalabs/core-coingecko-sdk" "3.1.0-alpha.1" - "@avalabs/core-etherscan-sdk" "3.1.0-alpha.1" - "@avalabs/core-utils-sdk" "3.1.0-alpha.1" - "@avalabs/core-wallets-sdk" "3.1.0-alpha.1" - "@avalabs/glacier-sdk" "3.1.0-alpha.1" - "@avalabs/types" "3.1.0-alpha.1" - "@avalabs/vm-module-types" "0.3.0" +"@avalabs/evm-module@0.0.0-feat-add-core-version-to-evm-p-20240903160850": + version "0.0.0-feat-add-core-version-to-evm-p-20240903160850" + resolved "https://registry.yarnpkg.com/@avalabs/evm-module/-/evm-module-0.0.0-feat-add-core-version-to-evm-p-20240903160850.tgz#869d4a0f2d2d9a25f99170ba6d1c43e9d8a3da20" + integrity sha512-o23rZtJ3ddUQHCmT7HlgNW3AzJSFSQR9ybynnMjAmK2kp4agzb4YEztvAHJha8OMJNO+S7wjdyGBct8NgKJKmw== + dependencies: + "@avalabs/core-coingecko-sdk" "3.1.0-alpha.5" + "@avalabs/core-etherscan-sdk" "3.1.0-alpha.5" + "@avalabs/core-utils-sdk" "3.1.0-alpha.5" + "@avalabs/core-wallets-sdk" "3.1.0-alpha.5" + "@avalabs/glacier-sdk" "3.1.0-alpha.5" + "@avalabs/types" "3.1.0-alpha.5" + "@avalabs/vm-module-types" "0.0.0-feat-add-core-version-to-evm-p-20240903160850" "@blockaid/client" "0.11.0" "@metamask/rpc-errors" "6.3.0" "@zodios/core" "10.9.6" @@ -246,16 +246,16 @@ lodash.startcase "4.4.0" zod "3.23.8" -"@avalabs/glacier-sdk@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/glacier-sdk/-/glacier-sdk-3.1.0-alpha.1.tgz#e627bc8564ad2c4114a6cfc8eb04390143fe9a03" - integrity sha512-INVlu7ClJDzrIQglABGhRO5nYs+sOLL/zSEHuEcf81TMS91NHReomqH/6V5uDLyZ06x1fvWXUFRl8le2kbpIbQ== - "@avalabs/glacier-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" resolved "https://registry.yarnpkg.com/@avalabs/glacier-sdk/-/glacier-sdk-3.1.0-alpha.4.tgz#6dfcbe965f085ba752c9d99140ea8d02bfdeecfa" integrity sha512-lS1vSl/cogouZTs1QEvxjU6ikH6MR0OLbVSqWEypYn53BO794U+6Yagufbg8leRigHDelqk8/YKhxkLDkYVwhQ== +"@avalabs/glacier-sdk@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/glacier-sdk/-/glacier-sdk-3.1.0-alpha.5.tgz#54b141a2cc88805067be5519db20c444128d013c" + integrity sha512-RbSqfDhdp5iiMv8ERuU3vGSBNjtR1cfwKMpopVVjURQuuV3+BIFmGsy1nwqlbeax22Lr8445kD0SQbNQn7vjuA== + "@avalabs/hw-app-avalanche@0.14.1": version "0.14.1" resolved "https://registry.yarnpkg.com/@avalabs/hw-app-avalanche/-/hw-app-avalanche-0.14.1.tgz#70b6248e67cf7d64d0640517f88c19632524ac68" @@ -267,25 +267,25 @@ ledger-bitcoin "^0.2.1" sha3 "2.1.4" -"@avalabs/types@3.1.0-alpha.1": - version "3.1.0-alpha.1" - resolved "https://registry.yarnpkg.com/@avalabs/types/-/types-3.1.0-alpha.1.tgz#b3b1673f4c9f4e1db623f663423d9d768c6cccad" - integrity sha512-zzmsaE51W17P6trWfWmPQJn21+z350kxYg5y6HDyUkZYPWxBpmg6NLkj5bfw06qBCjhTlp0mscEe8OOUGrU6DA== - "@avalabs/types@3.1.0-alpha.3": version "3.1.0-alpha.3" resolved "https://registry.yarnpkg.com/@avalabs/types/-/types-3.1.0-alpha.3.tgz#3b7fb8cb8e2f124e0b11406788a903142753b5a0" integrity sha512-WN6NyPsF9XpmQp2SN4UbwjBT+XYH8di8gvL9rppQxz4d43xRLI1T2WVt/LXJgYdOauhuaSelb6zB4yNqGyKutQ== -"@avalabs/vm-module-types@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@avalabs/vm-module-types/-/vm-module-types-0.3.0.tgz#4139cd70b9266979a5e3e783d9d6bd005e1d4279" - integrity sha512-IzVd/LnXxMIsfAJryl4HniMrfEB8FjgMm1VUVcG0tOHdFgRcqw10/O746lxbkU11ZPBbjr3tSxjn6fnbsQ6PPA== +"@avalabs/types@3.1.0-alpha.5": + version "3.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@avalabs/types/-/types-3.1.0-alpha.5.tgz#82dcb2cdb63b47186689186e09ad13e207180e8c" + integrity sha512-OpHTXQL/RGzL9FOfQAN4mc6jD/kZVthRtzS2eCqoI6gAZmNzf3Paq46U6cKabti1coyJDIcrXrRl1U1qtCpoYg== + +"@avalabs/vm-module-types@0.0.0-feat-add-core-version-to-evm-p-20240903160850": + version "0.0.0-feat-add-core-version-to-evm-p-20240903160850" + resolved "https://registry.yarnpkg.com/@avalabs/vm-module-types/-/vm-module-types-0.0.0-feat-add-core-version-to-evm-p-20240903160850.tgz#ef8a5bea03f9dc4c4cbe0bf5525258bafd108908" + integrity sha512-9VhA/2JbLejxDhVOHTg5wiKbOhREsM51Pwwyk++IIR60JMmda2cWzpA2X+j9b7mu1OfZOaTntYA5EhqHIYQh7w== dependencies: - "@avalabs/core-wallets-sdk" "3.1.0-alpha.1" + "@avalabs/core-wallets-sdk" "3.1.0-alpha.5" + "@avalabs/glacier-sdk" "3.1.0-alpha.5" "@metamask/rpc-errors" "6.3.0" bitcoinjs-lib "5.2.0" - ethers "6.8.1" zod "3.23.8" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":