@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.
- package/dist/abi/erc20.d.ts +101 -0
- package/dist/abi/erc20.d.ts.map +1 -0
- package/dist/abi/erc20.js +74 -0
- package/dist/abi/erc20.js.map +1 -0
- package/dist/adapter.d.ts +82 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +783 -0
- package/dist/adapter.js.map +1 -0
- package/dist/evm-chain-map.d.ts +10 -0
- package/dist/evm-chain-map.d.ts.map +1 -0
- package/dist/evm-chain-map.js +14 -0
- package/dist/evm-chain-map.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/tx-parser.d.ts +27 -0
- package/dist/tx-parser.d.ts.map +1 -0
- package/dist/tx-parser.js +89 -0
- package/dist/tx-parser.js.map +1 -0
- package/package.json +46 -0
package/dist/adapter.js
ADDED
|
@@ -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
|