@avalabs/evm-module 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +36 -5
  4. package/CHANGELOG.md +19 -0
  5. package/dist/index.cjs +7 -4
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +30 -12
  8. package/dist/index.d.ts +30 -12
  9. package/dist/index.js +6 -4
  10. package/dist/index.js.map +1 -1
  11. package/package.json +5 -3
  12. package/src/env.ts +25 -0
  13. package/src/handlers/eth-send-transaction/eth-send-transaction.test.ts +333 -0
  14. package/src/handlers/eth-send-transaction/eth-send-transaction.ts +170 -0
  15. package/src/handlers/eth-send-transaction/schema.test.ts +240 -0
  16. package/src/handlers/eth-send-transaction/schema.ts +20 -0
  17. package/src/handlers/get-network-fee/get-network-fee.test.ts +1 -1
  18. package/src/handlers/get-network-fee/get-network-fee.ts +9 -11
  19. package/src/handlers/get-tokens/get-tokens.ts +2 -1
  20. package/src/handlers/get-transaction-history/converters/etherscan-transaction-converter/get-transaction-from-etherscan.ts +11 -3
  21. package/src/handlers/get-transaction-history/converters/evm-transaction-converter/get-sender-info.ts +2 -2
  22. package/src/handlers/get-transaction-history/converters/evm-transaction-converter/get-transaction-from-glacier.test.ts +1 -5
  23. package/src/handlers/get-transaction-history/converters/evm-transaction-converter/get-transactions-from-glacier.ts +12 -3
  24. package/src/handlers/get-transaction-history/converters/evm-transaction-converter/get-tx-type.ts +9 -5
  25. package/src/handlers/get-transaction-history/get-transaction-history.test.ts +2 -0
  26. package/src/handlers/get-transaction-history/get-transaction-history.ts +11 -3
  27. package/src/index.ts +2 -72
  28. package/src/module.ts +95 -0
  29. package/src/types.ts +13 -0
  30. package/src/utils/estimate-gas-limit.ts +27 -0
  31. package/src/utils/get-chain-id.ts +12 -0
  32. package/src/utils/get-nonce.ts +11 -0
  33. package/src/utils/get-provider.ts +18 -23
  34. package/tsconfig.json +1 -5
