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

feat: use eth_sendTransactionBatch for swaps requiring allowance bump #122

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
783f42e
feat: support eth_sendTransactionBatch on the backend
meeh0w Jan 14, 2025
4ec9dfa
feat: support batch transactions in fee customizer hook
meeh0w Jan 14, 2025
7ff2f2d
refactor: extract some common utils for SwapProvider
meeh0w Jan 14, 2025
ac7b88d
feat: implement one-click swaps in SwapProvider
meeh0w Jan 14, 2025
2b8853c
feat: implement the batch approval screen
meeh0w Jan 14, 2025
1313ec9
chore: use canary VMMs and fix types, tests, translations
meeh0w Jan 14, 2025
e42bab5
fix: ui improvements, some cleanup
meeh0w Jan 15, 2025
82d787d
test: missing suite for WalletService.signTransactionBatch()
meeh0w Jan 15, 2025
4241421
test: missing suites for ActionsService & ApprovalController
meeh0w Jan 15, 2025
6f6792c
fix: validate wallet type before engaging one-click-swap mode
meeh0w Jan 15, 2025
e7da81b
test: fix SwapProvider tests
meeh0w Jan 15, 2025
fa2cf5f
fix: allow changing fees for each tx individually
meeh0w Jan 17, 2025
54f79f4
Merge remote-tracking branch 'origin/main' into feat/batch-approvals
meeh0w Jan 17, 2025
a365ce9
chore: typings
meeh0w Jan 17, 2025
a4d1a47
chore: typing fixes
meeh0w Jan 17, 2025
ede9bbf
test: fix failing updateTxData() suite
meeh0w Jan 17, 2025
0c67340
refactor: address review comments
meeh0w Jan 27, 2025
f14aa49
Merge remote-tracking branch 'origin/main' into feat/batch-approvals
meeh0w Jan 27, 2025
4dac362
chore: update dependencies
meeh0w Jan 29, 2025
5a9d3b5
Merge remote-tracking branch 'origin/main' into feat/batch-approvals
meeh0w Jan 29, 2025
05f330b
Merge remote-tracking branch 'origin/main' into feat/batch-approvals
meeh0w Jan 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.11.12",
"@avalabs/avalanche-module": "0.0.0-feat-eth-send-transaction-batc-20250117113406",
"@avalabs/avalanchejs": "4.1.0-alpha.25",
"@avalabs/bitcoin-module": "0.11.12",
"@avalabs/bitcoin-module": "0.0.0-feat-eth-send-transaction-batc-20250117113406",
"@avalabs/bridge-unified": "4.0.1",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.19",
"@avalabs/core-chains-sdk": "3.1.0-alpha.19",
Expand All @@ -37,11 +37,11 @@
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.19",
"@avalabs/core-utils-sdk": "3.1.0-alpha.19",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.19",
"@avalabs/evm-module": "0.11.12",
"@avalabs/evm-module": "0.0.0-feat-eth-send-transaction-batc-20250117113406",
"@avalabs/glacier-sdk": "3.1.0-alpha.19",
"@avalabs/hw-app-avalanche": "0.14.1",
"@avalabs/types": "3.1.0-alpha.19",
"@avalabs/vm-module-types": "0.11.12",
"@avalabs/vm-module-types": "0.0.0-feat-eth-send-transaction-batc-20250117113406",
"@blockaid/client": "0.10.0",
"@coinbase/cbpay-js": "1.6.0",
"@cubist-labs/cubesigner-sdk": "0.3.28",
Expand Down
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
52 changes: 52 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', () => {
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 +83,57 @@ 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': {
actionId: 'id-0',
},
'id-1': {
signingRequests,
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
119 changes: 84 additions & 35 deletions src/background/services/actions/ActionsService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { omit } from 'lodash';
import { EventEmitter } from 'events';
import { injectAll, singleton } from 'tsyringe';
import { StorageService } from '../storage/StorageService';
Expand All @@ -9,6 +10,8 @@ import {
ActionStatus,
ACTIONS_STORAGE_KEY,
ActionUpdate,
MultiTxAction,
isBatchApprovalAction,
} from './models';
import { ethErrors } from 'eth-rpc-errors';
import { DAppRequestHandler } from '@src/background/connections/dAppConnection/DAppRequestHandler';
Expand Down Expand Up @@ -78,8 +81,10 @@ export class ActionsService implements OnStorageReady {
this.eventEmitter.emit(ActionsEvent.ACTION_UPDATED, actions);
}

async addAction(action: Action): Promise<Action> {
const pendingAction: Action = {
async addAction(
action: Action | MultiTxAction,
): Promise<Action | MultiTxAction> {
const pendingAction: Action | MultiTxAction = {
...action,
time: Date.now(),
status: ActionStatus.PENDING,
Expand All @@ -104,16 +109,21 @@ export class ActionsService implements OnStorageReady {

async emitResult(
id: string,
action: Action,
action: Action | MultiTxAction,
isSuccess: boolean,
result: any,
) {
await this.removeAction(id);

// We dont want display data to be emitted. Sometimes it can not be serialized and it's content is internal to Core
// Make sure not to modify the original action object

const { displayData, ...actionWithoutDisplayData } = action;
// We dont want display data to be emitted. Sometimes it can be not serialized and it's content is internal to Core
const actionWithoutDisplayData = isBatchApprovalAction(action)
? {
...action,
signingRequests: action.signingRequests.map((req) =>
omit(req, 'displayData'),
),
}
: omit(action, 'displayData');

this.eventEmitter.emit(ActionsEvent.ACTION_COMPLETED, {
type: isSuccess
Expand Down Expand Up @@ -193,29 +203,43 @@ export class ActionsService implements OnStorageReady {
ethErrors.provider.userRejectedRequest(),
);
} else {
await this.saveActions({
...currentPendingActions,
[id]: {
...pendingMessage,
displayData: {
...pendingMessage.displayData,
...displayData,
if (isBatchApprovalAction(pendingMessage)) {
await this.saveActions({
...currentPendingActions,
[id]: {
...pendingMessage,
signingRequests: pendingMessage.signingRequests,
status,
result,
error,
},
signingData: getUpdatedSigningData(
pendingMessage.signingData,
signingData,
),
status,
result,
error,
},
});
});
} else {
await this.saveActions({
...currentPendingActions,
[id]: {
...pendingMessage,
displayData: {
...pendingMessage.displayData,
...displayData,
},
signingData: getUpdatedSigningData(
pendingMessage.signingData,
signingData,
),
status,
result,
error,
},
});
}
}
}

async updateTx(
id: string,
newData: Parameters<EvmTxUpdateFn>[0] | Parameters<BtcTxUpdateFn>[0],
txIndex?: number,
) {
const currentPendingRequests = await this.getActions();
const pendingRequest = currentPendingRequests[id];
Expand All @@ -224,19 +248,44 @@ export class ActionsService implements OnStorageReady {
throw new Error(`No request found with id: ${id}`);
}

const { signingData, displayData } = this.approvalController.updateTx(
id,
newData,
);
if (isBatchApprovalAction(pendingRequest)) {
if (
(!('maxFeeRate' in newData) && !('maxTipRate' in newData)) ||
typeof txIndex !== 'number'
) {
return;
}

await this.saveActions({
...currentPendingRequests,
[id]: {
...pendingRequest,
signingData,
displayData,
},
});
const { displayData, signingRequests } =
this.approvalController.updateTxInBatch(
pendingRequest,
newData,
txIndex,
);

await this.saveActions({
...currentPendingRequests,
[id]: {
...pendingRequest,
signingRequests,
displayData,
},
});
} else {
const { signingData, displayData } = this.approvalController.updateTx(
pendingRequest,
newData,
);

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

addListener(
Expand Down
4 changes: 2 additions & 2 deletions src/background/services/actions/handlers/getActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { ExtensionRequest } from '@src/background/connections/extensionConnectio
import { ExtensionRequestHandler } from '@src/background/connections/models';
import { injectable } from 'tsyringe';
import { ActionsService } from '../ActionsService';
import { Action } from '../models';
import { Action, MultiTxAction } from '../models';

type HandlerType = ExtensionRequestHandler<
ExtensionRequest.ACTION_GET,
Action,
Action | MultiTxAction,
[messageId: string]
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ describe('src/background/services/actions/handlers/updateTxData', () => {
});

await handleRequest(getRequest(['id', { feeRate: 5 }]));
expect(actionsService.updateTx).toHaveBeenCalledWith('id', { feeRate: 5 });
expect(actionsService.updateTx).toHaveBeenCalledWith(
gergelylovas marked this conversation as resolved.
Show resolved Hide resolved
'id',
{ feeRate: 5 },
undefined,
);
});
});
19 changes: 12 additions & 7 deletions src/background/services/actions/handlers/updateTxData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { injectable } from 'tsyringe';
import { EvmTxUpdateFn, BtcTxUpdateFn } from '@avalabs/vm-module-types';
import {
EvmTxUpdateFn,
BtcTxUpdateFn,
EvmTxBatchUpdateFn,
} from '@avalabs/vm-module-types';

import { SendErrorMessage } from '@src/utils/send/models';
import { ExtensionRequest } from '@src/background/connections/extensionConnection/models';
Expand All @@ -10,10 +14,11 @@ import { ActionsService } from '../ActionsService';
type HandlerType = ExtensionRequestHandler<
ExtensionRequest.ACTION_UPDATE_TX_DATA,
null,
[
id: string,
newData: Parameters<EvmTxUpdateFn>[0] | Parameters<BtcTxUpdateFn>[0],
]
| [
id: string,
newData: Parameters<EvmTxUpdateFn>[0] | Parameters<BtcTxUpdateFn>[0],
]
| [id: string, newData: Parameters<EvmTxBatchUpdateFn>[0], txIndex: number]
>;

@injectable()
Expand All @@ -22,7 +27,7 @@ export class UpdateActionTxDataHandler implements HandlerType {

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

if (!id) {
return {
Expand All @@ -44,7 +49,7 @@ export class UpdateActionTxDataHandler implements HandlerType {
}

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