Skip to content

Commit

Permalink
feat: use eth_sendTransactionBatch for swaps requiring allowance bump (
Browse files Browse the repository at this point in the history
  • Loading branch information
meeh0w authored Jan 30, 2025
1 parent 4a00253 commit 0171432
Show file tree
Hide file tree
Showing 46 changed files with 2,779 additions and 686 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export function DAppRequestHandlerMiddleware(
}, new Map<string, DAppRequestHandler>());

return async (context, next) => {
const handler = handlerMap.get(context.request.params.request.method);
const method = context.request.params.request.method;
const handler = handlerMap.get(method);
// Call correct handler method based on authentication status
let promise: Promise<JsonRpcResponse<unknown>>;

Expand All @@ -49,43 +50,43 @@ export function DAppRequestHandlerMiddleware(
: handler.handleUnauthenticated(params);
} else {
const [module] = await resolve(
moduleManager.loadModule(
context.request.params.scope,
context.request.params.request.method,
),
moduleManager.loadModule(context.request.params.scope, method),
);

if (!context.network) {
promise = Promise.reject(ethErrors.provider.disconnected());
} else if (!module) {
promise = engine(context.network).then((e) =>
e.handle<unknown, unknown>({
...context.request.params.request,
id: crypto.randomUUID(),
jsonrpc: '2.0',
}),
);
} else if (
!context.authenticated &&
!moduleManager.isNonRestrictedMethod(module, method)
) {
promise = Promise.reject(ethErrors.provider.unauthorized());
} else {
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,
promise = module.onRpcRequest(
{
chainId: context.network.caipId,
dappInfo: {
icon: context.domainMetadata.icon ?? '',
name: context.domainMetadata.name ?? '',
url: context.domainMetadata.url ?? '',
},
context.network,
);
} else {
promise = engine(context.network).then((e) =>
e.handle<unknown, unknown>({
...context.request.params.request,
id: crypto.randomUUID(),
jsonrpc: '2.0',
}),
);
}
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,
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/background/connections/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type ExtensionConnectionMessage<
Params = any,
> = JsonRpcRequest<Method, Params>;

export type HandlerParameters<Type> = ExtractHandlerTypes<Type>['Params'];

export type ExtensionConnectionMessageResponse<
Method extends ExtensionRequest | DAppProviderRequest | RpcMethod = any,
Result = any,
Expand Down
13 changes: 13 additions & 0 deletions src/background/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,17 @@ export type Never<T> = {

export type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;

export type FirstParameter<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P[0]
: never;

export const ACTION_HANDLED_BY_MODULE = '__handled.via.vm.modules__';

export const hasDefined = <T extends object, K extends keyof T>(
obj: T,
key: K,
): obj is EnsureDefined<T, K> => {
return obj[key] !== undefined;
};
7 changes: 5 additions & 2 deletions src/background/runtime/openApprovalWindow.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { container } from 'tsyringe';

import { Action } from '../services/actions/models';
import { Action, MultiTxAction } from '../services/actions/models';
import { ApprovalService } from '../services/approvals/ApprovalService';

export const openApprovalWindow = async (action: Action, url: string) => {
export const openApprovalWindow = async (
action: Action | MultiTxAction,
url: string,
) => {
const actionId = crypto.randomUUID();
// using direct injection instead of the constructor to prevent circular dependencies
const approvalService = container.resolve(ApprovalService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/model
import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow';
import { canSkipApproval } from '@src/utils/canSkipApproval';

import type { Action } from '../../actions/models';
import { type Action, buildActionForRequest } from '../../actions/models';
import { SecretsService } from '../../secrets/SecretsService';
import { AccountsService } from '../AccountsService';
import type { ImportedAccount, PrimaryAccount } from '../models';
Expand Down Expand Up @@ -162,10 +162,7 @@ export class AvalancheDeleteAccountsHandler extends DAppRequestHandler<
}
}

const actionData: Action<{
accounts: DeleteAccountsDisplayData;
}> = {
...request,
const actionData = buildActionForRequest(request, {
scope,
displayData: {
accounts: {
Expand All @@ -174,7 +171,7 @@ export class AvalancheDeleteAccountsHandler extends DAppRequestHandler<
wallet: walletNames,
},
},
};
});

await openApprovalWindow(actionData, 'deleteAccounts');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow';
import { canSkipApproval } from '@src/utils/canSkipApproval';

import { AccountsService } from '../AccountsService';
import { Action } from '../../actions/models';
import { Action, buildActionForRequest } from '../../actions/models';
import { Account } from '../models';

type Params = [accountId: string, newName: string];
Expand Down Expand Up @@ -79,14 +79,13 @@ export class AvalancheRenameAccountHandler extends DAppRequestHandler<
}
}

const actionData: Action<{ account: Account; newName: string }> = {
...request,
const actionData = buildActionForRequest(request, {
scope,
displayData: {
account,
newName,
},
};
});

await openApprovalWindow(actionData, 'renameAccount');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/model
import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow';
import { AccountsService } from '../AccountsService';
import { Account } from '../models';
import { Action } from '../../actions/models';
import { Action, buildActionForRequest } from '../../actions/models';
import { PermissionsService } from '../../permissions/PermissionsService';
import { isPrimaryAccount } from '../utils/typeGuards';
import { canSkipApproval } from '@src/utils/canSkipApproval';
Expand Down Expand Up @@ -82,13 +82,12 @@ export class AvalancheSelectAccountHandler extends DAppRequestHandler<
return { ...request, result: null };
}

const actionData: Action<{ selectedAccount: Account }> = {
...request,
const actionData = buildActionForRequest(request, {
scope,
displayData: {
selectedAccount,
},
};
});

await openApprovalWindow(actionData, `switchAccount`);

Expand Down
55 changes: 55 additions & 0 deletions src/background/services/actions/ActionsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ActionsEvent,
ActionStatus,
ACTIONS_STORAGE_KEY,
ActionType,
} from './models';
import { filterStaleActions } from './utils';
import { ApprovalController } from '@src/background/vmModules/ApprovalController';
Expand Down Expand Up @@ -64,6 +65,7 @@ describe('background/services/actions/ActionsService.ts', () => {
onApproved: jest.fn(),
onRejected: jest.fn(),
updateTx: jest.fn(),
updateTxInBatch: jest.fn(),
} as unknown as jest.Mocked<ApprovalController>;

actionsService = new ActionsService(
Expand All @@ -82,6 +84,59 @@ describe('background/services/actions/ActionsService.ts', () => {
);
});

describe('when dealing with a batch action', () => {
const signingRequests = [
{ from: '0x1', to: '0x2', value: '0x3' },
{ from: '0x1', to: '0x2', value: '0x4' },
];
const pendingActions = {
'id-0': {
type: ActionType.Single,
actionId: 'id-0',
},
'id-1': {
signingRequests,
type: ActionType.Batch,
actionId: 'id-1',
},
};

beforeEach(() => {
jest
.spyOn(actionsService, 'getActions')
.mockResolvedValueOnce(pendingActions as any);
});

it('uses the ApprovalController.updateTxInBatch() to fetch the new action data & saves it', async () => {
const newDisplayData = { ...displayData };
const updatedActionData = {
signingRequests,
displayData: newDisplayData,
} as any;

approvalController.updateTxInBatch.mockReturnValueOnce(
updatedActionData,
);

await actionsService.updateTx(
'id-1',
{
maxFeeRate: 5n,
maxTipRate: 1n,
},
0,
);

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

it('uses the ApprovalController.updateTx() to fetch the new action data & saves it', async () => {
const pendingActions = {
'id-0': {
Expand Down
Loading

0 comments on commit 0171432

Please sign in to comment.