@@ -0,0 +1,333 @@
1
+ import { ethSendTransaction } from './eth-send-transaction';
2
+ import { parseRequestParams } from './schema';
3
+ import { estimateGasLimit } from '../../utils/estimate-gas-limit';
4
+ import { getNonce } from '../../utils/get-nonce';
5
+ import { rpcErrors } from '@metamask/rpc-errors';
6
+ import { RpcMethod, type ApprovalController, type Network } from '@avalabs/vm-module-types';
7
+ import { ZodError } from 'zod';
8
+ import { getProvider } from '../../utils/get-provider';
9
+
10
+ const mockGetProvider = getProvider as jest.MockedFunction<typeof getProvider>;
11
+
12
+ jest.mock('./schema');
13
+ jest.mock('../../utils/estimate-gas-limit');
14
+ jest.mock('../../utils/get-nonce');
15
+ jest.mock('../../utils/get-provider');
16
+
17
+ const mockOnTransactionConfirmed = jest.fn();
18
+ const mockOnTransactionReverted = jest.fn();
19
+ const mockApprovalController: jest.Mocked<ApprovalController> = {
20
+ requestApproval: jest.fn(),
21
+ onTransactionConfirmed: mockOnTransactionConfirmed,
22
+ onTransactionReverted: mockOnTransactionReverted,
23
+ };
24
+
25
+ const mockParseRequestParams = parseRequestParams as jest.MockedFunction<typeof parseRequestParams>;
26
+ const mockEstimateGasLimit = estimateGasLimit as jest.MockedFunction<typeof estimateGasLimit>;
27
+ const mockGetNonce = getNonce as jest.MockedFunction<typeof getNonce>;
28
+ const mockSend = jest.fn();
29
+ const mockWaitForTransaction = jest.fn();
30
+
31
+ const mockProvider = {
32
+ send: mockSend,
33
+ waitForTransaction: mockWaitForTransaction,
34
+ };
35
+
36
+ // @ts-expect-error missing properties
37
+ mockGetProvider.mockReturnValue(mockProvider);
38
+ const testNetwork: Network = {
39
+ isTestnet: false,
40
+ chainId: 1,
41
+ chainName: 'chainName',
42
+ rpcUrl: 'rpcUrl',
43
+ logoUrl: 'logoUrl',
44
+ utilityAddresses: { multicall: 'multiContractAddress' },
45
+ networkToken: {
46
+ name: 'Ethereum',
47
+ symbol: 'ETH',
48
+ decimals: 9,
49
+ description: 'Ethereum Token',
50
+ logoUri: 'some logo uri',
51
+ },
52
+ };
53
+
54
+ const testParams = { from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue', nonce: '12', gas: '0x5208' };
55
+
56
+ const testRequestParams = () => ({
57
+ request: {
58
+ requestId: '1',
59
+ sessionId: '2',
60
+ method: RpcMethod.ETH_SEND_TRANSACTION,
61
+ chainId: 'eip155:1',
62
+ dappInfo: { url: 'https://example.com', name: 'dapp', icon: 'icon' },
63
+ params: [testParams],
64
+ },
65
+ network: testNetwork,
66
+ approvalController: mockApprovalController,
67
+ });
68
+
69
+ const displayData = {
70
+ title: 'Approve Transaction',
71
+ network: {
72
+ chainId: testNetwork.chainId,
73
+ name: testNetwork.chainName,
74
+ logoUrl: testNetwork.logoUrl,
75
+ },
76
+ transactionDetails: {
77
+ website: 'example.com',
78
+ from: '0xfrom',
79
+ to: '0xto',
80
+ data: '0xdata',
81
+ },
82
+ networkFeeSelector: true,
83
+ };
84
+
85
+ const signingData = {
86
+ type: 'evm_transaction',
87
+ account: '0xfrom',
88
+ chainId: 1,
89
+ data: {
90
+ type: 2,
91
+ nonce: 12,
92
+ gasLimit: 21000,
93
+ to: '0xto',
94
+ from: '0xfrom',
95
+ data: '0xdata',
96
+ value: '0xvalue',
97
+ },
98
+ };
99
+
100
+ const testTxHash = '0xtxhash';
101
+
102
+ describe('eth_sendTransaction handler', () => {
103
+ beforeEach(() => {
104
+ jest.clearAllMocks();
105
+
106
+ mockParseRequestParams.mockReturnValue({
107
+ success: true,
108
+ data: [testParams],
109
+ });
110
+
111
+ mockApprovalController.requestApproval.mockResolvedValue({ result: testTxHash });
112
+ });
113
+
114
+ it('should return error if request params are invalid', async () => {
115
+ mockParseRequestParams.mockReturnValue({
116
+ success: false,
117
+ error: new Error('Invalid params') as ZodError,
118
+ });
119
+
120
+ const response = await ethSendTransaction(testRequestParams());
121
+
122
+ expect(response).toEqual({
123
+ error: rpcErrors.invalidParams('Transaction params are invalid'),
124
+ });
125
+ });
126
+
127
+ it('should calculate gas limit if not provided', async () => {
128
+ mockParseRequestParams.mockReturnValue({
129
+ success: true,
130
+ data: [{ from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue', nonce: '12' }],
131
+ });
132
+ mockEstimateGasLimit.mockResolvedValue(21000);
133
+
134
+ const requestParams = testRequestParams();
135
+
136
+ await ethSendTransaction(requestParams);
137
+
138
+ expect(mockGetProvider).toHaveBeenCalledWith({
139
+ chainId: 1,
140
+ chainName: 'chainName',
141
+ rpcUrl: 'rpcUrl',
142
+ multiContractAddress: 'multiContractAddress',
143
+ pollingInterval: 1000,
144
+ });
145
+
146
+ expect(mockEstimateGasLimit).toHaveBeenCalledWith({
147
+ provider: mockProvider,
148
+ transactionParams: { from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue' },
149
+ });
150
+
151
+ expect(mockApprovalController.requestApproval).toHaveBeenCalledWith({
152
+ request: requestParams.request,
153
+ displayData,
154
+ signingData,
155
+ });
156
+ });
157
+
158
+ it('should calculate nonce if not provided', async () => {
159
+ mockParseRequestParams.mockReturnValue({
160
+ success: true,
161
+ data: [{ from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue', gas: '0x5208' }],
162
+ });
163
+ mockGetNonce.mockResolvedValue(12);
164
+
165
+ const requestParams = testRequestParams();
166
+ await ethSendTransaction(requestParams);
167
+
168
+ expect(mockGetProvider).toHaveBeenCalledWith({
169
+ chainId: 1,
170
+ chainName: 'chainName',
171
+ rpcUrl: 'rpcUrl',
172
+ multiContractAddress: 'multiContractAddress',
173
+ pollingInterval: 1000,
174
+ });
175
+
176
+ expect(mockGetNonce).toHaveBeenCalledWith({
177
+ provider: mockProvider,
178
+ from: '0xfrom',
179
+ });
180
+
181
+ expect(mockApprovalController.requestApproval).toHaveBeenCalledWith({
182
+ request: requestParams.request,
183
+ displayData,
184
+ signingData,
185
+ });
186
+ });
187
+
188
+ it('should calculate both gas and nonce if not provided', async () => {
189
+ mockParseRequestParams.mockReturnValue({
190
+ success: true,
191
+ data: [{ from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue' }],
192
+ });
193
+ mockGetNonce.mockResolvedValue(12);
194
+ mockEstimateGasLimit.mockResolvedValue(21000);
195
+
196
+ const requestParams = testRequestParams();
197
+ await ethSendTransaction(requestParams);
198
+
199
+ expect(mockGetProvider).toHaveBeenCalledWith({
200
+ chainId: 1,
201
+ chainName: 'chainName',
202
+ rpcUrl: 'rpcUrl',
203
+ multiContractAddress: 'multiContractAddress',
204
+ pollingInterval: 1000,
205
+ });
206
+
207
+ expect(mockGetNonce).toHaveBeenCalledWith({
208
+ provider: mockProvider,
209
+ from: '0xfrom',
210
+ });
211
+
212
+ expect(mockEstimateGasLimit).toHaveBeenCalledWith({
213
+ provider: mockProvider,
214
+ transactionParams: { from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue' },
215
+ });
216
+
217
+ expect(mockApprovalController.requestApproval).toHaveBeenCalledWith({
218
+ request: requestParams.request,
219
+ displayData,
220
+ signingData,
221
+ });
222
+ });
223
+
224
+ it('should return error if gas limit calculation fails', async () => {
225
+ mockParseRequestParams.mockReturnValue({
226
+ success: true,
227
+ data: [{ from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue', nonce: '12' }],
228
+ });
229
+
230
+ mockEstimateGasLimit.mockRejectedValue(new Error('gas calculation error'));
231
+
232
+ const requestParams = testRequestParams();
233
+ const response = await ethSendTransaction(requestParams);
234
+
235
+ expect(response).toEqual({
236
+ error: rpcErrors.internal('Unable to calculate gas limit'),
237
+ });
238
+ });
239
+
240
+ it('should return error if nonce calculation fails', async () => {
241
+ mockParseRequestParams.mockReturnValue({
242
+ success: true,
243
+ data: [{ from: '0xfrom', to: '0xto', data: '0xdata', value: '0xvalue', gas: '0x5208' }],
244
+ });
245
+ mockGetNonce.mockRejectedValue(new Error('Nonce calculation error'));
246
+
247
+ const requestParams = testRequestParams();
248
+ const response = await ethSendTransaction(requestParams);
249
+
250
+ expect(response).toEqual({
251
+ error: rpcErrors.internal('Unable to calculate nonce'),
252
+ });
253
+ });
254
+
255
+ describe('approval succeeds', () => {
256
+ beforeEach(() => {
257
+ jest.clearAllMocks();
258
+
259
+ mockApprovalController.requestApproval.mockResolvedValue({ result: testTxHash });
260
+ mockSend.mockResolvedValue(testTxHash);
261
+ });
262
+
263
+ it('should broadcast the signed transaction and return transaction hash', async () => {
264
+ const requestParams = testRequestParams();
265
+ const response = await ethSendTransaction(requestParams);
266
+
267
+ expect(mockGetProvider).toHaveBeenCalledWith({
268
+ chainId: 1,
269
+ chainName: 'chainName',
270
+ rpcUrl: 'rpcUrl',
271
+ multiContractAddress: 'multiContractAddress',
272
+ pollingInterval: 1000,
273
+ });
274
+
275
+ expect(mockSend).toHaveBeenCalledWith('eth_sendRawTransaction', [testTxHash]);
276
+
277
+ expect(response).toStrictEqual({ result: testTxHash });
278
+ });
279
+
280
+ it('should notify when transaction is confirmed', async () => {
281
+ mockWaitForTransaction.mockResolvedValue({ status: 1 });
282
+
283
+ const requestParams = testRequestParams();
284
+ const response = await ethSendTransaction(requestParams);
285
+
286
+ expect(mockGetProvider).toHaveBeenCalledWith({
287
+ chainId: 1,
288
+ chainName: 'chainName',
289
+ rpcUrl: 'rpcUrl',
290
+ multiContractAddress: 'multiContractAddress',
291
+ pollingInterval: 1000,
292
+ });
293
+
294
+ expect(response).toStrictEqual({ result: testTxHash });
295
+
296
+ expect(mockWaitForTransaction).toHaveBeenCalledWith(testTxHash);
297
+
298
+ expect(mockOnTransactionConfirmed).toHaveBeenCalledWith(testTxHash);
299
+ });
300
+
301
+ it('should notify when transaction is reverted', async () => {
302
+ mockWaitForTransaction.mockResolvedValue({ status: 0 });
303
+
304
+ const requestParams = testRequestParams();
305
+ const response = await ethSendTransaction(requestParams);
306
+
307
+ expect(mockGetProvider).toHaveBeenCalledWith({
308
+ chainId: 1,
309
+ chainName: 'chainName',
310
+ rpcUrl: 'rpcUrl',
311
+ multiContractAddress: 'multiContractAddress',
312
+ pollingInterval: 1000,
313
+ });
314
+
315
+ expect(response).toStrictEqual({ result: testTxHash });
316
+
317
+ expect(mockWaitForTransaction).toHaveBeenCalledWith(testTxHash);
318
+
319
+ expect(mockOnTransactionReverted).toHaveBeenCalledWith(testTxHash);
320
+ });
321
+ });
322
+
323
+ describe('approval fails', () => {
324
+ it('should return error', async () => {
325
+ mockApprovalController.requestApproval.mockResolvedValue({ error: rpcErrors.internal('something went wrong') });
326
+
327
+ const requestParams = testRequestParams();
328
+ const response = await ethSendTransaction(requestParams);
329
+
330
+ expect(response).toStrictEqual({ error: rpcErrors.internal('something went wrong') });
331
+ });
332
+ });
333
+ });
@@ -0,0 +1,170 @@
1
+ import {
2
+ type Network,
3
+ type Hex,
4
+ type RpcRequest,
5
+ type ApprovalController,
6
+ type DisplayData,
7
+ type SigningData,
8
+ SigningDataType,
9
+ } from '@avalabs/vm-module-types';
10
+ import { parseRequestParams } from './schema';
11
+ import { estimateGasLimit } from '../../utils/estimate-gas-limit';
12
+ import { getNonce } from '../../utils/get-nonce';
13
+ import { rpcErrors } from '@metamask/rpc-errors';
14
+ import { getProvider } from '../../utils/get-provider';
15
+ import type { JsonRpcBatchInternal } from '@avalabs/wallets-sdk';
16
+
17
+ export const ethSendTransaction = async ({
18
+ request,
19
+ network,
20
+ approvalController,
21
+ }: {
22
+ request: RpcRequest;
23
+ network: Network;
24
+ approvalController: ApprovalController;
25
+ }) => {
26
+ const { dappInfo, params } = request;
27
+
28
+ // validate params
29
+ const result = parseRequestParams(params);
30
+
31
+ if (!result.success) {
32
+ console.error(result.error);
33
+ return {
34
+ error: rpcErrors.invalidParams('Transaction params are invalid'),
35
+ };
36
+ }
37
+
38
+ const transaction = result.data[0];
39
+
40
+ if (!transaction) {
41
+ return {
42
+ error: rpcErrors.invalidParams('Transaction params are invalid'),
43
+ };
44
+ }
45
+
46
+ const provider = getProvider({
47
+ chainId: network.chainId,
48
+ chainName: network.chainName,
49
+ rpcUrl: network.rpcUrl,
50
+ multiContractAddress: network.utilityAddresses?.multicall,
51
+ pollingInterval: 1000,
52
+ });
53
+
54
+ // calculate gas limit if not provided/invalid
55
+ if (!transaction.gas || Number(transaction.gas) < 0) {
56
+ try {
57
+ const gasLimit = await estimateGasLimit({
58
+ transactionParams: {
59
+ from: transaction.from,
60
+ to: transaction.to,
61
+ data: transaction.data,
62
+ value: transaction.value,
63
+ },
64
+ provider,
65
+ });
66
+
67
+ transaction.gas = '0x' + gasLimit.toString(16);
68
+ } catch (error) {
69
+ return {
70
+ error: rpcErrors.internal('Unable to calculate gas limit'),
71
+ };
72
+ }
73
+ }
74
+
75
+ // calculate nonce if not provided
76
+ if (!transaction.nonce) {
77
+ try {
78
+ const nonce = await getNonce({
79
+ from: transaction.from,
80
+ provider,
81
+ });
82
+ transaction.nonce = String(nonce);
83
+ } catch (error) {
84
+ return {
85
+ error: rpcErrors.internal('Unable to calculate nonce'),
86
+ };
87
+ }
88
+ }
89
+
90
+ // TODO: validate + simulate transaction
91
+ // https://ava-labs.atlassian.net/browse/CP-8870
92
+
93
+ // generate display and signing data
94
+ // TODO adjust title for different transaction types
95
+ // https://ava-labs.atlassian.net/browse/CP-8870
96
+ const displayData: DisplayData = {
97
+ title: 'Approve Transaction',
98
+ network: {
99
+ chainId: network.chainId,
100
+ name: network.chainName,
101
+ logoUrl: network.logoUrl,
102
+ },
103
+ transactionDetails: {
104
+ website: new URL(dappInfo.url).hostname,
105
+ from: transaction.from,
106
+ to: transaction.to,
107
+ data: transaction.data,
108
+ },
109
+ networkFeeSelector: true,
110
+ };
111
+
112
+ const signingData: SigningData = {
113
+ type: SigningDataType.EVM_TRANSACTION,
114
+ account: transaction.from,
115
+ chainId: network.chainId,
116
+ data: {
117
+ type: 2, // hardcoding to 2 for now as we only support EIP-1559
118
+ nonce: Number(transaction.nonce),
119
+ gasLimit: Number(transaction.gas),
120
+ to: transaction.to,
121
+ from: transaction.from,
122
+ data: transaction.data,
123
+ value: transaction.value,
124
+ chainId: transaction.chainId,
125
+ },
126
+ };
127
+
128
+ // prompt user for approval
129
+ const response = await approvalController.requestApproval({ request, displayData, signingData });
130
+
131
+ if ('error' in response) {
132
+ return {
133
+ error: response.error,
134
+ };
135
+ }
136
+
137
+ // broadcast the signed transaction
138
+ const txHash = await provider.send('eth_sendRawTransaction', [response.result]);
139
+
140
+ waitForTransactionReceipt({
141
+ provider,
142
+ txHash,
143
+ onTransactionConfirmed: approvalController.onTransactionConfirmed,
144
+ onTransactionReverted: approvalController.onTransactionReverted,
145
+ });
146
+
147
+ return { result: txHash };
148
+ };
149
+
150
+ const waitForTransactionReceipt = async ({
151
+ provider,
152
+ txHash,
153
+ onTransactionConfirmed,
154
+ onTransactionReverted,
155
+ }: {
156
+ provider: JsonRpcBatchInternal;
157
+ txHash: Hex;
158
+ onTransactionConfirmed: (txHash: Hex) => void;
159
+ onTransactionReverted: (txHash: Hex) => void;
160
+ }) => {
161
+ const receipt = await provider.waitForTransaction(txHash);
162
+
163
+ const success = receipt?.status === 1; // 1 indicates success, 0 indicates revert
164
+
165
+ if (success) {
166
+ onTransactionConfirmed(txHash);
167
+ } else {
168
+ onTransactionReverted(txHash);
169
+ }
170
+ };