Skip to content

Commit

Permalink
refactor: perform eth_sendTransaction via EVM module (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
meeh0w authored Oct 10, 2024
1 parent 1777f07 commit f779698
Show file tree
Hide file tree
Showing 67 changed files with 1,021 additions and 4,624 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"sentry": "node sentryscript.js"
},
"dependencies": {
"@avalabs/avalanche-module": "0.7.3",
"@avalabs/avalanche-module": "0.8.0",
"@avalabs/avalanchejs": "4.0.5",
"@avalabs/bitcoin-module": "0.7.3",
"@avalabs/bitcoin-module": "0.8.0",
"@avalabs/bridge-unified": "2.1.0",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.7",
"@avalabs/core-chains-sdk": "3.1.0-alpha.7",
Expand All @@ -37,11 +37,11 @@
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.7",
"@avalabs/core-utils-sdk": "3.1.0-alpha.7",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.7",
"@avalabs/evm-module": "0.7.3",
"@avalabs/evm-module": "0.8.0",
"@avalabs/glacier-sdk": "3.1.0-alpha.7",
"@avalabs/hw-app-avalanche": "0.14.1",
"@avalabs/types": "3.1.0-alpha.3",
"@avalabs/vm-module-types": "0.7.3",
"@avalabs/vm-module-types": "0.8.0",
"@blockaid/client": "0.10.0",
"@coinbase/cbpay-js": "1.6.0",
"@cubist-labs/cubesigner-sdk": "0.3.28",
Expand Down
1 change: 0 additions & 1 deletion src/background/connections/dAppConnection/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export enum DAppProviderRequest {
WALLET_GET_CHAIN = 'wallet_getEthereumChain',
WALLET_SWITCH_ETHEREUM_CHAIN = 'wallet_switchEthereumChain',
WALLET_WATCH_ASSET = 'wallet_watchAsset',
ETH_SEND_TX = 'eth_sendTransaction',
PERSONAL_EC_RECOVER = 'personal_ecRecover',
PERSONAL_SIGN = 'personal_sign',
ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4',
Expand Down
2 changes: 0 additions & 2 deletions src/background/connections/dAppConnection/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { WalletGetPermissionsHandler } from '@src/background/services/permission
import { WalletRequestPermissionsHandler } from '@src/background/services/permissions/handlers/wallet_requestPermissions';
import { WalletWatchAssetHandler } from '@src/background/services/settings/events/wallet_watchAsset';
import { WalletGetEthereumChainHandler } from '@src/background/services/network/handlers/wallet_getEthereumChain';
import { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction';
import { AvalancheSelectWalletHandler } from '@src/background/services/web3/handlers/avalanche_selectWallet';
import { ConnectRequestHandler } from '@src/background/services/web3/handlers/connect';
import { AvalancheGetProviderState } from '@src/background/services/web3/handlers/avalanche_getProviderState';
Expand Down Expand Up @@ -69,7 +68,6 @@ import { AvalancheRenameAccountHandler } from '@src/background/services/accounts
{ token: 'DAppRequestHandler', useToken: WalletGetPermissionsHandler },
{ token: 'DAppRequestHandler', useToken: WalletRequestPermissionsHandler },
{ token: 'DAppRequestHandler', useToken: WalletWatchAssetHandler },
{ token: 'DAppRequestHandler', useToken: EthSendTransactionHandler },
{ token: 'DAppRequestHandler', useToken: ConnectRequestHandler },
{ token: 'DAppRequestHandler', useToken: AvalancheGetProviderState },
{
Expand Down
1 change: 1 addition & 0 deletions src/background/connections/extensionConnection/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum ExtensionRequest {

ACTION_GET = 'action_getAction',
ACTION_UPDATE = 'action_updateAction',
ACTION_UPDATE_TX_DATA = 'action_updateTxData',

PERMISSIONS_ADD_DOMAIN = 'permissions_addDomain',
PERMISSIONS_GET_PERMISSIONS = 'permissions_getPermissionsForDomain',
Expand Down
2 changes: 2 additions & 0 deletions src/background/connections/extensionConnection/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import { StartBalancesPollingHandler } from '@src/background/services/balances/h
import { StopBalancesPollingHandler } from '@src/background/services/balances/handlers/stopBalancesPolling';
import { BalancesUpdatedEvents } from '@src/background/services/balances/events/balancesUpdatedEvent';
import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBridge/handlers/unifiedBridgeTrackTransfer';
import { UpdateActionTxDataHandler } from '@src/background/services/actions/handlers/updateTxData';

/**
* TODO: GENERATE THIS FILE AS PART OF THE BUILD PROCESS
Expand All @@ -141,6 +142,7 @@ import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBrid
{ token: 'ExtensionRequestHandler', useToken: DeleteAccountHandler },
{ token: 'ExtensionRequestHandler', useToken: GetActionHandler },
{ token: 'ExtensionRequestHandler', useToken: UpdateActionHandler },
{ token: 'ExtensionRequestHandler', useToken: UpdateActionTxDataHandler },
{ token: 'ExtensionRequestHandler', useToken: ClearAnalyticsIdsHandler },
{ token: 'ExtensionRequestHandler', useToken: GetAnalyticsIdsHandler },
{ token: 'ExtensionRequestHandler', useToken: InitAnalyticsIdsHandler },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NetworkService } from '@src/background/services/network/NetworkService';
import { ActiveNetworkMiddleware } from './ActiveNetworkMiddleware';
import { RpcMethod } from '@avalabs/vm-module-types';
import getTargetNetworkForTx from '@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx';

jest.mock(
'@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx'
);

describe('src/background/connections/middlewares/ActiveNetworkMiddleware', () => {
const networkService = {
getNetwork: jest.fn(),
} as unknown as NetworkService;

beforeEach(() => {
jest.resetAllMocks();
});

it('errors out for cross-environment EVM transaction attempts', async () => {
const call = ActiveNetworkMiddleware(networkService);

const next = jest.fn();
const onError = jest.fn();
const error = new Error('Cross-env error');

jest.mocked(getTargetNetworkForTx).mockRejectedValueOnce(error);

await call(
{
request: {
params: {
scope: 'eip155:43114', // C-Chain Mainnet
request: {
method: RpcMethod.ETH_SEND_TRANSACTION,
params: [
{
chainId: '0xa869', // C-Chain Fuji (43113)
},
],
},
},
},
} as any,
next,
onError
);

expect(next).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalledWith(error);
});
});
36 changes: 28 additions & 8 deletions src/background/connections/middlewares/ActiveNetworkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ExtensionConnectionMessage,
ExtensionConnectionMessageResponse,
} from '../models';
import { RpcMethod } from '@avalabs/vm-module-types';
import getTargetNetworkForTx from '@src/background/services/wallet/handlers/eth_sendTransaction/utils/getTargetNetworkForTx';

export function ActiveNetworkMiddleware(
networkService: NetworkService
Expand All @@ -15,21 +17,39 @@ export function ActiveNetworkMiddleware(
JsonRpcResponse | ExtensionConnectionMessageResponse
> {
return async (context, next, error) => {
const { scope } = context.request.params;
const {
scope,
request: { method, params },
} = context.request.params;

if (scope) {
const network = await networkService.getNetwork(
context.request.params.scope
);
if (!scope) {
next();
return;
}

const isEthSendTx = method === RpcMethod.ETH_SEND_TRANSACTION;
const hasParams = Array.isArray(params) && typeof params[0] === 'object';

let network;

if (!network) {
error(new Error(`Unrecognized network: ${scope}`));
if (isEthSendTx && hasParams) {
try {
network = await getTargetNetworkForTx(params[0], networkService, scope);
} catch (err: any) {
error(err);
return;
}
} else {
network = await networkService.getNetwork(scope);
}

context.network = network;
if (!network) {
error(new Error(`Unrecognized network: ${scope}`));
return;
}

context.network = network;

next();
};
}
42 changes: 42 additions & 0 deletions src/background/services/actions/ActionsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('background/services/actions/ActionsService.ts', () => {
approvalController = {
onApproved: jest.fn(),
onRejected: jest.fn(),
updateTx: jest.fn(),
} as unknown as jest.Mocked<ApprovalController>;

actionsService = new ActionsService(
Expand All @@ -75,6 +76,47 @@ describe('background/services/actions/ActionsService.ts', () => {
(filterStaleActions as jest.Mock).mockImplementation((a) => a);
});

describe('updateTx()', () => {
it('throws error if the request does not exist', async () => {
await expect(actionsService.updateTx('weird-id', {})).rejects.toThrow(
/No request found with id/
);
});

it('uses the ApprovalController.updateTx() to fetch the new action data & saves it', async () => {
const pendingActions = {
'id-0': {
actionId: 'id-0',
},
'id-1': {
actionId: 'id-1',
},
};
jest
.spyOn(actionsService, 'getActions')
.mockResolvedValueOnce(pendingActions as any);

const signingData = { outputs: [], inputs: [] } as any;
const newDisplayData = { ...displayData };
const updatedActionData = {
signingData,
displayData: newDisplayData,
} as any;

approvalController.updateTx.mockReturnValueOnce(updatedActionData);

await actionsService.updateTx('id-1', { feeRate: 5 });

expect(storageService.save).toHaveBeenCalledWith(ACTIONS_STORAGE_KEY, {
...pendingActions,
'id-1': {
...pendingActions['id-1'],
...updatedActionData,
},
});
});
});

describe('getActions', () => {
it('gets actions from storage and session when unlocked', async () => {
const actions = {
Expand Down
27 changes: 27 additions & 0 deletions src/background/services/actions/ActionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ACTION_HANDLED_BY_MODULE } from '@src/background/models';
import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models';
import { getUpdatedSigningData } from '@src/utils/actions/getUpdatedActionData';
import { ApprovalController } from '@src/background/vmModules/ApprovalController';
import { BtcTxUpdateFn, EvmTxUpdateFn } from '@avalabs/vm-module-types';

@singleton()
export class ActionsService implements OnStorageReady {
Expand Down Expand Up @@ -212,6 +213,32 @@ export class ActionsService implements OnStorageReady {
}
}

async updateTx(
id: string,
newData: Parameters<EvmTxUpdateFn>[0] | Parameters<BtcTxUpdateFn>[0]
) {
const currentPendingRequests = await this.getActions();
const pendingRequest = currentPendingRequests[id];

if (!pendingRequest) {
throw new Error(`No request found with id: ${id}`);
}

const { signingData, displayData } = this.approvalController.updateTx(
id,
newData
);

await this.saveActions({
...currentPendingRequests,
[id]: {
...pendingRequest,
signingData,
displayData,
},
});
}

addListener(
event: ActionsEvent.ACTION_COMPLETED,
callback: (data: {
Expand Down
57 changes: 57 additions & 0 deletions src/background/services/actions/handlers/updateTxData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ExtensionRequest } from '@src/background/connections/extensionConnection/models';
import { UpdateActionTxDataHandler } from './updateTxData';
import { matchingPayload } from '@src/tests/test-utils';
import { SendErrorMessage } from '@src/utils/send/models';

describe('src/background/services/actions/handlers/updateTxData', () => {
const actionsService = {
getActions: jest.fn(),
updateTx: jest.fn(),
};

const handleRequest = async (request) => {
const handler = new UpdateActionTxDataHandler(actionsService as any);

return handler.handle(request);
};

const getRequest = (params) => ({
request: {
id: '1234',
method: ExtensionRequest.ACTION_UPDATE_TX_DATA,
params,
},
});

beforeEach(() => {
jest.resetAllMocks();
});

it('remaps BTC tx transaction error to insufficient balance for fee', async () => {
jest.mocked(actionsService.getActions).mockResolvedValue({
id: {},
});
jest
.mocked(actionsService.updateTx)
.mockRejectedValueOnce({ message: 'Unable to create transaction' });

expect(await handleRequest(getRequest(['id', { feeRate: 5 }]))).toEqual(
matchingPayload({ error: SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE })
);
});

it('validates the request to update exists', async () => {
expect(await handleRequest(getRequest(['id', { feeRate: 5 }]))).toEqual(
matchingPayload({ error: 'no pending requests found' })
);
});

it('calls ActionsService.updateTx()', async () => {
jest.mocked(actionsService.getActions).mockResolvedValue({
id: {},
});

await handleRequest(getRequest(['id', { feeRate: 5 }]));
expect(actionsService.updateTx).toHaveBeenCalledWith('id', { feeRate: 5 });
});
});
60 changes: 60 additions & 0 deletions src/background/services/actions/handlers/updateTxData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { injectable } from 'tsyringe';
import { EvmTxUpdateFn, BtcTxUpdateFn } from '@avalabs/vm-module-types';

import { SendErrorMessage } from '@src/utils/send/models';
import { ExtensionRequest } from '@src/background/connections/extensionConnection/models';
import { ExtensionRequestHandler } from '@src/background/connections/models';

import { ActionsService } from '../ActionsService';

type HandlerType = ExtensionRequestHandler<
ExtensionRequest.ACTION_UPDATE_TX_DATA,
null,
[
id: string,
newData: Parameters<EvmTxUpdateFn>[0] | Parameters<BtcTxUpdateFn>[0]
]
>;

@injectable()
export class UpdateActionTxDataHandler implements HandlerType {
method = ExtensionRequest.ACTION_UPDATE_TX_DATA as const;

constructor(private actionsService: ActionsService) {}
handle: HandlerType['handle'] = async ({ request }) => {
const [id, newData] = request.params;

if (!id) {
return {
...request,
error: 'no request id in params',
};
}

const actions = await this.actionsService.getActions();

if (!actions) {
return { ...request, error: 'no pending requests found' };
}

const action = actions[id];

if (!action) {
return { ...request, error: 'no request found with that id' };
}

try {
await this.actionsService.updateTx(id, newData);
return { ...request, result: null };
} catch (err: any) {
if (err?.message === 'Unable to create transaction') {
return {
...request,
error: SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE,
};
}

return { ...request, error: err };
}
};
}
Loading

0 comments on commit f779698

Please sign in to comment.