@etherkit/viem-tx-tracker 0.0.4 → 0.0.6
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/TrackedWalletClient.d.ts +34 -3
- package/dist/TrackedWalletClient.d.ts.map +1 -1
- package/dist/TrackedWalletClient.js +433 -97
- package/dist/TrackedWalletClient.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +257 -38
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrackedWalletClient.ts +785 -112
- package/src/index.ts +1 -0
- package/src/types.ts +407 -38
|
@@ -24,38 +24,189 @@ function normalizeAccount(account) {
|
|
|
24
24
|
return account === null ? undefined : account;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Infer transaction type from provided params.
|
|
28
|
+
* Returns undefined if can't be determined (wallet will decide).
|
|
28
29
|
*/
|
|
29
|
-
function
|
|
30
|
-
|
|
30
|
+
function inferTxType(params) {
|
|
31
|
+
if (params.maxFeePerGas !== undefined) {
|
|
32
|
+
return 'eip1559';
|
|
33
|
+
}
|
|
34
|
+
if (params.gasPrice !== undefined && params.accessList !== undefined) {
|
|
35
|
+
return 'eip2930';
|
|
36
|
+
}
|
|
37
|
+
if (params.gasPrice !== undefined) {
|
|
38
|
+
return 'legacy';
|
|
39
|
+
}
|
|
40
|
+
return undefined; // Wallet will determine
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
33
|
-
* Create
|
|
34
|
-
*
|
|
35
|
-
* The tracked client provides the same API as WalletClient but with:
|
|
36
|
-
* - Metadata field for transaction tracking (required unless TMetadata includes undefined)
|
|
37
|
-
* - Automatic nonce fetching (with 'pending' by default)
|
|
38
|
-
* - Post-broadcast transaction verification
|
|
39
|
-
* - Event emission for tracking
|
|
40
|
-
*
|
|
41
|
-
* @typeParam TMetadata - The metadata type. Use `MyMeta | undefined` to make metadata optional.
|
|
42
|
-
* @returns A builder with a `.using()` method to provide the wallet and public clients
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```typescript
|
|
46
|
-
* // With required metadata
|
|
47
|
-
* const tracked = createTrackedWalletClient<{purpose: string}>()
|
|
48
|
-
* .using(walletClient, publicClient);
|
|
49
|
-
*
|
|
50
|
-
* // With optional metadata
|
|
51
|
-
* const tracked = createTrackedWalletClient<{purpose: string} | undefined>()
|
|
52
|
-
* .using(walletClient, publicClient);
|
|
53
|
-
* ```
|
|
43
|
+
* Create an UnknownTrackedTransaction for immediate emission.
|
|
44
|
+
* Populates all known intended values from the transaction parameters.
|
|
54
45
|
*/
|
|
55
|
-
|
|
46
|
+
function createUnknownTrackedTransaction(hash, from, nonce, chainId, metadata, broadcastTimestampMs, params) {
|
|
47
|
+
const txType = inferTxType(params);
|
|
48
|
+
return {
|
|
49
|
+
known: false,
|
|
50
|
+
chainId,
|
|
51
|
+
hash,
|
|
52
|
+
from,
|
|
53
|
+
nonce,
|
|
54
|
+
broadcastTimestampMs,
|
|
55
|
+
metadata,
|
|
56
|
+
...(txType !== undefined && { txType }),
|
|
57
|
+
...(params.to !== undefined && { to: params.to }),
|
|
58
|
+
...(params.value !== undefined && { value: params.value }),
|
|
59
|
+
...(params.data !== undefined && { data: params.data }),
|
|
60
|
+
...(params.gas !== undefined && { gas: params.gas }),
|
|
61
|
+
...(params.gasPrice !== undefined && { gasPrice: params.gasPrice }),
|
|
62
|
+
...(params.maxFeePerGas !== undefined && {
|
|
63
|
+
maxFeePerGas: params.maxFeePerGas,
|
|
64
|
+
}),
|
|
65
|
+
...(params.maxPriorityFeePerGas !== undefined && {
|
|
66
|
+
maxPriorityFeePerGas: params.maxPriorityFeePerGas,
|
|
67
|
+
}),
|
|
68
|
+
...(params.accessList !== undefined && { accessList: params.accessList }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract transaction type-specific fields from a fetched transaction.
|
|
73
|
+
*/
|
|
74
|
+
function extractTransactionTypeFields(tx) {
|
|
75
|
+
if (tx.type === 'eip1559') {
|
|
76
|
+
return {
|
|
77
|
+
txType: 'eip1559',
|
|
78
|
+
maxFeePerGas: tx.maxFeePerGas,
|
|
79
|
+
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
|
|
80
|
+
...(tx.accessList && { accessList: tx.accessList }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
else if (tx.type === 'eip2930') {
|
|
84
|
+
return {
|
|
85
|
+
txType: 'eip2930',
|
|
86
|
+
gasPrice: tx.gasPrice,
|
|
87
|
+
accessList: (tx.accessList ?? []),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Legacy or unknown - treat as legacy
|
|
92
|
+
return {
|
|
93
|
+
txType: 'legacy',
|
|
94
|
+
gasPrice: tx.gasPrice,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create a KnownTrackedTransaction from a fetched transaction.
|
|
100
|
+
*/
|
|
101
|
+
function createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs) {
|
|
102
|
+
const typeFields = extractTransactionTypeFields(tx);
|
|
103
|
+
return {
|
|
104
|
+
known: true,
|
|
105
|
+
chainId: tx.chainId,
|
|
106
|
+
hash: tx.hash,
|
|
107
|
+
from: tx.from,
|
|
108
|
+
to: tx.to,
|
|
109
|
+
nonce: tx.nonce,
|
|
110
|
+
value: tx.value,
|
|
111
|
+
data: tx.input,
|
|
112
|
+
gas: tx.gas,
|
|
113
|
+
broadcastTimestampMs,
|
|
114
|
+
metadata,
|
|
115
|
+
...typeFields,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create a KnownTrackedTransaction from a parsed raw transaction.
|
|
120
|
+
*/
|
|
121
|
+
function createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, chainId, broadcastTimestampMs) {
|
|
122
|
+
// Determine transaction type from parsed tx
|
|
123
|
+
let typeFields;
|
|
124
|
+
if ('maxFeePerGas' in parsedTx && parsedTx.maxFeePerGas !== undefined) {
|
|
125
|
+
typeFields = {
|
|
126
|
+
txType: 'eip1559',
|
|
127
|
+
maxFeePerGas: parsedTx.maxFeePerGas,
|
|
128
|
+
maxPriorityFeePerGas: parsedTx.maxPriorityFeePerGas,
|
|
129
|
+
...('accessList' in parsedTx &&
|
|
130
|
+
parsedTx.accessList && {
|
|
131
|
+
accessList: parsedTx.accessList,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
else if ('accessList' in parsedTx && parsedTx.accessList) {
|
|
136
|
+
typeFields = {
|
|
137
|
+
txType: 'eip2930',
|
|
138
|
+
gasPrice: parsedTx.gasPrice,
|
|
139
|
+
accessList: parsedTx.accessList,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
typeFields = {
|
|
144
|
+
txType: 'legacy',
|
|
145
|
+
gasPrice: parsedTx.gasPrice,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
known: true,
|
|
150
|
+
chainId: parsedTx.chainId ?? chainId,
|
|
151
|
+
hash,
|
|
152
|
+
from,
|
|
153
|
+
to: parsedTx.to ?? null,
|
|
154
|
+
nonce: parsedTx.nonce,
|
|
155
|
+
value: parsedTx.value ?? 0n,
|
|
156
|
+
data: parsedTx.data ?? '0x',
|
|
157
|
+
gas: parsedTx.gas,
|
|
158
|
+
broadcastTimestampMs,
|
|
159
|
+
metadata,
|
|
160
|
+
...typeFields,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extract intended params from sendTransaction args.
|
|
165
|
+
* Uses 'unknown' for 'to' since viem's SendTransactionParameters has a complex type for it.
|
|
166
|
+
*/
|
|
167
|
+
function extractIntendedParamsFromSendTransaction(args) {
|
|
168
|
+
// Normalize 'to' to either a hex address, null, or undefined
|
|
169
|
+
const to = args.to === null || args.to === undefined
|
|
170
|
+
? args.to
|
|
171
|
+
: typeof args.to === 'string'
|
|
172
|
+
? args.to
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
to,
|
|
176
|
+
value: args.value ?? 0n,
|
|
177
|
+
data: args.data,
|
|
178
|
+
gas: args.gas,
|
|
179
|
+
gasPrice: args.gasPrice,
|
|
180
|
+
maxFeePerGas: args.maxFeePerGas,
|
|
181
|
+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
|
|
182
|
+
accessList: args.accessList,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Extract intended params from writeContract args.
|
|
187
|
+
*/
|
|
188
|
+
function extractIntendedParamsFromWriteContract(args) {
|
|
189
|
+
return {
|
|
190
|
+
to: args.address,
|
|
191
|
+
value: args.value ?? 0n,
|
|
192
|
+
// Note: data could be encoded using encodeFunctionData(args) if desired
|
|
193
|
+
gas: args.gas,
|
|
194
|
+
gasPrice: args.gasPrice,
|
|
195
|
+
maxFeePerGas: args.maxFeePerGas,
|
|
196
|
+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
|
|
197
|
+
accessList: args.accessList,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Implementation
|
|
201
|
+
export function createTrackedWalletClient(options) {
|
|
202
|
+
const populateMetadata = options?.populateMetadata ?? false;
|
|
203
|
+
const clock = options?.clock ?? Date.now;
|
|
204
|
+
if (populateMetadata) {
|
|
205
|
+
return createAutoPopulateBuilder(clock);
|
|
206
|
+
}
|
|
56
207
|
return {
|
|
57
208
|
using(walletClient, publicClient) {
|
|
58
|
-
// Create emitter for transaction
|
|
209
|
+
// Create emitter for transaction events
|
|
59
210
|
const emitter = new Emitter();
|
|
60
211
|
/**
|
|
61
212
|
* Resolve the nonce to use for a transaction.
|
|
@@ -96,104 +247,263 @@ export function createTrackedWalletClient() {
|
|
|
96
247
|
return { from, intendedNonce };
|
|
97
248
|
}
|
|
98
249
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
250
|
+
* Fetch full transaction data and emit transaction:fetched event.
|
|
251
|
+
* Non-blocking, runs in background. Does not throw.
|
|
252
|
+
*/
|
|
253
|
+
async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
|
|
254
|
+
try {
|
|
255
|
+
const tx = await publicClient.getTransaction({ hash });
|
|
256
|
+
const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
|
|
257
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
// Log but don't throw - transaction:fetched simply won't fire
|
|
261
|
+
console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
|
|
262
|
+
`transaction:fetched event will not be emitted. Error: ${error}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
|
|
267
|
+
* Emits transaction:broadcasted immediately with intended values,
|
|
268
|
+
* then fetches and emits transaction:fetched with actual values.
|
|
104
269
|
*/
|
|
105
|
-
async function
|
|
106
|
-
|
|
270
|
+
async function executeTrackedTransaction(args) {
|
|
271
|
+
const { metadata, restArgs, intendedParams, execute, extractHash } = args;
|
|
272
|
+
const broadcastTimestampMs = clock();
|
|
273
|
+
// Extract common context
|
|
274
|
+
const { from, intendedNonce } = await extractTransactionContext(args);
|
|
275
|
+
// Execute the underlying transaction with nonce injected
|
|
276
|
+
const result = await execute({
|
|
277
|
+
...restArgs,
|
|
278
|
+
nonce: intendedNonce,
|
|
279
|
+
});
|
|
280
|
+
const hash = extractHash(result);
|
|
281
|
+
// Emit transaction:broadcasted immediately with intended values
|
|
282
|
+
const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, broadcastTimestampMs, intendedParams);
|
|
283
|
+
emitter.emit('transaction:broadcasted', unknownTx);
|
|
284
|
+
// Fire-and-forget: fetch full data and emit transaction:fetched
|
|
285
|
+
fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Common wrapper for raw transaction broadcasts (sendRawTransaction).
|
|
290
|
+
* For raw transactions, we can parse full data immediately.
|
|
291
|
+
* Emits KnownTrackedTransaction directly to transaction:broadcasted.
|
|
292
|
+
*/
|
|
293
|
+
async function executeTrackedRawTransaction(args) {
|
|
294
|
+
const { serializedTransaction, metadata, execute, extractHash } = args;
|
|
295
|
+
const broadcastTimestampMs = clock();
|
|
296
|
+
const from = await recoverTransactionAddress({ serializedTransaction });
|
|
107
297
|
const parsedTx = parseTransaction(serializedTransaction);
|
|
108
|
-
|
|
109
|
-
|
|
298
|
+
// Execute the broadcast
|
|
299
|
+
const result = await execute();
|
|
300
|
+
const hash = extractHash(result);
|
|
301
|
+
// For raw transactions, we can parse full data immediately
|
|
302
|
+
const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, walletClient.chain?.id, broadcastTimestampMs);
|
|
303
|
+
// Emit as KnownTrackedTransaction since we have all data
|
|
304
|
+
emitter.emit('transaction:broadcasted', knownTx);
|
|
305
|
+
// Also emit to transaction:fetched for consistency
|
|
306
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
walletClient: walletClient,
|
|
311
|
+
publicClient,
|
|
312
|
+
// ============================================
|
|
313
|
+
// Async methods (return hash)
|
|
314
|
+
// ============================================
|
|
315
|
+
async writeContract(args) {
|
|
316
|
+
const { metadata, nonce, ...writeArgs } = args;
|
|
317
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
318
|
+
return executeTrackedTransaction({
|
|
319
|
+
account: normalizeAccount(args.account),
|
|
320
|
+
nonce,
|
|
321
|
+
metadata: metadata,
|
|
322
|
+
restArgs: writeArgs,
|
|
323
|
+
intendedParams,
|
|
324
|
+
execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
|
|
325
|
+
extractHash: (hash) => hash,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
async sendTransaction(args) {
|
|
329
|
+
const { metadata, nonce, ...sendArgs } = args;
|
|
330
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
331
|
+
return executeTrackedTransaction({
|
|
332
|
+
account: normalizeAccount(args.account),
|
|
333
|
+
nonce,
|
|
334
|
+
metadata: metadata,
|
|
335
|
+
restArgs: sendArgs,
|
|
336
|
+
intendedParams,
|
|
337
|
+
execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
|
|
338
|
+
extractHash: (hash) => hash,
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
async sendRawTransaction(args) {
|
|
342
|
+
const { metadata, serializedTransaction } = args;
|
|
343
|
+
return executeTrackedRawTransaction({
|
|
344
|
+
serializedTransaction,
|
|
345
|
+
metadata: metadata,
|
|
346
|
+
execute: () => walletClient.sendRawTransaction({ serializedTransaction }),
|
|
347
|
+
extractHash: (hash) => hash,
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
// ============================================
|
|
351
|
+
// Sync methods (return receipt, wait for confirmation)
|
|
352
|
+
// ============================================
|
|
353
|
+
async writeContractSync(args) {
|
|
354
|
+
const { metadata, nonce, ...writeArgs } = args;
|
|
355
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
356
|
+
return executeTrackedTransaction({
|
|
357
|
+
account: normalizeAccount(args.account),
|
|
358
|
+
nonce,
|
|
359
|
+
metadata: metadata,
|
|
360
|
+
restArgs: writeArgs,
|
|
361
|
+
intendedParams,
|
|
362
|
+
execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
|
|
363
|
+
extractHash: (receipt) => receipt.transactionHash,
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
async sendTransactionSync(args) {
|
|
367
|
+
const { metadata, nonce, ...sendArgs } = args;
|
|
368
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
369
|
+
return executeTrackedTransaction({
|
|
370
|
+
account: normalizeAccount(args.account),
|
|
371
|
+
nonce,
|
|
372
|
+
metadata: metadata,
|
|
373
|
+
restArgs: sendArgs,
|
|
374
|
+
intendedParams,
|
|
375
|
+
execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
|
|
376
|
+
extractHash: (receipt) => receipt.transactionHash,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
async sendRawTransactionSync(args) {
|
|
380
|
+
const { metadata, serializedTransaction } = args;
|
|
381
|
+
return executeTrackedRawTransaction({
|
|
382
|
+
serializedTransaction,
|
|
383
|
+
metadata: metadata,
|
|
384
|
+
execute: () => walletClient.sendRawTransactionSync({ serializedTransaction }),
|
|
385
|
+
extractHash: (receipt) => receipt.transactionHash,
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
// ============================================
|
|
389
|
+
// Event subscription methods
|
|
390
|
+
// ============================================
|
|
391
|
+
on: emitter.on.bind(emitter),
|
|
392
|
+
off: emitter.off.bind(emitter),
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Create an auto-populate builder for TrackedWalletClient.
|
|
399
|
+
* This builder auto-populates operation, functionName and args in writeContract metadata.
|
|
400
|
+
*/
|
|
401
|
+
function createAutoPopulateBuilder(clock) {
|
|
402
|
+
return {
|
|
403
|
+
using(walletClient, publicClient) {
|
|
404
|
+
// Create emitter for transaction events
|
|
405
|
+
const emitter = new Emitter();
|
|
406
|
+
/**
|
|
407
|
+
* Resolve the nonce to use for a transaction.
|
|
408
|
+
*/
|
|
409
|
+
async function resolveNonce(nonceOption, from) {
|
|
410
|
+
if (typeof nonceOption === 'number') {
|
|
411
|
+
return nonceOption;
|
|
110
412
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
413
|
+
const blockTag = isBlockTag(nonceOption) ? nonceOption : 'pending';
|
|
414
|
+
return await publicClient.getTransactionCount({
|
|
415
|
+
address: from,
|
|
416
|
+
blockTag,
|
|
114
417
|
});
|
|
115
|
-
return {
|
|
116
|
-
from,
|
|
117
|
-
intendedNonce: parsedTx.nonce,
|
|
118
|
-
};
|
|
119
418
|
}
|
|
120
419
|
/**
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
420
|
+
* Extract common transaction context (account, nonce) from request args.
|
|
421
|
+
*/
|
|
422
|
+
async function extractTransactionContext(args) {
|
|
423
|
+
const account = args.account ?? walletClient.account;
|
|
424
|
+
const from = resolveAccountAddress(account);
|
|
425
|
+
if (!from) {
|
|
426
|
+
throw new Error('[TrackedWalletClient] No account available. ' +
|
|
427
|
+
'Provide an account in the request or configure the wallet client with an account.');
|
|
428
|
+
}
|
|
429
|
+
const intendedNonce = await resolveNonce(args.nonce, from);
|
|
430
|
+
return { from, intendedNonce };
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Fetch full transaction data and emit transaction:fetched event.
|
|
434
|
+
* Non-blocking, runs in background. Does not throw.
|
|
127
435
|
*/
|
|
128
|
-
async function
|
|
436
|
+
async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
|
|
129
437
|
try {
|
|
130
438
|
const tx = await publicClient.getTransaction({ hash });
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
console.warn(`[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
|
|
134
|
-
`Wallet may have overridden the nonce.`);
|
|
135
|
-
}
|
|
136
|
-
return actualNonce;
|
|
439
|
+
const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
|
|
440
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
137
441
|
}
|
|
138
|
-
catch (
|
|
139
|
-
//
|
|
140
|
-
console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}
|
|
141
|
-
`
|
|
142
|
-
return intendedNonce;
|
|
442
|
+
catch (error) {
|
|
443
|
+
// Log but don't throw - transaction:fetched simply won't fire
|
|
444
|
+
console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
|
|
445
|
+
`transaction:fetched event will not be emitted. Error: ${error}`);
|
|
143
446
|
}
|
|
144
447
|
}
|
|
145
448
|
/**
|
|
146
|
-
*
|
|
449
|
+
* Validate that user didn't provide operation, functionName or args in metadata
|
|
450
|
+
* when populateMetadata is enabled.
|
|
147
451
|
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
452
|
+
function validateNoAutoPopulatedFieldsInMetadata(userMetadata) {
|
|
453
|
+
if (userMetadata && typeof userMetadata === 'object') {
|
|
454
|
+
if ('type' in userMetadata) {
|
|
455
|
+
throw new Error('[TrackedWalletClient] Cannot specify type in metadata when populateMetadata is enabled. ' +
|
|
456
|
+
'The type is automatically populated from the contract call.');
|
|
457
|
+
}
|
|
458
|
+
if ('functionName' in userMetadata) {
|
|
459
|
+
throw new Error('[TrackedWalletClient] Cannot specify functionName in metadata when populateMetadata is enabled. ' +
|
|
460
|
+
'The functionName is automatically populated from the contract call.');
|
|
461
|
+
}
|
|
462
|
+
if ('args' in userMetadata) {
|
|
463
|
+
throw new Error('[TrackedWalletClient] Cannot specify args in metadata when populateMetadata is enabled. ' +
|
|
464
|
+
'The args are automatically populated from the contract call.');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
157
467
|
}
|
|
158
468
|
/**
|
|
159
|
-
* Common wrapper for transaction methods that broadcast
|
|
160
|
-
*
|
|
469
|
+
* Common wrapper for transaction methods that broadcast.
|
|
470
|
+
* Emits transaction:broadcasted immediately with intended values,
|
|
471
|
+
* then fetches and emits transaction:fetched with actual values.
|
|
161
472
|
*/
|
|
162
473
|
async function executeTrackedTransaction(args) {
|
|
163
|
-
const { metadata, restArgs, execute, extractHash } = args;
|
|
164
|
-
|
|
474
|
+
const { metadata, restArgs, intendedParams, execute, extractHash } = args;
|
|
475
|
+
const broadcastTimestampMs = clock();
|
|
165
476
|
const { from, intendedNonce } = await extractTransactionContext(args);
|
|
166
|
-
// Execute the underlying transaction with nonce injected
|
|
167
477
|
const result = await execute({
|
|
168
478
|
...restArgs,
|
|
169
479
|
nonce: intendedNonce,
|
|
170
480
|
});
|
|
171
481
|
const hash = extractHash(result);
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
emitter.emit('transaction:broadcasted', trackedTx);
|
|
482
|
+
// Emit transaction:broadcasted immediately with intended values
|
|
483
|
+
const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, broadcastTimestampMs, intendedParams);
|
|
484
|
+
emitter.emit('transaction:broadcasted', unknownTx);
|
|
485
|
+
// Fire-and-forget: fetch full data and emit transaction:fetched
|
|
486
|
+
fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
|
|
178
487
|
return result;
|
|
179
488
|
}
|
|
180
489
|
/**
|
|
181
|
-
* Common wrapper for raw transaction broadcasts
|
|
182
|
-
*
|
|
490
|
+
* Common wrapper for raw transaction broadcasts.
|
|
491
|
+
* For raw transactions, we can parse full data immediately.
|
|
492
|
+
* Emits KnownTrackedTransaction directly to transaction:broadcasted.
|
|
183
493
|
*/
|
|
184
494
|
async function executeTrackedRawTransaction(args) {
|
|
185
495
|
const { serializedTransaction, metadata, execute, extractHash } = args;
|
|
186
|
-
|
|
187
|
-
const
|
|
496
|
+
const broadcastTimestampMs = clock();
|
|
497
|
+
const from = await recoverTransactionAddress({ serializedTransaction });
|
|
498
|
+
const parsedTx = parseTransaction(serializedTransaction);
|
|
188
499
|
// Execute the broadcast
|
|
189
500
|
const result = await execute();
|
|
190
501
|
const hash = extractHash(result);
|
|
191
|
-
// For raw transactions,
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
emitter.emit('transaction:broadcasted', trackedTx);
|
|
502
|
+
// For raw transactions, we can parse full data immediately
|
|
503
|
+
const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, walletClient.chain?.id, broadcastTimestampMs);
|
|
504
|
+
// Emit as KnownTrackedTransaction since we have all data
|
|
505
|
+
emitter.emit('transaction:broadcasted', knownTx);
|
|
506
|
+
// We do not emit fetched as the tx is already known
|
|
197
507
|
return result;
|
|
198
508
|
}
|
|
199
509
|
return {
|
|
@@ -203,23 +513,36 @@ export function createTrackedWalletClient() {
|
|
|
203
513
|
// Async methods (return hash)
|
|
204
514
|
// ============================================
|
|
205
515
|
async writeContract(args) {
|
|
206
|
-
const { metadata, nonce, ...writeArgs } = args;
|
|
516
|
+
const { metadata: userMetadata, nonce, ...writeArgs } = args;
|
|
517
|
+
// Validate that user didn't provide operation, functionName or args
|
|
518
|
+
validateNoAutoPopulatedFieldsInMetadata(userMetadata);
|
|
519
|
+
// Auto-populate type, functionName and args
|
|
520
|
+
const finalMetadata = {
|
|
521
|
+
...(userMetadata ?? {}),
|
|
522
|
+
type: 'functionCall',
|
|
523
|
+
functionName: args.functionName,
|
|
524
|
+
args: args.args,
|
|
525
|
+
};
|
|
526
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
207
527
|
return executeTrackedTransaction({
|
|
208
528
|
account: normalizeAccount(args.account),
|
|
209
529
|
nonce,
|
|
210
|
-
metadata:
|
|
530
|
+
metadata: finalMetadata,
|
|
211
531
|
restArgs: writeArgs,
|
|
532
|
+
intendedParams,
|
|
212
533
|
execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
|
|
213
534
|
extractHash: (hash) => hash,
|
|
214
535
|
});
|
|
215
536
|
},
|
|
216
537
|
async sendTransaction(args) {
|
|
217
538
|
const { metadata, nonce, ...sendArgs } = args;
|
|
539
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
218
540
|
return executeTrackedTransaction({
|
|
219
541
|
account: normalizeAccount(args.account),
|
|
220
542
|
nonce,
|
|
221
543
|
metadata: metadata,
|
|
222
544
|
restArgs: sendArgs,
|
|
545
|
+
intendedParams,
|
|
223
546
|
execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
|
|
224
547
|
extractHash: (hash) => hash,
|
|
225
548
|
});
|
|
@@ -237,23 +560,36 @@ export function createTrackedWalletClient() {
|
|
|
237
560
|
// Sync methods (return receipt, wait for confirmation)
|
|
238
561
|
// ============================================
|
|
239
562
|
async writeContractSync(args) {
|
|
240
|
-
const { metadata, nonce, ...writeArgs } = args;
|
|
563
|
+
const { metadata: userMetadata, nonce, ...writeArgs } = args;
|
|
564
|
+
// Validate that user didn't provide operation, functionName or args
|
|
565
|
+
validateNoAutoPopulatedFieldsInMetadata(userMetadata);
|
|
566
|
+
// Auto-populate type, functionName and args
|
|
567
|
+
const finalMetadata = {
|
|
568
|
+
...(userMetadata ?? {}),
|
|
569
|
+
type: 'functionCall',
|
|
570
|
+
functionName: args.functionName,
|
|
571
|
+
args: args.args,
|
|
572
|
+
};
|
|
573
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
241
574
|
return executeTrackedTransaction({
|
|
242
575
|
account: normalizeAccount(args.account),
|
|
243
576
|
nonce,
|
|
244
|
-
metadata:
|
|
577
|
+
metadata: finalMetadata,
|
|
245
578
|
restArgs: writeArgs,
|
|
579
|
+
intendedParams,
|
|
246
580
|
execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
|
|
247
581
|
extractHash: (receipt) => receipt.transactionHash,
|
|
248
582
|
});
|
|
249
583
|
},
|
|
250
584
|
async sendTransactionSync(args) {
|
|
251
585
|
const { metadata, nonce, ...sendArgs } = args;
|
|
586
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
252
587
|
return executeTrackedTransaction({
|
|
253
588
|
account: normalizeAccount(args.account),
|
|
254
589
|
nonce,
|
|
255
590
|
metadata: metadata,
|
|
256
591
|
restArgs: sendArgs,
|
|
592
|
+
intendedParams,
|
|
257
593
|
execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
|
|
258
594
|
extractHash: (receipt) => receipt.transactionHash,
|
|
259
595
|
});
|
|
@@ -270,8 +606,8 @@ export function createTrackedWalletClient() {
|
|
|
270
606
|
// ============================================
|
|
271
607
|
// Event subscription methods
|
|
272
608
|
// ============================================
|
|
273
|
-
|
|
274
|
-
|
|
609
|
+
on: emitter.on.bind(emitter),
|
|
610
|
+
off: emitter.off.bind(emitter),
|
|
275
611
|
};
|
|
276
612
|
},
|
|
277
613
|
};
|