@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.
@@ -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>() as TrackedWalletClientAutoPopulateBuilder<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 broadcast events
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
- * Extract transaction context from a serialized (signed) transaction.
277
- * Parses the transaction and recovers the sender address.
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 verifyTransactionNonce(
528
+ async function fetchAndEmitFullData(
314
529
  hash: Hash,
315
- intendedNonce: number,
316
- ): Promise<number> {
530
+ metadata: TMetadata,
531
+ broadcastTimestampMs: number,
532
+ ): Promise<void> {
317
533
  try {
318
534
  const tx = await publicClient.getTransaction({hash});
319
- const actualNonce = tx.nonce;
320
-
321
- if (actualNonce !== intendedNonce) {
322
- console.warn(
323
- `[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
324
- `Wallet may have overridden the nonce.`,
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} after broadcast. ` +
333
- `It may not be in the mempool yet.`,
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
- * Handles nonce resolution, underlying call, post-broadcast verification, and tracking record creation.
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
- // Verify transaction and get actual nonce
385
- const actualNonce = await verifyTransactionNonce(hash, intendedNonce);
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
- actualNonce,
583
+ intendedNonce,
584
+ walletClient.chain?.id,
392
585
  metadata,
586
+ broadcastTimestampMs,
587
+ intendedParams,
393
588
  );
589
+ emitter.emit('transaction:broadcasted', unknownTx);
394
590
 
395
- // Emit transaction broadcasted event
396
- emitter.emit('transaction:broadcasted', trackedTx);
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
- * Decodes the transaction to extract from/nonce, broadcasts, and creates tracking record.
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
- // Extract context from the serialized transaction
414
- const {from, intendedNonce} = await extractRawTransactionContext(
415
- serializedTransaction,
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, the nonce is already embedded, so no verification needed
423
- // (wallet cannot override nonce in an already-signed transaction)
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
- intendedNonce,
623
+ hash,
430
624
  metadata,
625
+ walletClient.chain?.id,
626
+ broadcastTimestampMs,
431
627
  );
432
628
 
433
- // Emit transaction broadcasted event
434
- emitter.emit('transaction:broadcasted', trackedTx);
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
- onTransactionBroadcasted: (
606
- listener: (event: TrackedTransaction<TMetadata>) => void,
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
- TMetadata,
623
- >(): TrackedWalletClientAutoPopulateBuilder<TMetadata> {
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 broadcast events
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
- * Extract transaction context from a serialized (signed) transaction.
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 verifyTransactionNonce(
889
+ async function fetchAndEmitFullData(
710
890
  hash: Hash,
711
- intendedNonce: number,
712
- ): Promise<number> {
891
+ metadata: TMetadata,
892
+ broadcastTimestampMs: number,
893
+ ): Promise<void> {
713
894
  try {
714
895
  const tx = await publicClient.getTransaction({hash});
715
- const actualNonce = tx.nonce;
716
-
717
- if (actualNonce !== intendedNonce) {
718
- console.warn(
719
- `[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
720
- `Wallet may have overridden the nonce.`,
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} after broadcast. ` +
728
- `It may not be in the mempool yet.`,
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
- const actualNonce = await verifyTransactionNonce(hash, intendedNonce);
806
-
807
- const trackedTx = createTrackedTransactionRecord(
967
+ // Emit transaction:broadcasted immediately with intended values
968
+ const unknownTx = createUnknownTrackedTransaction(
808
969
  hash,
809
970
  from,
810
- actualNonce,
971
+ intendedNonce,
972
+ walletClient.chain?.id,
811
973
  metadata,
974
+ broadcastTimestampMs,
975
+ intendedParams,
812
976
  );
977
+ emitter.emit('transaction:broadcasted', unknownTx);
813
978
 
814
- emitter.emit('transaction:broadcasted', trackedTx);
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 {from, intendedNonce} = await extractRawTransactionContext(
831
- serializedTransaction,
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
- const trackedTx = createTrackedTransactionRecord(
838
- hash,
1006
+ // For raw transactions, we can parse full data immediately
1007
+ const knownTx = createKnownTrackedTransactionFromRaw(
1008
+ parsedTx,
839
1009
  from,
840
- intendedNonce,
1010
+ hash,
841
1011
  metadata,
1012
+ walletClient.chain?.id,
1013
+ broadcastTimestampMs,
842
1014
  );
843
1015
 
844
- emitter.emit('transaction:broadcasted', trackedTx);
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
- onTransactionBroadcasted: (
1038
- listener: (event: TrackedTransaction<TMetadata>) => void,
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
  };