Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: perform eth_sendTransaction via EVM module #51

Merged
merged 10 commits into from
Oct 10, 2024
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(
gergelylovas marked this conversation as resolved.
Show resolved Hide resolved
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
Loading