@etherkit/viem-tx-tracker 0.0.5 → 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.map +1 -1
- package/dist/TrackedWalletClient.js +254 -129
- package/dist/TrackedWalletClient.js.map +1 -1
- package/dist/types.d.ts +110 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrackedWalletClient.ts +378 -198
- package/src/types.ts +130 -24
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseTransaction,
|
|
3
|
+
ParseTransactionReturnType,
|
|
3
4
|
recoverTransactionAddress,
|
|
4
5
|
type Abi,
|
|
5
6
|
type Account,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
type ContractFunctionName,
|
|
10
11
|
type Hash,
|
|
11
12
|
type PublicClient,
|
|
13
|
+
type Transaction,
|
|
12
14
|
type TransactionReceipt,
|
|
13
15
|
type TransactionSerialized,
|
|
14
16
|
type Transport,
|
|
@@ -16,8 +18,10 @@ import {
|
|
|
16
18
|
} from 'viem';
|
|
17
19
|
import {Emitter} from 'radiate';
|
|
18
20
|
import type {
|
|
21
|
+
AccessList,
|
|
19
22
|
BlockTag,
|
|
20
23
|
CreateTrackedWalletClientOptions,
|
|
24
|
+
KnownTrackedTransaction,
|
|
21
25
|
NonceOption,
|
|
22
26
|
PopulatedMetadata,
|
|
23
27
|
TrackedRawTransactionParameters,
|
|
@@ -27,6 +31,7 @@ import type {
|
|
|
27
31
|
TrackedWalletClientAutoPopulate,
|
|
28
32
|
TrackedWriteContractAutoPopulateParameters,
|
|
29
33
|
TrackedWriteContractParameters,
|
|
34
|
+
UnknownTrackedTransaction,
|
|
30
35
|
} from './types.js';
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -59,13 +64,6 @@ function normalizeAccount(
|
|
|
59
64
|
return account === null ? undefined : account;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
/**
|
|
63
|
-
* Generate a unique tracking ID if not provided in metadata
|
|
64
|
-
*/
|
|
65
|
-
function generateTrackingId(): string {
|
|
66
|
-
return crypto.randomUUID();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
67
|
/**
|
|
70
68
|
* Context for transaction tracking - common data extracted from request
|
|
71
69
|
*/
|
|
@@ -74,6 +72,253 @@ interface TransactionContext {
|
|
|
74
72
|
intendedNonce: number;
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Parameters we can extract from writeContract/sendTransaction calls.
|
|
77
|
+
* These are the intended values - wallet may modify them.
|
|
78
|
+
*/
|
|
79
|
+
interface IntendedTransactionParams {
|
|
80
|
+
to?: Address | null;
|
|
81
|
+
value?: bigint;
|
|
82
|
+
data?: `0x${string}`;
|
|
83
|
+
gas?: bigint;
|
|
84
|
+
gasPrice?: bigint;
|
|
85
|
+
maxFeePerGas?: bigint;
|
|
86
|
+
maxPriorityFeePerGas?: bigint;
|
|
87
|
+
accessList?: AccessList;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Infer transaction type from provided params.
|
|
92
|
+
* Returns undefined if can't be determined (wallet will decide).
|
|
93
|
+
*/
|
|
94
|
+
function inferTxType(
|
|
95
|
+
params: IntendedTransactionParams,
|
|
96
|
+
): 'eip1559' | 'legacy' | 'eip2930' | undefined {
|
|
97
|
+
if (params.maxFeePerGas !== undefined) {
|
|
98
|
+
return 'eip1559';
|
|
99
|
+
}
|
|
100
|
+
if (params.gasPrice !== undefined && params.accessList !== undefined) {
|
|
101
|
+
return 'eip2930';
|
|
102
|
+
}
|
|
103
|
+
if (params.gasPrice !== undefined) {
|
|
104
|
+
return 'legacy';
|
|
105
|
+
}
|
|
106
|
+
return undefined; // Wallet will determine
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create an UnknownTrackedTransaction for immediate emission.
|
|
111
|
+
* Populates all known intended values from the transaction parameters.
|
|
112
|
+
*/
|
|
113
|
+
function createUnknownTrackedTransaction<TMetadata>(
|
|
114
|
+
hash: Hash,
|
|
115
|
+
from: Address,
|
|
116
|
+
nonce: number | undefined,
|
|
117
|
+
chainId: number | undefined,
|
|
118
|
+
metadata: TMetadata,
|
|
119
|
+
broadcastTimestampMs: number,
|
|
120
|
+
params: IntendedTransactionParams,
|
|
121
|
+
): UnknownTrackedTransaction<TMetadata> {
|
|
122
|
+
const txType = inferTxType(params);
|
|
123
|
+
return {
|
|
124
|
+
known: false,
|
|
125
|
+
chainId,
|
|
126
|
+
hash,
|
|
127
|
+
from,
|
|
128
|
+
nonce,
|
|
129
|
+
broadcastTimestampMs,
|
|
130
|
+
metadata,
|
|
131
|
+
...(txType !== undefined && {txType}),
|
|
132
|
+
...(params.to !== undefined && {to: params.to}),
|
|
133
|
+
...(params.value !== undefined && {value: params.value}),
|
|
134
|
+
...(params.data !== undefined && {data: params.data}),
|
|
135
|
+
...(params.gas !== undefined && {gas: params.gas}),
|
|
136
|
+
...(params.gasPrice !== undefined && {gasPrice: params.gasPrice}),
|
|
137
|
+
...(params.maxFeePerGas !== undefined && {
|
|
138
|
+
maxFeePerGas: params.maxFeePerGas,
|
|
139
|
+
}),
|
|
140
|
+
...(params.maxPriorityFeePerGas !== undefined && {
|
|
141
|
+
maxPriorityFeePerGas: params.maxPriorityFeePerGas,
|
|
142
|
+
}),
|
|
143
|
+
...(params.accessList !== undefined && {accessList: params.accessList}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract transaction type-specific fields from a fetched transaction.
|
|
149
|
+
*/
|
|
150
|
+
function extractTransactionTypeFields(tx: Transaction): {
|
|
151
|
+
txType: 'eip1559' | 'legacy' | 'eip2930';
|
|
152
|
+
gasPrice?: bigint;
|
|
153
|
+
maxFeePerGas?: bigint;
|
|
154
|
+
maxPriorityFeePerGas?: bigint;
|
|
155
|
+
accessList?: AccessList;
|
|
156
|
+
} {
|
|
157
|
+
if (tx.type === 'eip1559') {
|
|
158
|
+
return {
|
|
159
|
+
txType: 'eip1559',
|
|
160
|
+
maxFeePerGas: tx.maxFeePerGas!,
|
|
161
|
+
maxPriorityFeePerGas: tx.maxPriorityFeePerGas!,
|
|
162
|
+
...(tx.accessList && {accessList: tx.accessList as AccessList}),
|
|
163
|
+
};
|
|
164
|
+
} else if (tx.type === 'eip2930') {
|
|
165
|
+
return {
|
|
166
|
+
txType: 'eip2930',
|
|
167
|
+
gasPrice: tx.gasPrice!,
|
|
168
|
+
accessList: (tx.accessList ?? []) as AccessList,
|
|
169
|
+
};
|
|
170
|
+
} else {
|
|
171
|
+
// Legacy or unknown - treat as legacy
|
|
172
|
+
return {
|
|
173
|
+
txType: 'legacy',
|
|
174
|
+
gasPrice: tx.gasPrice!,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a KnownTrackedTransaction from a fetched transaction.
|
|
181
|
+
*/
|
|
182
|
+
function createKnownTrackedTransaction<TMetadata>(
|
|
183
|
+
tx: Transaction,
|
|
184
|
+
metadata: TMetadata,
|
|
185
|
+
broadcastTimestampMs: number,
|
|
186
|
+
): KnownTrackedTransaction<TMetadata> {
|
|
187
|
+
const typeFields = extractTransactionTypeFields(tx);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
known: true,
|
|
191
|
+
chainId: tx.chainId!,
|
|
192
|
+
hash: tx.hash,
|
|
193
|
+
from: tx.from,
|
|
194
|
+
to: tx.to,
|
|
195
|
+
nonce: tx.nonce,
|
|
196
|
+
value: tx.value,
|
|
197
|
+
data: tx.input,
|
|
198
|
+
gas: tx.gas,
|
|
199
|
+
broadcastTimestampMs,
|
|
200
|
+
metadata,
|
|
201
|
+
...typeFields,
|
|
202
|
+
} as KnownTrackedTransaction<TMetadata>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create a KnownTrackedTransaction from a parsed raw transaction.
|
|
207
|
+
*/
|
|
208
|
+
function createKnownTrackedTransactionFromRaw<TMetadata>(
|
|
209
|
+
parsedTx: ParseTransactionReturnType<`0x${string}`>,
|
|
210
|
+
from: `0x${string}`,
|
|
211
|
+
hash: Hash,
|
|
212
|
+
metadata: TMetadata,
|
|
213
|
+
chainId: number | undefined,
|
|
214
|
+
broadcastTimestampMs: number,
|
|
215
|
+
): KnownTrackedTransaction<TMetadata> {
|
|
216
|
+
// Determine transaction type from parsed tx
|
|
217
|
+
let typeFields: {
|
|
218
|
+
txType: 'eip1559' | 'legacy' | 'eip2930';
|
|
219
|
+
gasPrice?: bigint;
|
|
220
|
+
maxFeePerGas?: bigint;
|
|
221
|
+
maxPriorityFeePerGas?: bigint;
|
|
222
|
+
accessList?: AccessList;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if ('maxFeePerGas' in parsedTx && parsedTx.maxFeePerGas !== undefined) {
|
|
226
|
+
typeFields = {
|
|
227
|
+
txType: 'eip1559',
|
|
228
|
+
maxFeePerGas: parsedTx.maxFeePerGas,
|
|
229
|
+
maxPriorityFeePerGas: parsedTx.maxPriorityFeePerGas!,
|
|
230
|
+
...('accessList' in parsedTx &&
|
|
231
|
+
parsedTx.accessList && {
|
|
232
|
+
accessList: parsedTx.accessList as AccessList,
|
|
233
|
+
}),
|
|
234
|
+
};
|
|
235
|
+
} else if ('accessList' in parsedTx && parsedTx.accessList) {
|
|
236
|
+
typeFields = {
|
|
237
|
+
txType: 'eip2930',
|
|
238
|
+
gasPrice: parsedTx.gasPrice!,
|
|
239
|
+
accessList: parsedTx.accessList as AccessList,
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
typeFields = {
|
|
243
|
+
txType: 'legacy',
|
|
244
|
+
gasPrice: parsedTx.gasPrice!,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
known: true,
|
|
250
|
+
chainId: parsedTx.chainId ?? chainId!,
|
|
251
|
+
hash,
|
|
252
|
+
from,
|
|
253
|
+
to: parsedTx.to ?? null,
|
|
254
|
+
nonce: parsedTx.nonce!,
|
|
255
|
+
value: parsedTx.value ?? 0n,
|
|
256
|
+
data: parsedTx.data ?? '0x',
|
|
257
|
+
gas: parsedTx.gas!,
|
|
258
|
+
broadcastTimestampMs,
|
|
259
|
+
metadata,
|
|
260
|
+
...typeFields,
|
|
261
|
+
} as KnownTrackedTransaction<TMetadata>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Extract intended params from sendTransaction args.
|
|
266
|
+
* Uses 'unknown' for 'to' since viem's SendTransactionParameters has a complex type for it.
|
|
267
|
+
*/
|
|
268
|
+
function extractIntendedParamsFromSendTransaction(args: {
|
|
269
|
+
to?: unknown;
|
|
270
|
+
value?: bigint;
|
|
271
|
+
data?: `0x${string}`;
|
|
272
|
+
gas?: bigint;
|
|
273
|
+
gasPrice?: bigint;
|
|
274
|
+
maxFeePerGas?: bigint;
|
|
275
|
+
maxPriorityFeePerGas?: bigint;
|
|
276
|
+
accessList?: AccessList;
|
|
277
|
+
}): IntendedTransactionParams {
|
|
278
|
+
// Normalize 'to' to either a hex address, null, or undefined
|
|
279
|
+
const to =
|
|
280
|
+
args.to === null || args.to === undefined
|
|
281
|
+
? (args.to as null | undefined)
|
|
282
|
+
: typeof args.to === 'string'
|
|
283
|
+
? (args.to as Address)
|
|
284
|
+
: undefined;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
to,
|
|
288
|
+
value: args.value ?? 0n,
|
|
289
|
+
data: args.data,
|
|
290
|
+
gas: args.gas,
|
|
291
|
+
gasPrice: args.gasPrice,
|
|
292
|
+
maxFeePerGas: args.maxFeePerGas,
|
|
293
|
+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
|
|
294
|
+
accessList: args.accessList,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Extract intended params from writeContract args.
|
|
300
|
+
*/
|
|
301
|
+
function extractIntendedParamsFromWriteContract(args: {
|
|
302
|
+
address: Address;
|
|
303
|
+
value?: bigint;
|
|
304
|
+
gas?: bigint;
|
|
305
|
+
gasPrice?: bigint;
|
|
306
|
+
maxFeePerGas?: bigint;
|
|
307
|
+
maxPriorityFeePerGas?: bigint;
|
|
308
|
+
accessList?: AccessList;
|
|
309
|
+
}): IntendedTransactionParams {
|
|
310
|
+
return {
|
|
311
|
+
to: args.address,
|
|
312
|
+
value: args.value ?? 0n,
|
|
313
|
+
// Note: data could be encoded using encodeFunctionData(args) if desired
|
|
314
|
+
gas: args.gas,
|
|
315
|
+
gasPrice: args.gasPrice,
|
|
316
|
+
maxFeePerGas: args.maxFeePerGas,
|
|
317
|
+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
|
|
318
|
+
accessList: args.accessList,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
77
322
|
/**
|
|
78
323
|
* Infer transport type from WalletClient
|
|
79
324
|
*/
|
|
@@ -195,9 +440,12 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
195
440
|
| TrackedWalletClientBuilder<TMetadata>
|
|
196
441
|
| TrackedWalletClientAutoPopulateBuilder<TMetadata> {
|
|
197
442
|
const populateMetadata = options?.populateMetadata ?? false;
|
|
443
|
+
const clock = options?.clock ?? Date.now;
|
|
198
444
|
|
|
199
445
|
if (populateMetadata) {
|
|
200
|
-
return createAutoPopulateBuilder<TMetadata>(
|
|
446
|
+
return createAutoPopulateBuilder<TMetadata>(
|
|
447
|
+
clock,
|
|
448
|
+
) as TrackedWalletClientAutoPopulateBuilder<TMetadata>;
|
|
201
449
|
}
|
|
202
450
|
|
|
203
451
|
return {
|
|
@@ -215,9 +463,10 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
215
463
|
type TChain = InferChain<TClient>;
|
|
216
464
|
type TAccount = InferAccount<TClient>;
|
|
217
465
|
|
|
218
|
-
// Create emitter for transaction
|
|
466
|
+
// Create emitter for transaction events
|
|
219
467
|
const emitter = new Emitter<{
|
|
220
468
|
'transaction:broadcasted': TrackedTransaction<TMetadata>;
|
|
469
|
+
'transaction:fetched': KnownTrackedTransaction<TMetadata>;
|
|
221
470
|
}>();
|
|
222
471
|
|
|
223
472
|
/**
|
|
@@ -273,101 +522,47 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
273
522
|
}
|
|
274
523
|
|
|
275
524
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* @param serializedTransaction - The RLP-encoded signed transaction
|
|
280
|
-
* @returns TransactionContext with from address and nonce
|
|
281
|
-
*/
|
|
282
|
-
async function extractRawTransactionContext(
|
|
283
|
-
serializedTransaction: TransactionSerialized,
|
|
284
|
-
): Promise<TransactionContext> {
|
|
285
|
-
// Parse the serialized transaction to get the nonce
|
|
286
|
-
const parsedTx = parseTransaction(serializedTransaction);
|
|
287
|
-
|
|
288
|
-
if (parsedTx.nonce === undefined) {
|
|
289
|
-
throw new Error(
|
|
290
|
-
'[TrackedWalletClient] Could not extract nonce from serialized transaction.',
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Recover the sender address from the signature
|
|
295
|
-
const from = await recoverTransactionAddress({
|
|
296
|
-
serializedTransaction,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
from,
|
|
301
|
-
intendedNonce: parsedTx.nonce,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Fetch the transaction after broadcast to verify nonce.
|
|
307
|
-
* Logs a warning if the nonce was overridden or if tx cannot be found.
|
|
308
|
-
*
|
|
309
|
-
* @param hash - The transaction hash
|
|
310
|
-
* @param intendedNonce - The nonce we intended to use
|
|
311
|
-
* @returns The actual nonce, or the intended nonce if fetch failed
|
|
525
|
+
* Fetch full transaction data and emit transaction:fetched event.
|
|
526
|
+
* Non-blocking, runs in background. Does not throw.
|
|
312
527
|
*/
|
|
313
|
-
async function
|
|
528
|
+
async function fetchAndEmitFullData(
|
|
314
529
|
hash: Hash,
|
|
315
|
-
|
|
316
|
-
|
|
530
|
+
metadata: TMetadata,
|
|
531
|
+
broadcastTimestampMs: number,
|
|
532
|
+
): Promise<void> {
|
|
317
533
|
try {
|
|
318
534
|
const tx = await publicClient.getTransaction({hash});
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return actualNonce;
|
|
329
|
-
} catch (fetchError) {
|
|
330
|
-
// Transaction not found in mempool/chain yet
|
|
535
|
+
const knownTx = createKnownTrackedTransaction(
|
|
536
|
+
tx,
|
|
537
|
+
metadata,
|
|
538
|
+
broadcastTimestampMs,
|
|
539
|
+
);
|
|
540
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
// Log but don't throw - transaction:fetched simply won't fire
|
|
331
543
|
console.warn(
|
|
332
|
-
`[TrackedWalletClient] Could not fetch tx ${hash}
|
|
333
|
-
`
|
|
544
|
+
`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
|
|
545
|
+
`transaction:fetched event will not be emitted. Error: ${error}`,
|
|
334
546
|
);
|
|
335
|
-
return intendedNonce;
|
|
336
547
|
}
|
|
337
548
|
}
|
|
338
549
|
|
|
339
|
-
/**
|
|
340
|
-
* Create a tracked transaction record.
|
|
341
|
-
*/
|
|
342
|
-
function createTrackedTransactionRecord(
|
|
343
|
-
txHash: Hash,
|
|
344
|
-
from: Address,
|
|
345
|
-
nonce: number,
|
|
346
|
-
metadata: TMetadata,
|
|
347
|
-
): TrackedTransaction<TMetadata> {
|
|
348
|
-
return {
|
|
349
|
-
hash: txHash,
|
|
350
|
-
from,
|
|
351
|
-
nonce,
|
|
352
|
-
chainId: walletClient.chain?.id,
|
|
353
|
-
metadata,
|
|
354
|
-
broadcastTimestampMs: Date.now(),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
550
|
/**
|
|
359
551
|
* Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
|
|
360
|
-
*
|
|
552
|
+
* Emits transaction:broadcasted immediately with intended values,
|
|
553
|
+
* then fetches and emits transaction:fetched with actual values.
|
|
361
554
|
*/
|
|
362
555
|
async function executeTrackedTransaction<T, R>(args: {
|
|
363
556
|
account?: Account | Address;
|
|
364
557
|
nonce?: NonceOption;
|
|
365
558
|
metadata: TMetadata;
|
|
366
559
|
restArgs: T;
|
|
560
|
+
intendedParams: IntendedTransactionParams;
|
|
367
561
|
execute: (argsWithNonce: T & {nonce: number}) => Promise<R>;
|
|
368
562
|
extractHash: (result: R) => Hash;
|
|
369
563
|
}): Promise<R> {
|
|
370
|
-
const {metadata, restArgs, execute, extractHash} = args;
|
|
564
|
+
const {metadata, restArgs, intendedParams, execute, extractHash} = args;
|
|
565
|
+
const broadcastTimestampMs = clock();
|
|
371
566
|
|
|
372
567
|
// Extract common context
|
|
373
568
|
const {from, intendedNonce} = await extractTransactionContext(args);
|
|
@@ -381,26 +576,28 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
381
576
|
});
|
|
382
577
|
const hash = extractHash(result);
|
|
383
578
|
|
|
384
|
-
//
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
// Create tracked transaction record
|
|
388
|
-
const trackedTx = createTrackedTransactionRecord(
|
|
579
|
+
// Emit transaction:broadcasted immediately with intended values
|
|
580
|
+
const unknownTx = createUnknownTrackedTransaction(
|
|
389
581
|
hash,
|
|
390
582
|
from,
|
|
391
|
-
|
|
583
|
+
intendedNonce,
|
|
584
|
+
walletClient.chain?.id,
|
|
392
585
|
metadata,
|
|
586
|
+
broadcastTimestampMs,
|
|
587
|
+
intendedParams,
|
|
393
588
|
);
|
|
589
|
+
emitter.emit('transaction:broadcasted', unknownTx);
|
|
394
590
|
|
|
395
|
-
//
|
|
396
|
-
|
|
591
|
+
// Fire-and-forget: fetch full data and emit transaction:fetched
|
|
592
|
+
fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
|
|
397
593
|
|
|
398
594
|
return result;
|
|
399
595
|
}
|
|
400
596
|
|
|
401
597
|
/**
|
|
402
598
|
* Common wrapper for raw transaction broadcasts (sendRawTransaction).
|
|
403
|
-
*
|
|
599
|
+
* For raw transactions, we can parse full data immediately.
|
|
600
|
+
* Emits KnownTrackedTransaction directly to transaction:broadcasted.
|
|
404
601
|
*/
|
|
405
602
|
async function executeTrackedRawTransaction<R>(args: {
|
|
406
603
|
serializedTransaction: TransactionSerialized;
|
|
@@ -409,29 +606,31 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
409
606
|
extractHash: (result: R) => Hash;
|
|
410
607
|
}): Promise<R> {
|
|
411
608
|
const {serializedTransaction, metadata, execute, extractHash} = args;
|
|
609
|
+
const broadcastTimestampMs = clock();
|
|
412
610
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
);
|
|
611
|
+
const from = await recoverTransactionAddress({serializedTransaction});
|
|
612
|
+
|
|
613
|
+
const parsedTx = parseTransaction(serializedTransaction);
|
|
417
614
|
|
|
418
615
|
// Execute the broadcast
|
|
419
616
|
const result = await execute();
|
|
420
617
|
const hash = extractHash(result);
|
|
421
618
|
|
|
422
|
-
// For raw transactions,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// Create tracked transaction record
|
|
426
|
-
const trackedTx = createTrackedTransactionRecord(
|
|
427
|
-
hash,
|
|
619
|
+
// For raw transactions, we can parse full data immediately
|
|
620
|
+
const knownTx = createKnownTrackedTransactionFromRaw(
|
|
621
|
+
parsedTx,
|
|
428
622
|
from,
|
|
429
|
-
|
|
623
|
+
hash,
|
|
430
624
|
metadata,
|
|
625
|
+
walletClient.chain?.id,
|
|
626
|
+
broadcastTimestampMs,
|
|
431
627
|
);
|
|
432
628
|
|
|
433
|
-
// Emit
|
|
434
|
-
emitter.emit('transaction:broadcasted',
|
|
629
|
+
// Emit as KnownTrackedTransaction since we have all data
|
|
630
|
+
emitter.emit('transaction:broadcasted', knownTx);
|
|
631
|
+
|
|
632
|
+
// Also emit to transaction:fetched for consistency
|
|
633
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
435
634
|
|
|
436
635
|
return result;
|
|
437
636
|
}
|
|
@@ -472,12 +671,14 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
472
671
|
>,
|
|
473
672
|
): Promise<Hash> {
|
|
474
673
|
const {metadata, nonce, ...writeArgs} = args;
|
|
674
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
475
675
|
|
|
476
676
|
return executeTrackedTransaction({
|
|
477
677
|
account: normalizeAccount(args.account),
|
|
478
678
|
nonce,
|
|
479
679
|
metadata: metadata as TMetadata,
|
|
480
680
|
restArgs: writeArgs,
|
|
681
|
+
intendedParams,
|
|
481
682
|
execute: (argsWithNonce) =>
|
|
482
683
|
walletClient.writeContract(argsWithNonce as any),
|
|
483
684
|
extractHash: (hash) => hash,
|
|
@@ -495,12 +696,14 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
495
696
|
>,
|
|
496
697
|
): Promise<Hash> {
|
|
497
698
|
const {metadata, nonce, ...sendArgs} = args;
|
|
699
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
498
700
|
|
|
499
701
|
return executeTrackedTransaction({
|
|
500
702
|
account: normalizeAccount(args.account),
|
|
501
703
|
nonce,
|
|
502
704
|
metadata: metadata as TMetadata,
|
|
503
705
|
restArgs: sendArgs,
|
|
706
|
+
intendedParams,
|
|
504
707
|
execute: (argsWithNonce) =>
|
|
505
708
|
walletClient.sendTransaction(argsWithNonce as any),
|
|
506
709
|
extractHash: (hash) => hash,
|
|
@@ -549,12 +752,14 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
549
752
|
>,
|
|
550
753
|
): Promise<TransactionReceipt> {
|
|
551
754
|
const {metadata, nonce, ...writeArgs} = args;
|
|
755
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
552
756
|
|
|
553
757
|
return executeTrackedTransaction({
|
|
554
758
|
account: normalizeAccount(args.account),
|
|
555
759
|
nonce,
|
|
556
760
|
metadata: metadata as TMetadata,
|
|
557
761
|
restArgs: writeArgs,
|
|
762
|
+
intendedParams,
|
|
558
763
|
execute: (argsWithNonce) =>
|
|
559
764
|
walletClient.writeContractSync(argsWithNonce as any),
|
|
560
765
|
extractHash: (receipt) => receipt.transactionHash,
|
|
@@ -572,12 +777,14 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
572
777
|
>,
|
|
573
778
|
): Promise<TransactionReceipt> {
|
|
574
779
|
const {metadata, nonce, ...sendArgs} = args;
|
|
780
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
575
781
|
|
|
576
782
|
return executeTrackedTransaction({
|
|
577
783
|
account: normalizeAccount(args.account),
|
|
578
784
|
nonce,
|
|
579
785
|
metadata: metadata as TMetadata,
|
|
580
786
|
restArgs: sendArgs,
|
|
787
|
+
intendedParams,
|
|
581
788
|
execute: (argsWithNonce) =>
|
|
582
789
|
walletClient.sendTransactionSync(argsWithNonce as any),
|
|
583
790
|
extractHash: (receipt) => receipt.transactionHash,
|
|
@@ -602,13 +809,8 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
602
809
|
// Event subscription methods
|
|
603
810
|
// ============================================
|
|
604
811
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
) => emitter.on('transaction:broadcasted', listener),
|
|
608
|
-
|
|
609
|
-
offTransactionBroadcasted: (
|
|
610
|
-
listener: (event: TrackedTransaction<TMetadata>) => void,
|
|
611
|
-
) => emitter.off('transaction:broadcasted', listener),
|
|
812
|
+
on: emitter.on.bind(emitter),
|
|
813
|
+
off: emitter.off.bind(emitter),
|
|
612
814
|
};
|
|
613
815
|
},
|
|
614
816
|
};
|
|
@@ -618,9 +820,9 @@ export function createTrackedWalletClient<TMetadata>(
|
|
|
618
820
|
* Create an auto-populate builder for TrackedWalletClient.
|
|
619
821
|
* This builder auto-populates operation, functionName and args in writeContract metadata.
|
|
620
822
|
*/
|
|
621
|
-
function createAutoPopulateBuilder<
|
|
622
|
-
|
|
623
|
-
|
|
823
|
+
function createAutoPopulateBuilder<TMetadata>(
|
|
824
|
+
clock: () => number,
|
|
825
|
+
): TrackedWalletClientAutoPopulateBuilder<TMetadata> {
|
|
624
826
|
return {
|
|
625
827
|
using<TClient extends WalletClient>(
|
|
626
828
|
walletClient: TClient,
|
|
@@ -636,9 +838,10 @@ function createAutoPopulateBuilder<
|
|
|
636
838
|
type TChain = InferChain<TClient>;
|
|
637
839
|
type TAccount = InferAccount<TClient>;
|
|
638
840
|
|
|
639
|
-
// Create emitter for transaction
|
|
841
|
+
// Create emitter for transaction events
|
|
640
842
|
const emitter = new Emitter<{
|
|
641
843
|
'transaction:broadcasted': TrackedTransaction<TMetadata>;
|
|
844
|
+
'transaction:fetched': KnownTrackedTransaction<TMetadata>;
|
|
642
845
|
}>();
|
|
643
846
|
|
|
644
847
|
/**
|
|
@@ -680,76 +883,31 @@ function createAutoPopulateBuilder<
|
|
|
680
883
|
}
|
|
681
884
|
|
|
682
885
|
/**
|
|
683
|
-
*
|
|
684
|
-
|
|
685
|
-
async function extractRawTransactionContext(
|
|
686
|
-
serializedTransaction: TransactionSerialized,
|
|
687
|
-
): Promise<TransactionContext> {
|
|
688
|
-
const parsedTx = parseTransaction(serializedTransaction);
|
|
689
|
-
|
|
690
|
-
if (parsedTx.nonce === undefined) {
|
|
691
|
-
throw new Error(
|
|
692
|
-
'[TrackedWalletClient] Could not extract nonce from serialized transaction.',
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const from = await recoverTransactionAddress({
|
|
697
|
-
serializedTransaction,
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
return {
|
|
701
|
-
from,
|
|
702
|
-
intendedNonce: parsedTx.nonce,
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Fetch the transaction after broadcast to verify nonce.
|
|
886
|
+
* Fetch full transaction data and emit transaction:fetched event.
|
|
887
|
+
* Non-blocking, runs in background. Does not throw.
|
|
708
888
|
*/
|
|
709
|
-
async function
|
|
889
|
+
async function fetchAndEmitFullData(
|
|
710
890
|
hash: Hash,
|
|
711
|
-
|
|
712
|
-
|
|
891
|
+
metadata: TMetadata,
|
|
892
|
+
broadcastTimestampMs: number,
|
|
893
|
+
): Promise<void> {
|
|
713
894
|
try {
|
|
714
895
|
const tx = await publicClient.getTransaction({hash});
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return actualNonce;
|
|
725
|
-
} catch (fetchError) {
|
|
896
|
+
const knownTx = createKnownTrackedTransaction(
|
|
897
|
+
tx,
|
|
898
|
+
metadata,
|
|
899
|
+
broadcastTimestampMs,
|
|
900
|
+
);
|
|
901
|
+
emitter.emit('transaction:fetched', knownTx);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
// Log but don't throw - transaction:fetched simply won't fire
|
|
726
904
|
console.warn(
|
|
727
|
-
`[TrackedWalletClient] Could not fetch tx ${hash}
|
|
728
|
-
`
|
|
905
|
+
`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
|
|
906
|
+
`transaction:fetched event will not be emitted. Error: ${error}`,
|
|
729
907
|
);
|
|
730
|
-
return intendedNonce;
|
|
731
908
|
}
|
|
732
909
|
}
|
|
733
910
|
|
|
734
|
-
/**
|
|
735
|
-
* Create a tracked transaction record.
|
|
736
|
-
*/
|
|
737
|
-
function createTrackedTransactionRecord(
|
|
738
|
-
txHash: Hash,
|
|
739
|
-
from: Address,
|
|
740
|
-
nonce: number,
|
|
741
|
-
metadata: TMetadata,
|
|
742
|
-
): TrackedTransaction<TMetadata> {
|
|
743
|
-
return {
|
|
744
|
-
hash: txHash,
|
|
745
|
-
from,
|
|
746
|
-
nonce,
|
|
747
|
-
chainId: walletClient.chain?.id,
|
|
748
|
-
metadata,
|
|
749
|
-
broadcastTimestampMs: Date.now(),
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
|
|
753
911
|
/**
|
|
754
912
|
* Validate that user didn't provide operation, functionName or args in metadata
|
|
755
913
|
* when populateMetadata is enabled.
|
|
@@ -781,16 +939,20 @@ function createAutoPopulateBuilder<
|
|
|
781
939
|
|
|
782
940
|
/**
|
|
783
941
|
* Common wrapper for transaction methods that broadcast.
|
|
942
|
+
* Emits transaction:broadcasted immediately with intended values,
|
|
943
|
+
* then fetches and emits transaction:fetched with actual values.
|
|
784
944
|
*/
|
|
785
945
|
async function executeTrackedTransaction<T, R>(args: {
|
|
786
946
|
account?: Account | Address;
|
|
787
947
|
nonce?: NonceOption;
|
|
788
948
|
metadata: TMetadata;
|
|
789
949
|
restArgs: T;
|
|
950
|
+
intendedParams: IntendedTransactionParams;
|
|
790
951
|
execute: (argsWithNonce: T & {nonce: number}) => Promise<R>;
|
|
791
952
|
extractHash: (result: R) => Hash;
|
|
792
953
|
}): Promise<R> {
|
|
793
|
-
const {metadata, restArgs, execute, extractHash} = args;
|
|
954
|
+
const {metadata, restArgs, intendedParams, execute, extractHash} = args;
|
|
955
|
+
const broadcastTimestampMs = clock();
|
|
794
956
|
|
|
795
957
|
const {from, intendedNonce} = await extractTransactionContext(args);
|
|
796
958
|
|
|
@@ -802,22 +964,28 @@ function createAutoPopulateBuilder<
|
|
|
802
964
|
});
|
|
803
965
|
const hash = extractHash(result);
|
|
804
966
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const trackedTx = createTrackedTransactionRecord(
|
|
967
|
+
// Emit transaction:broadcasted immediately with intended values
|
|
968
|
+
const unknownTx = createUnknownTrackedTransaction(
|
|
808
969
|
hash,
|
|
809
970
|
from,
|
|
810
|
-
|
|
971
|
+
intendedNonce,
|
|
972
|
+
walletClient.chain?.id,
|
|
811
973
|
metadata,
|
|
974
|
+
broadcastTimestampMs,
|
|
975
|
+
intendedParams,
|
|
812
976
|
);
|
|
977
|
+
emitter.emit('transaction:broadcasted', unknownTx);
|
|
813
978
|
|
|
814
|
-
|
|
979
|
+
// Fire-and-forget: fetch full data and emit transaction:fetched
|
|
980
|
+
fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
|
|
815
981
|
|
|
816
982
|
return result;
|
|
817
983
|
}
|
|
818
984
|
|
|
819
985
|
/**
|
|
820
986
|
* Common wrapper for raw transaction broadcasts.
|
|
987
|
+
* For raw transactions, we can parse full data immediately.
|
|
988
|
+
* Emits KnownTrackedTransaction directly to transaction:broadcasted.
|
|
821
989
|
*/
|
|
822
990
|
async function executeTrackedRawTransaction<R>(args: {
|
|
823
991
|
serializedTransaction: TransactionSerialized;
|
|
@@ -826,22 +994,29 @@ function createAutoPopulateBuilder<
|
|
|
826
994
|
extractHash: (result: R) => Hash;
|
|
827
995
|
}): Promise<R> {
|
|
828
996
|
const {serializedTransaction, metadata, execute, extractHash} = args;
|
|
997
|
+
const broadcastTimestampMs = clock();
|
|
829
998
|
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
);
|
|
999
|
+
const from = await recoverTransactionAddress({serializedTransaction});
|
|
1000
|
+
const parsedTx = parseTransaction(serializedTransaction);
|
|
833
1001
|
|
|
1002
|
+
// Execute the broadcast
|
|
834
1003
|
const result = await execute();
|
|
835
1004
|
const hash = extractHash(result);
|
|
836
1005
|
|
|
837
|
-
|
|
838
|
-
|
|
1006
|
+
// For raw transactions, we can parse full data immediately
|
|
1007
|
+
const knownTx = createKnownTrackedTransactionFromRaw(
|
|
1008
|
+
parsedTx,
|
|
839
1009
|
from,
|
|
840
|
-
|
|
1010
|
+
hash,
|
|
841
1011
|
metadata,
|
|
1012
|
+
walletClient.chain?.id,
|
|
1013
|
+
broadcastTimestampMs,
|
|
842
1014
|
);
|
|
843
1015
|
|
|
844
|
-
|
|
1016
|
+
// Emit as KnownTrackedTransaction since we have all data
|
|
1017
|
+
emitter.emit('transaction:broadcasted', knownTx);
|
|
1018
|
+
|
|
1019
|
+
// We do not emit fetched as the tx is already known
|
|
845
1020
|
|
|
846
1021
|
return result;
|
|
847
1022
|
}
|
|
@@ -894,11 +1069,14 @@ function createAutoPopulateBuilder<
|
|
|
894
1069
|
args: args.args as readonly unknown[],
|
|
895
1070
|
} as TMetadata;
|
|
896
1071
|
|
|
1072
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
1073
|
+
|
|
897
1074
|
return executeTrackedTransaction({
|
|
898
1075
|
account: normalizeAccount(args.account),
|
|
899
1076
|
nonce,
|
|
900
1077
|
metadata: finalMetadata,
|
|
901
1078
|
restArgs: writeArgs,
|
|
1079
|
+
intendedParams,
|
|
902
1080
|
execute: (argsWithNonce) =>
|
|
903
1081
|
walletClient.writeContract(argsWithNonce as any),
|
|
904
1082
|
extractHash: (hash) => hash,
|
|
@@ -916,12 +1094,14 @@ function createAutoPopulateBuilder<
|
|
|
916
1094
|
>,
|
|
917
1095
|
): Promise<Hash> {
|
|
918
1096
|
const {metadata, nonce, ...sendArgs} = args;
|
|
1097
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
919
1098
|
|
|
920
1099
|
return executeTrackedTransaction({
|
|
921
1100
|
account: normalizeAccount(args.account),
|
|
922
1101
|
nonce,
|
|
923
1102
|
metadata: metadata as TMetadata,
|
|
924
1103
|
restArgs: sendArgs,
|
|
1104
|
+
intendedParams,
|
|
925
1105
|
execute: (argsWithNonce) =>
|
|
926
1106
|
walletClient.sendTransaction(argsWithNonce as any),
|
|
927
1107
|
extractHash: (hash) => hash,
|
|
@@ -982,11 +1162,14 @@ function createAutoPopulateBuilder<
|
|
|
982
1162
|
args: args.args as readonly unknown[],
|
|
983
1163
|
} as TMetadata;
|
|
984
1164
|
|
|
1165
|
+
const intendedParams = extractIntendedParamsFromWriteContract(args);
|
|
1166
|
+
|
|
985
1167
|
return executeTrackedTransaction({
|
|
986
1168
|
account: normalizeAccount(args.account),
|
|
987
1169
|
nonce,
|
|
988
1170
|
metadata: finalMetadata,
|
|
989
1171
|
restArgs: writeArgs,
|
|
1172
|
+
intendedParams,
|
|
990
1173
|
execute: (argsWithNonce) =>
|
|
991
1174
|
walletClient.writeContractSync(argsWithNonce as any),
|
|
992
1175
|
extractHash: (receipt) => receipt.transactionHash,
|
|
@@ -1004,12 +1187,14 @@ function createAutoPopulateBuilder<
|
|
|
1004
1187
|
>,
|
|
1005
1188
|
): Promise<TransactionReceipt> {
|
|
1006
1189
|
const {metadata, nonce, ...sendArgs} = args;
|
|
1190
|
+
const intendedParams = extractIntendedParamsFromSendTransaction(args);
|
|
1007
1191
|
|
|
1008
1192
|
return executeTrackedTransaction({
|
|
1009
1193
|
account: normalizeAccount(args.account),
|
|
1010
1194
|
nonce,
|
|
1011
1195
|
metadata: metadata as TMetadata,
|
|
1012
1196
|
restArgs: sendArgs,
|
|
1197
|
+
intendedParams,
|
|
1013
1198
|
execute: (argsWithNonce) =>
|
|
1014
1199
|
walletClient.sendTransactionSync(argsWithNonce as any),
|
|
1015
1200
|
extractHash: (receipt) => receipt.transactionHash,
|
|
@@ -1034,13 +1219,8 @@ function createAutoPopulateBuilder<
|
|
|
1034
1219
|
// Event subscription methods
|
|
1035
1220
|
// ============================================
|
|
1036
1221
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
) => emitter.on('transaction:broadcasted', listener),
|
|
1040
|
-
|
|
1041
|
-
offTransactionBroadcasted: (
|
|
1042
|
-
listener: (event: TrackedTransaction<TMetadata>) => void,
|
|
1043
|
-
) => emitter.off('transaction:broadcasted', listener),
|
|
1222
|
+
on: emitter.on.bind(emitter),
|
|
1223
|
+
off: emitter.off.bind(emitter),
|
|
1044
1224
|
};
|
|
1045
1225
|
},
|
|
1046
1226
|
};
|