@waiaas/adapter-evm 2.0.0-rc.1

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.
@@ -0,0 +1,783 @@
1
+ /**
2
+ * EvmAdapter -- IChainAdapter implementation for EVM chains using viem 2.x.
3
+ *
4
+ * Phase 77-01: Scaffolding with 6 real implementations.
5
+ * Phase 77-02: 11 more real implementations (build/simulate/sign/submit/confirm/fee/nonce/assets/tokenInfo/approve/txFee).
6
+ * Phase 78-02: buildTokenTransfer real implementation + getAssets ERC-20 multicall expansion.
7
+ * Phase 79-01: buildContractCall real implementation.
8
+ *
9
+ * Real implementations (21):
10
+ * connect, disconnect, isConnected, getHealth, getBalance, getCurrentNonce,
11
+ * buildTransaction, simulateTransaction, signTransaction, submitTransaction,
12
+ * waitForConfirmation, estimateFee, getTransactionFee, getAssets, getTokenInfo,
13
+ * buildApprove, buildBatch (BATCH_NOT_SUPPORTED), buildTokenTransfer, buildContractCall,
14
+ * parseTransaction, signExternalTransaction
15
+ *
16
+ * Stubs for later phases (1):
17
+ * sweepAll (Phase 80)
18
+ */
19
+ import { createPublicClient, http, serializeTransaction, parseTransaction as viemParseTransaction, encodeFunctionData, hexToBytes, toHex, } from 'viem';
20
+ import { privateKeyToAccount } from 'viem/accounts';
21
+ import { parseEvmTransaction } from './tx-parser.js';
22
+ import { WAIaaSError, ChainError } from '@waiaas/core';
23
+ import { ERC20_ABI } from './abi/erc20.js';
24
+ /** Gas safety margin multiplier: 1.2x (120/100). */
25
+ const GAS_SAFETY_NUMERATOR = 120n;
26
+ const GAS_SAFETY_DENOMINATOR = 100n;
27
+ /**
28
+ * EVM chain adapter implementing the 20-method IChainAdapter contract.
29
+ *
30
+ * Connection: connect, disconnect, isConnected, getHealth
31
+ * Balance: getBalance
32
+ * Pipeline: buildTransaction, simulateTransaction, signTransaction, submitTransaction
33
+ * Confirmation: waitForConfirmation
34
+ * Assets: getAssets
35
+ * Fee: estimateFee
36
+ * Token: buildTokenTransfer, getTokenInfo
37
+ * Contract: buildContractCall, buildApprove
38
+ * Batch: buildBatch
39
+ * Utility: getTransactionFee, getCurrentNonce, sweepAll
40
+ */
41
+ export class EvmAdapter {
42
+ chain = 'ethereum';
43
+ network;
44
+ _client = null;
45
+ _connected = false;
46
+ _chain;
47
+ _nativeSymbol;
48
+ _nativeName;
49
+ _allowedTokens = [];
50
+ constructor(network, chain, nativeSymbol = 'ETH', nativeName = 'Ether') {
51
+ this.network = network;
52
+ this._chain = chain;
53
+ this._nativeSymbol = nativeSymbol;
54
+ this._nativeName = nativeName;
55
+ }
56
+ /** Set the allowed tokens list for getAssets ERC-20 queries. */
57
+ setAllowedTokens(tokens) {
58
+ this._allowedTokens = tokens;
59
+ }
60
+ // -- Connection management (4) --
61
+ async connect(rpcUrl) {
62
+ this._client = createPublicClient({
63
+ transport: http(rpcUrl),
64
+ chain: this._chain,
65
+ });
66
+ this._connected = true;
67
+ }
68
+ async disconnect() {
69
+ this._client = null;
70
+ this._connected = false;
71
+ }
72
+ isConnected() {
73
+ return this._connected;
74
+ }
75
+ async getHealth() {
76
+ const client = this.getClient();
77
+ try {
78
+ const start = Date.now();
79
+ const blockNumber = await client.getBlockNumber();
80
+ const latencyMs = Date.now() - start;
81
+ return {
82
+ healthy: true,
83
+ latencyMs,
84
+ blockHeight: blockNumber,
85
+ };
86
+ }
87
+ catch {
88
+ return { healthy: false, latencyMs: 0 };
89
+ }
90
+ }
91
+ // -- Balance query (1) --
92
+ async getBalance(addr) {
93
+ const client = this.getClient();
94
+ try {
95
+ const balance = await client.getBalance({
96
+ address: addr,
97
+ });
98
+ return {
99
+ address: addr,
100
+ balance,
101
+ decimals: 18,
102
+ symbol: this._nativeSymbol,
103
+ };
104
+ }
105
+ catch (error) {
106
+ throw new WAIaaSError('CHAIN_ERROR', {
107
+ message: `Failed to get balance: ${error instanceof Error ? error.message : String(error)}`,
108
+ cause: error instanceof Error ? error : undefined,
109
+ });
110
+ }
111
+ }
112
+ // -- Asset query (1) --
113
+ async getAssets(addr) {
114
+ const client = this.getClient();
115
+ try {
116
+ // 1. Get native balance
117
+ const ethBalance = await client.getBalance({
118
+ address: addr,
119
+ });
120
+ const assets = [
121
+ {
122
+ mint: 'native',
123
+ symbol: this._nativeSymbol,
124
+ name: this._nativeName,
125
+ balance: ethBalance,
126
+ decimals: 18,
127
+ isNative: true,
128
+ },
129
+ ];
130
+ // 2. Query ERC-20 balances if allowedTokens configured
131
+ if (this._allowedTokens.length > 0) {
132
+ // Build multicall contracts array for balanceOf queries
133
+ const balanceContracts = this._allowedTokens.map(token => ({
134
+ address: token.address,
135
+ abi: ERC20_ABI,
136
+ functionName: 'balanceOf',
137
+ args: [addr],
138
+ }));
139
+ const results = await client.multicall({ contracts: balanceContracts });
140
+ // 3. Process results, skip failed calls and zero balances
141
+ for (let i = 0; i < results.length; i++) {
142
+ const result = results[i];
143
+ const tokenDef = this._allowedTokens[i];
144
+ if (result.status === 'success') {
145
+ const balance = result.result;
146
+ if (balance > 0n) {
147
+ assets.push({
148
+ mint: tokenDef.address,
149
+ symbol: tokenDef.symbol ?? '',
150
+ name: tokenDef.name ?? '',
151
+ balance,
152
+ decimals: tokenDef.decimals ?? 18,
153
+ isNative: false,
154
+ });
155
+ }
156
+ }
157
+ // Skip failed multicall results silently (token may not exist or revert)
158
+ }
159
+ // 4. Sort: native first (already first), then by balance descending
160
+ if (assets.length > 1) {
161
+ const native = assets[0];
162
+ const tokens = assets.slice(1).sort((a, b) => {
163
+ if (b.balance > a.balance)
164
+ return 1;
165
+ if (b.balance < a.balance)
166
+ return -1;
167
+ return a.symbol.localeCompare(b.symbol); // tie-break: alphabetical
168
+ });
169
+ return [native, ...tokens];
170
+ }
171
+ }
172
+ return assets;
173
+ }
174
+ catch (error) {
175
+ throw this.mapError(error, 'Failed to get assets');
176
+ }
177
+ }
178
+ // -- Transaction 4-stage pipeline (4) --
179
+ async buildTransaction(request) {
180
+ const client = this.getClient();
181
+ try {
182
+ const fromAddr = request.from;
183
+ const toAddr = request.to;
184
+ // 1. Get nonce
185
+ const nonce = await client.getTransactionCount({ address: fromAddr });
186
+ // 2. Get EIP-1559 fee data
187
+ const fees = await client.estimateFeesPerGas();
188
+ // 3. Estimate gas with 1.2x safety margin
189
+ const estimatedGas = await client.estimateGas({
190
+ account: fromAddr,
191
+ to: toAddr,
192
+ value: request.amount,
193
+ data: request.memo
194
+ ? `0x${Buffer.from(request.memo).toString('hex')}`
195
+ : undefined,
196
+ });
197
+ const gasLimit = (estimatedGas * GAS_SAFETY_NUMERATOR) / GAS_SAFETY_DENOMINATOR;
198
+ const maxFeePerGas = fees.maxFeePerGas;
199
+ const maxPriorityFeePerGas = fees.maxPriorityFeePerGas;
200
+ // 4. Build EIP-1559 transaction request
201
+ const chainId = client.chain?.id ?? 1;
202
+ const txRequest = {
203
+ type: 'eip1559',
204
+ to: toAddr,
205
+ value: request.amount,
206
+ nonce,
207
+ gas: gasLimit,
208
+ maxFeePerGas,
209
+ maxPriorityFeePerGas,
210
+ chainId,
211
+ data: request.memo
212
+ ? `0x${Buffer.from(request.memo).toString('hex')}`
213
+ : undefined,
214
+ };
215
+ // 5. Serialize transaction
216
+ const serializedHex = serializeTransaction(txRequest);
217
+ const serializedBytes = hexToBytes(serializedHex);
218
+ // 6. Calculate estimated fee
219
+ const estimatedFee = gasLimit * maxFeePerGas;
220
+ return {
221
+ chain: 'ethereum',
222
+ serialized: serializedBytes,
223
+ estimatedFee,
224
+ expiresAt: undefined, // EVM uses nonce, no expiry
225
+ metadata: {
226
+ from: request.from,
227
+ nonce,
228
+ chainId,
229
+ maxFeePerGas,
230
+ maxPriorityFeePerGas,
231
+ gasLimit,
232
+ type: 'eip1559',
233
+ },
234
+ nonce,
235
+ };
236
+ }
237
+ catch (error) {
238
+ if (error instanceof ChainError || error instanceof WAIaaSError)
239
+ throw error;
240
+ const msg = error instanceof Error ? error.message : String(error);
241
+ if (msg.toLowerCase().includes('insufficient funds')) {
242
+ throw new ChainError('INSUFFICIENT_BALANCE', 'evm', {
243
+ message: `Insufficient funds for transfer: ${msg}`,
244
+ cause: error instanceof Error ? error : undefined,
245
+ });
246
+ }
247
+ if (msg.toLowerCase().includes('nonce too low')) {
248
+ throw new ChainError('NONCE_TOO_LOW', 'evm', {
249
+ message: `Nonce too low: ${msg}`,
250
+ cause: error instanceof Error ? error : undefined,
251
+ });
252
+ }
253
+ throw this.mapError(error, 'Failed to build transaction');
254
+ }
255
+ }
256
+ async simulateTransaction(tx) {
257
+ const client = this.getClient();
258
+ try {
259
+ // Deserialize the tx from serialized bytes back to tx params
260
+ const serializedHex = toHex(tx.serialized);
261
+ const parsed = viemParseTransaction(serializedHex);
262
+ // Use client.call() to simulate via eth_call
263
+ await client.call({
264
+ to: parsed.to,
265
+ value: parsed.value,
266
+ data: parsed.data,
267
+ account: tx.metadata.from,
268
+ });
269
+ return {
270
+ success: true,
271
+ logs: [],
272
+ unitsConsumed: tx.metadata.gasLimit != null ? BigInt(tx.metadata.gasLimit) : undefined,
273
+ };
274
+ }
275
+ catch (error) {
276
+ const msg = error instanceof Error ? error.message : String(error);
277
+ return {
278
+ success: false,
279
+ logs: [],
280
+ error: msg,
281
+ };
282
+ }
283
+ }
284
+ async signTransaction(tx, privateKey) {
285
+ this.ensureConnected();
286
+ try {
287
+ // Convert private key bytes to hex
288
+ const privateKeyHex = `0x${Buffer.from(privateKey).toString('hex')}`;
289
+ // Create account from private key
290
+ const account = privateKeyToAccount(privateKeyHex);
291
+ // Deserialize tx bytes back to tx object
292
+ const serializedHex = toHex(tx.serialized);
293
+ const parsed = viemParseTransaction(serializedHex);
294
+ // Sign the transaction
295
+ const signedHex = await account.signTransaction({
296
+ ...parsed,
297
+ type: 'eip1559',
298
+ });
299
+ // Convert signed hex to Uint8Array
300
+ return hexToBytes(signedHex);
301
+ }
302
+ catch (error) {
303
+ if (error instanceof ChainError || error instanceof WAIaaSError)
304
+ throw error;
305
+ throw this.mapError(error, 'Failed to sign transaction');
306
+ }
307
+ }
308
+ async submitTransaction(signedTx) {
309
+ const client = this.getClient();
310
+ try {
311
+ // Convert bytes to hex
312
+ const hex = toHex(signedTx);
313
+ // Submit via eth_sendRawTransaction
314
+ const txHash = await client.sendRawTransaction({
315
+ serializedTransaction: hex,
316
+ });
317
+ return {
318
+ txHash,
319
+ status: 'submitted',
320
+ };
321
+ }
322
+ catch (error) {
323
+ if (error instanceof ChainError || error instanceof WAIaaSError)
324
+ throw error;
325
+ const msg = error instanceof Error ? error.message : String(error);
326
+ if (msg.toLowerCase().includes('nonce') && msg.toLowerCase().includes('already')) {
327
+ throw new ChainError('NONCE_ALREADY_USED', 'evm', {
328
+ message: `Nonce already used: ${msg}`,
329
+ cause: error instanceof Error ? error : undefined,
330
+ });
331
+ }
332
+ throw this.mapError(error, 'Failed to submit transaction');
333
+ }
334
+ }
335
+ // -- Confirmation wait (1) --
336
+ async waitForConfirmation(txHash, timeoutMs = 30_000) {
337
+ const client = this.getClient();
338
+ try {
339
+ const receipt = await client.waitForTransactionReceipt({
340
+ hash: txHash,
341
+ timeout: timeoutMs,
342
+ });
343
+ return {
344
+ txHash,
345
+ status: receipt.status === 'success' ? 'confirmed' : 'failed',
346
+ blockNumber: receipt.blockNumber,
347
+ fee: receipt.gasUsed * receipt.effectiveGasPrice,
348
+ };
349
+ }
350
+ catch {
351
+ // Timeout or RPC error: fallback to direct receipt query
352
+ try {
353
+ const receipt = await client.getTransactionReceipt({
354
+ hash: txHash,
355
+ });
356
+ return {
357
+ txHash,
358
+ status: receipt.status === 'success' ? 'confirmed' : 'failed',
359
+ blockNumber: receipt.blockNumber,
360
+ fee: receipt.gasUsed * receipt.effectiveGasPrice,
361
+ };
362
+ }
363
+ catch {
364
+ // Receipt not found: tx still pending
365
+ return { txHash, status: 'submitted' };
366
+ }
367
+ }
368
+ }
369
+ // -- Fee estimation (1) --
370
+ async estimateFee(request) {
371
+ const client = this.getClient();
372
+ try {
373
+ // Get EIP-1559 fee data
374
+ const fees = await client.estimateFeesPerGas();
375
+ // Determine gas estimate based on request type
376
+ let gasEstimateParams;
377
+ if ('token' in request) {
378
+ // TokenTransferParams: estimate for ERC-20 transfer calldata
379
+ const tokenRequest = request;
380
+ const transferData = encodeFunctionData({
381
+ abi: ERC20_ABI,
382
+ functionName: 'transfer',
383
+ args: [tokenRequest.to, tokenRequest.amount],
384
+ });
385
+ gasEstimateParams = {
386
+ account: tokenRequest.from,
387
+ to: tokenRequest.token.address,
388
+ data: transferData,
389
+ };
390
+ }
391
+ else {
392
+ // TransferRequest: native transfer
393
+ gasEstimateParams = {
394
+ account: request.from,
395
+ to: request.to,
396
+ value: request.amount,
397
+ };
398
+ }
399
+ const estimatedGas = await client.estimateGas(gasEstimateParams);
400
+ const gasLimit = (estimatedGas * GAS_SAFETY_NUMERATOR) / GAS_SAFETY_DENOMINATOR;
401
+ const maxFeePerGas = fees.maxFeePerGas;
402
+ const maxPriorityFeePerGas = fees.maxPriorityFeePerGas;
403
+ const fee = gasLimit * maxFeePerGas;
404
+ return {
405
+ fee,
406
+ details: {
407
+ gasLimit,
408
+ maxFeePerGas,
409
+ maxPriorityFeePerGas,
410
+ },
411
+ };
412
+ }
413
+ catch (error) {
414
+ if (error instanceof ChainError || error instanceof WAIaaSError)
415
+ throw error;
416
+ throw this.mapError(error, 'Failed to estimate fee');
417
+ }
418
+ }
419
+ // -- Token operations (2) --
420
+ async buildTokenTransfer(request) {
421
+ const client = this.getClient();
422
+ try {
423
+ const fromAddr = request.from;
424
+ const tokenAddr = request.token.address;
425
+ const toAddr = request.to;
426
+ // 1. Encode ERC-20 transfer(address,uint256) calldata
427
+ const transferData = encodeFunctionData({
428
+ abi: ERC20_ABI,
429
+ functionName: 'transfer',
430
+ args: [toAddr, request.amount],
431
+ });
432
+ // 2. Get nonce
433
+ const nonce = await client.getTransactionCount({ address: fromAddr });
434
+ // 3. Get EIP-1559 fee data
435
+ const fees = await client.estimateFeesPerGas();
436
+ // 4. Estimate gas with 1.2x safety margin
437
+ const estimatedGas = await client.estimateGas({
438
+ account: fromAddr,
439
+ to: tokenAddr, // tx target is the TOKEN CONTRACT, not the recipient
440
+ data: transferData,
441
+ });
442
+ const gasLimit = (estimatedGas * GAS_SAFETY_NUMERATOR) / GAS_SAFETY_DENOMINATOR;
443
+ const maxFeePerGas = fees.maxFeePerGas;
444
+ const maxPriorityFeePerGas = fees.maxPriorityFeePerGas;
445
+ const chainId = client.chain?.id ?? 1;
446
+ // 5. Build EIP-1559 tx to token contract with transfer calldata, value=0
447
+ const txRequest = {
448
+ type: 'eip1559',
449
+ to: tokenAddr, // target is token contract
450
+ value: 0n, // no ETH value for ERC-20 transfer
451
+ nonce,
452
+ gas: gasLimit,
453
+ maxFeePerGas,
454
+ maxPriorityFeePerGas,
455
+ chainId,
456
+ data: transferData,
457
+ };
458
+ // 6. Serialize
459
+ const serializedHex = serializeTransaction(txRequest);
460
+ const serializedBytes = hexToBytes(serializedHex);
461
+ const estimatedFee = gasLimit * maxFeePerGas;
462
+ return {
463
+ chain: 'ethereum',
464
+ serialized: serializedBytes,
465
+ estimatedFee,
466
+ expiresAt: undefined, // EVM uses nonce, no expiry
467
+ metadata: {
468
+ from: request.from,
469
+ nonce,
470
+ chainId,
471
+ maxFeePerGas,
472
+ maxPriorityFeePerGas,
473
+ gasLimit,
474
+ type: 'eip1559',
475
+ tokenAddress: request.token.address,
476
+ recipient: request.to,
477
+ tokenAmount: request.amount,
478
+ },
479
+ nonce,
480
+ };
481
+ }
482
+ catch (error) {
483
+ if (error instanceof ChainError || error instanceof WAIaaSError)
484
+ throw error;
485
+ throw this.mapError(error, 'Failed to build token transfer');
486
+ }
487
+ }
488
+ async getTokenInfo(tokenAddress) {
489
+ const client = this.getClient();
490
+ try {
491
+ const contractAddr = tokenAddress;
492
+ // Use multicall to batch decimals, symbol, name in a single RPC
493
+ const results = await client.multicall({
494
+ contracts: [
495
+ { address: contractAddr, abi: ERC20_ABI, functionName: 'decimals' },
496
+ { address: contractAddr, abi: ERC20_ABI, functionName: 'symbol' },
497
+ { address: contractAddr, abi: ERC20_ABI, functionName: 'name' },
498
+ ],
499
+ });
500
+ // Extract results with defaults for failed calls
501
+ const decimals = results[0].status === 'success' ? Number(results[0].result) : 18;
502
+ const symbol = results[1].status === 'success' ? String(results[1].result) : '';
503
+ const name = results[2].status === 'success' ? String(results[2].result) : '';
504
+ return {
505
+ address: tokenAddress,
506
+ symbol,
507
+ name,
508
+ decimals,
509
+ };
510
+ }
511
+ catch (error) {
512
+ if (error instanceof ChainError || error instanceof WAIaaSError)
513
+ throw error;
514
+ throw this.mapError(error, 'Failed to get token info');
515
+ }
516
+ }
517
+ // -- Contract operations (2) --
518
+ async buildContractCall(request) {
519
+ const client = this.getClient();
520
+ try {
521
+ const fromAddr = request.from;
522
+ const toAddr = request.to;
523
+ // Validate calldata: must be hex string with 0x prefix + at least 4-byte selector (8 hex chars)
524
+ if (!request.calldata || !/^0x[0-9a-fA-F]{8,}$/.test(request.calldata)) {
525
+ throw new ChainError('INVALID_INSTRUCTION', 'evm', {
526
+ message: 'Invalid calldata: must be hex string with 0x prefix and at least 4-byte function selector',
527
+ });
528
+ }
529
+ const calldata = request.calldata;
530
+ // 1. Get nonce
531
+ const nonce = await client.getTransactionCount({ address: fromAddr });
532
+ // 2. Get EIP-1559 fee data
533
+ const fees = await client.estimateFeesPerGas();
534
+ // 3. Estimate gas with 1.2x safety margin
535
+ const estimatedGas = await client.estimateGas({
536
+ account: fromAddr,
537
+ to: toAddr,
538
+ data: calldata,
539
+ value: request.value ?? 0n,
540
+ });
541
+ const gasLimit = (estimatedGas * GAS_SAFETY_NUMERATOR) / GAS_SAFETY_DENOMINATOR;
542
+ const maxFeePerGas = fees.maxFeePerGas;
543
+ const maxPriorityFeePerGas = fees.maxPriorityFeePerGas;
544
+ const chainId = client.chain?.id ?? 1;
545
+ // 4. Build EIP-1559 tx targeting the contract address with calldata
546
+ const txRequest = {
547
+ type: 'eip1559',
548
+ to: toAddr,
549
+ value: request.value ?? 0n,
550
+ nonce,
551
+ gas: gasLimit,
552
+ maxFeePerGas,
553
+ maxPriorityFeePerGas,
554
+ chainId,
555
+ data: calldata,
556
+ };
557
+ // 5. Serialize
558
+ const serializedHex = serializeTransaction(txRequest);
559
+ const serializedBytes = hexToBytes(serializedHex);
560
+ const estimatedFee = gasLimit * maxFeePerGas;
561
+ // 6. Extract function selector (first 10 chars: 0x + 4-byte selector)
562
+ const selector = calldata.slice(0, 10);
563
+ return {
564
+ chain: 'ethereum',
565
+ serialized: serializedBytes,
566
+ estimatedFee,
567
+ expiresAt: undefined,
568
+ metadata: {
569
+ from: request.from,
570
+ nonce,
571
+ chainId,
572
+ maxFeePerGas,
573
+ maxPriorityFeePerGas,
574
+ gasLimit,
575
+ type: 'eip1559',
576
+ selector,
577
+ contractAddress: request.to,
578
+ value: request.value ?? 0n,
579
+ },
580
+ nonce,
581
+ };
582
+ }
583
+ catch (error) {
584
+ if (error instanceof ChainError || error instanceof WAIaaSError)
585
+ throw error;
586
+ const msg = error instanceof Error ? error.message : String(error);
587
+ if (msg.toLowerCase().includes('insufficient funds')) {
588
+ throw new ChainError('INSUFFICIENT_BALANCE', 'evm', {
589
+ message: `Insufficient funds for contract call: ${msg}`,
590
+ cause: error instanceof Error ? error : undefined,
591
+ });
592
+ }
593
+ throw this.mapError(error, 'Failed to build contract call');
594
+ }
595
+ }
596
+ async buildApprove(request) {
597
+ const client = this.getClient();
598
+ try {
599
+ const fromAddr = request.from;
600
+ const tokenAddr = request.token.address;
601
+ const spenderAddr = request.spender;
602
+ // 1. Encode approve calldata
603
+ const approveData = encodeFunctionData({
604
+ abi: ERC20_ABI,
605
+ functionName: 'approve',
606
+ args: [spenderAddr, request.amount],
607
+ });
608
+ // 2. Get nonce
609
+ const nonce = await client.getTransactionCount({ address: fromAddr });
610
+ // 3. Get EIP-1559 fee data
611
+ const fees = await client.estimateFeesPerGas();
612
+ // 4. Estimate gas for approve call
613
+ const estimatedGas = await client.estimateGas({
614
+ account: fromAddr,
615
+ to: tokenAddr,
616
+ data: approveData,
617
+ });
618
+ const gasLimit = (estimatedGas * GAS_SAFETY_NUMERATOR) / GAS_SAFETY_DENOMINATOR;
619
+ const maxFeePerGas = fees.maxFeePerGas;
620
+ const maxPriorityFeePerGas = fees.maxPriorityFeePerGas;
621
+ const chainId = client.chain?.id ?? 1;
622
+ // 5. Build EIP-1559 tx to token contract with approve calldata, value=0
623
+ const txRequest = {
624
+ type: 'eip1559',
625
+ to: tokenAddr,
626
+ value: 0n,
627
+ nonce,
628
+ gas: gasLimit,
629
+ maxFeePerGas,
630
+ maxPriorityFeePerGas,
631
+ chainId,
632
+ data: approveData,
633
+ };
634
+ // 6. Serialize
635
+ const serializedHex = serializeTransaction(txRequest);
636
+ const serializedBytes = hexToBytes(serializedHex);
637
+ const estimatedFee = gasLimit * maxFeePerGas;
638
+ return {
639
+ chain: 'ethereum',
640
+ serialized: serializedBytes,
641
+ estimatedFee,
642
+ expiresAt: undefined,
643
+ metadata: {
644
+ from: request.from,
645
+ nonce,
646
+ chainId,
647
+ maxFeePerGas,
648
+ maxPriorityFeePerGas,
649
+ gasLimit,
650
+ type: 'eip1559',
651
+ tokenAddress: request.token.address,
652
+ spender: request.spender,
653
+ approveAmount: request.amount,
654
+ },
655
+ nonce,
656
+ };
657
+ }
658
+ catch (error) {
659
+ if (error instanceof ChainError || error instanceof WAIaaSError)
660
+ throw error;
661
+ throw this.mapError(error, 'Failed to build approve transaction');
662
+ }
663
+ }
664
+ // -- Batch operations (1) --
665
+ async buildBatch(_request) {
666
+ throw new WAIaaSError('BATCH_NOT_SUPPORTED', {
667
+ message: 'EVM does not support atomic batch transactions. Use Account Abstraction for batching.',
668
+ });
669
+ }
670
+ // -- Utility operations (3) --
671
+ async getTransactionFee(tx) {
672
+ // Extract gasLimit and maxFeePerGas from metadata
673
+ const metadata = tx.metadata;
674
+ if (metadata.gasLimit != null && metadata.maxFeePerGas != null) {
675
+ return BigInt(metadata.gasLimit) * BigInt(metadata.maxFeePerGas);
676
+ }
677
+ // Fallback to estimatedFee
678
+ return tx.estimatedFee;
679
+ }
680
+ async getCurrentNonce(addr) {
681
+ const client = this.getClient();
682
+ try {
683
+ const nonce = await client.getTransactionCount({
684
+ address: addr,
685
+ });
686
+ return nonce;
687
+ }
688
+ catch (error) {
689
+ throw new WAIaaSError('CHAIN_ERROR', {
690
+ message: `Failed to get nonce: ${error instanceof Error ? error.message : String(error)}`,
691
+ cause: error instanceof Error ? error : undefined,
692
+ });
693
+ }
694
+ }
695
+ async sweepAll(_from, _to, _privateKey) {
696
+ throw new Error('Not implemented: sweepAll will be implemented in Phase 80');
697
+ }
698
+ // -- Sign-only operations (2) -- v1.4.7
699
+ async parseTransaction(rawTx) {
700
+ return parseEvmTransaction(rawTx);
701
+ }
702
+ async signExternalTransaction(rawTx, privateKey) {
703
+ try {
704
+ // Convert private key bytes to hex
705
+ const privateKeyHex = `0x${Buffer.from(privateKey).toString('hex')}`;
706
+ // Create account from private key
707
+ const account = privateKeyToAccount(privateKeyHex);
708
+ // Parse the raw unsigned tx
709
+ let parsed;
710
+ try {
711
+ parsed = viemParseTransaction(rawTx);
712
+ }
713
+ catch {
714
+ throw new ChainError('INVALID_RAW_TRANSACTION', 'evm', {
715
+ message: 'Failed to parse unsigned transaction for signing',
716
+ });
717
+ }
718
+ // Sign the transaction
719
+ const signedHex = await account.signTransaction({
720
+ ...parsed,
721
+ type: parsed.type ?? 'eip1559',
722
+ });
723
+ return { signedTransaction: signedHex };
724
+ }
725
+ catch (error) {
726
+ if (error instanceof ChainError)
727
+ throw error;
728
+ throw new ChainError('INVALID_RAW_TRANSACTION', 'evm', {
729
+ message: `Failed to sign external transaction: ${error instanceof Error ? error.message : String(error)}`,
730
+ cause: error instanceof Error ? error : undefined,
731
+ });
732
+ }
733
+ }
734
+ // -- Private helpers --
735
+ ensureConnected() {
736
+ if (!this._connected || !this._client) {
737
+ throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
738
+ message: 'EvmAdapter is not connected. Call connect() first.',
739
+ });
740
+ }
741
+ }
742
+ getClient() {
743
+ this.ensureConnected();
744
+ return this._client;
745
+ }
746
+ /**
747
+ * Map unknown errors to appropriate ChainError or WAIaaSError.
748
+ * Inspects error message for known patterns.
749
+ */
750
+ mapError(error, context) {
751
+ const msg = error instanceof Error ? error.message : String(error);
752
+ const lowerMsg = msg.toLowerCase();
753
+ if (lowerMsg.includes('insufficient funds') || lowerMsg.includes('insufficient balance')) {
754
+ return new ChainError('INSUFFICIENT_BALANCE', 'evm', {
755
+ message: `${context}: ${msg}`,
756
+ cause: error instanceof Error ? error : undefined,
757
+ });
758
+ }
759
+ if (lowerMsg.includes('nonce too low')) {
760
+ return new ChainError('NONCE_TOO_LOW', 'evm', {
761
+ message: `${context}: ${msg}`,
762
+ cause: error instanceof Error ? error : undefined,
763
+ });
764
+ }
765
+ if (lowerMsg.includes('connection') || lowerMsg.includes('econnrefused') || lowerMsg.includes('fetch failed')) {
766
+ return new ChainError('RPC_CONNECTION_ERROR', 'evm', {
767
+ message: `${context}: ${msg}`,
768
+ cause: error instanceof Error ? error : undefined,
769
+ });
770
+ }
771
+ if (lowerMsg.includes('timeout') || lowerMsg.includes('timed out')) {
772
+ return new ChainError('RPC_TIMEOUT', 'evm', {
773
+ message: `${context}: ${msg}`,
774
+ cause: error instanceof Error ? error : undefined,
775
+ });
776
+ }
777
+ return new WAIaaSError('CHAIN_ERROR', {
778
+ message: `${context}: ${msg}`,
779
+ cause: error instanceof Error ? error : undefined,
780
+ });
781
+ }
782
+ }
783
+ //# sourceMappingURL=adapter.js.map