@etherkit/viem-tx-tracker 0.0.9 → 0.2.0

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.
@@ -109,19 +109,72 @@ function inferTxType(
109
109
  return undefined; // Wallet will determine
110
110
  }
111
111
 
112
+ /**
113
+ * Blocks inference of TSource from the `source` option value.
114
+ *
115
+ * Without this, a call that passes no explicit type arguments would infer
116
+ * TSource from the value (widening `'local-signer'` to `string`) and silently
117
+ * collapse TMetadata to `unknown`, which turns metadata typing off. TSource is
118
+ * meant to be declared, never guessed.
119
+ *
120
+ * This is TypeScript 5.4's built-in `NoInfer`, spelled out so the package keeps
121
+ * working for consumers on older TypeScript versions.
122
+ */
123
+ type NoInferSource<T> = [T][T extends any ? 0 : never];
124
+
125
+ /**
126
+ * Resolve the configured source option into a value.
127
+ *
128
+ * A thunk is called every time (at broadcast), never memoized: one client can
129
+ * serve different wallets/accounts over its lifetime.
130
+ */
131
+ function resolveSource<TSource>(
132
+ source: TSource | (() => TSource) | undefined,
133
+ ): TSource {
134
+ return typeof source === 'function'
135
+ ? (source as () => TSource)()
136
+ : (source as TSource);
137
+ }
138
+
139
+ /**
140
+ * The values that are carried onto a tracked transaction rather than observed:
141
+ * they exist only because the caller (or the client construction) supplied
142
+ * them, so every emission path must thread them explicitly.
143
+ *
144
+ * They travel as one object so that adding a carried field cannot silently
145
+ * miss a path, and so that two same-typed markers cannot be swapped by a
146
+ * positional-argument mistake.
147
+ *
148
+ * The tracker never inspects any of them.
149
+ */
150
+ interface CarriedFields<TMetadata, TSource> {
151
+ /** What the application says the transaction MEANS. Persisted by consumers. */
152
+ metadata: TMetadata;
153
+ /** WHICH SIGNING ROUTE produced it. Observed at dispatch, persisted. */
154
+ source: TSource;
155
+ /**
156
+ * Which CALLER-SIDE REQUEST this send answers. Ephemeral plumbing.
157
+ *
158
+ * Deliberately a required property of type `string | undefined` rather than
159
+ * an optional one: a new call site must state that it has no correlation,
160
+ * instead of getting `undefined` by forgetting the field.
161
+ */
162
+ correlation: string | undefined;
163
+ }
164
+
112
165
  /**
113
166
  * Create an UnknownTrackedTransaction for immediate emission.
114
167
  * Populates all known intended values from the transaction parameters.
115
168
  */
116
- function createUnknownTrackedTransaction<TMetadata>(
169
+ function createUnknownTrackedTransaction<TMetadata, TSource>(
117
170
  hash: Hash,
118
171
  from: Address,
119
172
  nonce: number,
120
173
  chainId: number | undefined,
121
- metadata: TMetadata,
174
+ carried: CarriedFields<TMetadata, TSource>,
122
175
  broadcastTimestampMs: number,
123
176
  params: IntendedTransactionParams,
124
- ): UnknownTrackedTransaction<TMetadata> {
177
+ ): UnknownTrackedTransaction<TMetadata, TSource> {
125
178
  const base = {
126
179
  known: false as const,
127
180
  chainId,
@@ -129,7 +182,7 @@ function createUnknownTrackedTransaction<TMetadata>(
129
182
  from,
130
183
  nonce,
131
184
  broadcastTimestampMs,
132
- metadata,
185
+ ...carried,
133
186
  to: params.to,
134
187
  value: params.value,
135
188
  data: params.data,
@@ -247,12 +300,16 @@ function extractTransactionTypeFields(tx: Transaction):
247
300
 
248
301
  /**
249
302
  * Create a KnownTrackedTransaction from a fetched transaction.
303
+ *
304
+ * Note: this object is rebuilt from the chain fetch, not copied from the
305
+ * broadcasted one, so every carried-but-not-observed field (metadata, source,
306
+ * correlation) must be threaded in explicitly.
250
307
  */
251
- function createKnownTrackedTransaction<TMetadata>(
308
+ function createKnownTrackedTransaction<TMetadata, TSource>(
252
309
  tx: Transaction,
253
- metadata: TMetadata,
310
+ carried: CarriedFields<TMetadata, TSource>,
254
311
  broadcastTimestampMs: number,
255
- ): KnownTrackedTransaction<TMetadata> {
312
+ ): KnownTrackedTransaction<TMetadata, TSource> {
256
313
  const base = {
257
314
  known: true as const,
258
315
  hash: tx.hash,
@@ -262,7 +319,7 @@ function createKnownTrackedTransaction<TMetadata>(
262
319
  value: tx.value,
263
320
  data: tx.input,
264
321
  broadcastTimestampMs,
265
- metadata,
322
+ ...carried,
266
323
  };
267
324
 
268
325
  const typeFields = extractTransactionTypeFields(tx);
@@ -270,20 +327,20 @@ function createKnownTrackedTransaction<TMetadata>(
270
327
  return {
271
328
  ...base,
272
329
  ...typeFields,
273
- } as KnownTrackedTransaction<TMetadata>;
330
+ } as KnownTrackedTransaction<TMetadata, TSource>;
274
331
  }
275
332
 
276
333
  /**
277
334
  * Create a KnownTrackedTransaction from a parsed raw transaction.
278
335
  */
279
- function createKnownTrackedTransactionFromRaw<TMetadata>(
336
+ function createKnownTrackedTransactionFromRaw<TMetadata, TSource>(
280
337
  parsedTx: ParseTransactionReturnType<`0x${string}`>,
281
338
  from: `0x${string}`,
282
339
  hash: Hash,
283
- metadata: TMetadata,
340
+ carried: CarriedFields<TMetadata, TSource>,
284
341
  chainId: number | undefined,
285
342
  broadcastTimestampMs: number,
286
- ): KnownTrackedTransaction<TMetadata> {
343
+ ): KnownTrackedTransaction<TMetadata, TSource> {
287
344
  const base = {
288
345
  known: true as const,
289
346
  hash,
@@ -293,7 +350,7 @@ function createKnownTrackedTransactionFromRaw<TMetadata>(
293
350
  value: parsedTx.value ?? 0n,
294
351
  data: parsedTx.data ?? '0x',
295
352
  broadcastTimestampMs,
296
- metadata,
353
+ ...carried,
297
354
  };
298
355
 
299
356
  // Determine transaction type from parsed tx
@@ -428,7 +485,7 @@ type InferAccount<T> =
428
485
  /**
429
486
  * Builder interface returned by createTrackedWalletClient for the curried API.
430
487
  */
431
- export interface TrackedWalletClientBuilder<TMetadata> {
488
+ export interface TrackedWalletClientBuilder<TMetadata, TSource = undefined> {
432
489
  /**
433
490
  * Create the tracked wallet client using the provided wallet and public clients.
434
491
  *
@@ -443,7 +500,8 @@ export interface TrackedWalletClientBuilder<TMetadata> {
443
500
  TMetadata,
444
501
  InferTransport<TClient>,
445
502
  InferChain<TClient>,
446
- InferAccount<TClient>
503
+ InferAccount<TClient>,
504
+ TSource
447
505
  >;
448
506
  }
449
507
 
@@ -452,7 +510,10 @@ export interface TrackedWalletClientBuilder<TMetadata> {
452
510
  * This builder returns a TrackedWalletClientAutoPopulate that auto-populates operation, functionName and args.
453
511
  * TMetadata must be a type where FunctionCallMetadata is assignable to it.
454
512
  */
455
- export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
513
+ export interface TrackedWalletClientAutoPopulateBuilder<
514
+ TMetadata,
515
+ TSource = undefined,
516
+ > {
456
517
  /**
457
518
  * Create the tracked wallet client using the provided wallet and public clients.
458
519
  * writeContract and writeContractSync will automatically populate operation, functionName and args.
@@ -468,7 +529,8 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
468
529
  TMetadata,
469
530
  InferTransport<TClient>,
470
531
  InferChain<TClient>,
471
- InferAccount<TClient>
532
+ InferAccount<TClient>,
533
+ TSource
472
534
  >;
473
535
  }
474
536
 
@@ -482,6 +544,12 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
482
544
  * - Event emission for tracking
483
545
  *
484
546
  * @typeParam TMetadata - The metadata type. Use `MyMeta | undefined` to make metadata optional.
547
+ * @typeParam TSource - An opaque marker of which signing route this client is.
548
+ * Defaults to `undefined` (no source). When it does not include `undefined`,
549
+ * the `source` option is mandatory. It must always be passed explicitly:
550
+ * inference from the option value is deliberately blocked, so passing a
551
+ * `source` without also declaring TSource is an error rather than a silently
552
+ * widened type.
485
553
  * @returns A builder with a `.using()` method to provide the wallet and public clients
486
554
  *
487
555
  * @example
@@ -502,12 +570,32 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
502
570
  * type MyMetadata = OperationMetadata & { purpose: string };
503
571
  * const tracked = createTrackedWalletClient<MyMetadata>({ populateMetadata: true })
504
572
  * .using(walletClient, publicClient);
573
+ *
574
+ * // With an opaque source marking which signing route this client is.
575
+ * // Non-undefined TSource makes the `source` option mandatory.
576
+ * type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
577
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
578
+ * source: 'local-signer',
579
+ * }).using(walletClient, publicClient);
580
+ *
581
+ * // ...or as a thunk, re-evaluated at every broadcast
582
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
583
+ * source: () => currentRoute(),
584
+ * }).using(walletClient, publicClient);
505
585
  * ```
506
586
  */
507
- // Overload 1: Standard mode, no options
508
- export function createTrackedWalletClient<
509
- TMetadata,
510
- >(): TrackedWalletClientBuilder<TMetadata>;
587
+ // Overload 1: Standard mode. The options argument is required only when
588
+ // TSource does not include undefined.
589
+ export function createTrackedWalletClient<TMetadata, TSource = undefined>(
590
+ ...args: undefined extends TSource
591
+ ? [
592
+ options?: CreateTrackedWalletClientOptions<
593
+ false,
594
+ NoInferSource<TSource>
595
+ >,
596
+ ]
597
+ : [options: CreateTrackedWalletClientOptions<false, NoInferSource<TSource>>]
598
+ ): TrackedWalletClientBuilder<TMetadata, TSource>;
511
599
 
512
600
  // Overload 2: Auto-populate mode with default PopulatedMetadata
513
601
  export function createTrackedWalletClient(
@@ -515,23 +603,31 @@ export function createTrackedWalletClient(
515
603
  ): TrackedWalletClientAutoPopulateBuilder<PopulatedMetadata>;
516
604
 
517
605
  // Overload 3: Auto-populate mode with custom metadata (must allow FunctionCallMetadata)
518
- export function createTrackedWalletClient<TMetadata>(
519
- options: CreateTrackedWalletClientOptions<true>,
520
- ): TrackedWalletClientAutoPopulateBuilder<TMetadata>;
606
+ export function createTrackedWalletClient<TMetadata, TSource = undefined>(
607
+ options: CreateTrackedWalletClientOptions<true, NoInferSource<TSource>>,
608
+ ): TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource>;
521
609
 
522
610
  // Implementation
523
- export function createTrackedWalletClient<TMetadata>(
524
- options?: CreateTrackedWalletClientOptions<boolean>,
525
- ):
526
- | TrackedWalletClientBuilder<TMetadata>
527
- | TrackedWalletClientAutoPopulateBuilder<TMetadata> {
611
+ export function createTrackedWalletClient<
612
+ TMetadata,
613
+ TSource = undefined,
614
+ >(options?: {
615
+ populateMetadata?: boolean;
616
+ clock?: () => number;
617
+ source?: TSource | (() => TSource);
618
+ }):
619
+ | TrackedWalletClientBuilder<TMetadata, TSource>
620
+ | TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource> {
528
621
  const populateMetadata = options?.populateMetadata ?? false;
529
622
  const clock = options?.clock ?? Date.now;
623
+ // Kept unresolved: a thunk is evaluated at each broadcast, not here.
624
+ const sourceOption = options?.source;
530
625
 
531
626
  if (populateMetadata) {
532
- return createAutoPopulateBuilder<TMetadata>(
627
+ return createAutoPopulateBuilder<TMetadata, TSource>(
533
628
  clock,
534
- ) as TrackedWalletClientAutoPopulateBuilder<TMetadata>;
629
+ sourceOption,
630
+ ) as TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource>;
535
631
  }
536
632
 
537
633
  return {
@@ -542,7 +638,8 @@ export function createTrackedWalletClient<TMetadata>(
542
638
  TMetadata,
543
639
  InferTransport<TClient>,
544
640
  InferChain<TClient>,
545
- InferAccount<TClient>
641
+ InferAccount<TClient>,
642
+ TSource
546
643
  > {
547
644
  // Type aliases for internal use
548
645
  type TTransport = InferTransport<TClient>;
@@ -551,8 +648,8 @@ export function createTrackedWalletClient<TMetadata>(
551
648
 
552
649
  // Create emitter for transaction events
553
650
  const emitter = new Emitter<{
554
- 'transaction:broadcasted': TrackedTransaction<TMetadata>;
555
- 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
651
+ 'transaction:broadcasted': TrackedTransaction<TMetadata, TSource>;
652
+ 'transaction:known': KnownTrackedTransaction<TMetadata, TSource>;
556
653
  }>();
557
654
 
558
655
  /**
@@ -608,27 +705,27 @@ export function createTrackedWalletClient<TMetadata>(
608
705
  }
609
706
 
610
707
  /**
611
- * Fetch full transaction data and emit transaction:fetched event.
708
+ * Fetch full transaction data and emit transaction:known event.
612
709
  * Non-blocking, runs in background. Does not throw.
613
710
  */
614
711
  async function fetchAndEmitFullData(
615
712
  hash: Hash,
616
- metadata: TMetadata,
713
+ carried: CarriedFields<TMetadata, TSource>,
617
714
  broadcastTimestampMs: number,
618
715
  ): Promise<void> {
619
716
  try {
620
717
  const tx = await publicClient.getTransaction({hash});
621
718
  const knownTx = createKnownTrackedTransaction(
622
719
  tx,
623
- metadata,
720
+ carried,
624
721
  broadcastTimestampMs,
625
722
  );
626
- emitter.emit('transaction:fetched', knownTx);
723
+ emitter.emit('transaction:known', knownTx);
627
724
  } catch (error) {
628
- // Log but don't throw - transaction:fetched simply won't fire
725
+ // Log but don't throw - transaction:known simply won't fire
629
726
  console.warn(
630
727
  `[TrackedWalletClient] Could not fetch tx ${hash}. ` +
631
- `transaction:fetched event will not be emitted. Error: ${error}`,
728
+ `transaction:known event will not be emitted. Error: ${error}`,
632
729
  );
633
730
  }
634
731
  }
@@ -636,19 +733,46 @@ export function createTrackedWalletClient<TMetadata>(
636
733
  /**
637
734
  * Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
638
735
  * Emits transaction:broadcasted immediately with intended values,
639
- * then fetches and emits transaction:fetched with actual values.
736
+ * then fetches and emits transaction:known with actual values.
640
737
  */
641
738
  async function executeTrackedTransaction<T, R>(args: {
642
739
  account?: Account | Address;
643
740
  nonce?: NonceOption;
644
741
  metadata: TMetadata;
645
- restArgs: T;
742
+ correlation: string | undefined;
743
+ /**
744
+ * The caller's remaining args, spread verbatim into viem's own call.
745
+ *
746
+ * `{correlation?: never}` is the compile-time half of the no-forwarding
747
+ * rule: a wrapper that forgets to destructure `correlation` out fails
748
+ * here instead of handing the wallet an unrecognised parameter. Nothing
749
+ * downstream would catch it, since the viem call is behind an `as any`.
750
+ */
751
+ restArgs: T & {correlation?: never};
646
752
  intendedParams: IntendedTransactionParams;
647
753
  execute: (argsWithNonce: T & {nonce: number}) => Promise<R>;
648
754
  extractHash: (result: R) => Hash;
649
755
  }): Promise<R> {
650
- const {metadata, restArgs, intendedParams, execute, extractHash} = args;
756
+ const {
757
+ metadata,
758
+ correlation,
759
+ restArgs,
760
+ intendedParams,
761
+ execute,
762
+ extractHash,
763
+ } = args;
651
764
  const broadcastTimestampMs = clock();
765
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
766
+ // A thunk is re-evaluated on every send, never captured once.
767
+ const source = resolveSource(sourceOption);
768
+ // The carried values travel as one bundle from here on: metadata (what
769
+ // the tx means), source (which route signed) and correlation (which
770
+ // caller-side request this send answers). None is interpreted.
771
+ const carried: CarriedFields<TMetadata, TSource> = {
772
+ metadata,
773
+ source,
774
+ correlation,
775
+ };
652
776
 
653
777
  // Extract common context
654
778
  const {from, intendedNonce} = await extractTransactionContext(args);
@@ -668,14 +792,14 @@ export function createTrackedWalletClient<TMetadata>(
668
792
  from,
669
793
  intendedNonce,
670
794
  walletClient.chain?.id,
671
- metadata,
795
+ carried,
672
796
  broadcastTimestampMs,
673
797
  intendedParams,
674
798
  );
675
799
  emitter.emit('transaction:broadcasted', unknownTx);
676
800
 
677
- // Fire-and-forget: fetch full data and emit transaction:fetched
678
- fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
801
+ // Fire-and-forget: fetch full data and emit transaction:known
802
+ fetchAndEmitFullData(hash, carried, broadcastTimestampMs);
679
803
 
680
804
  return result;
681
805
  }
@@ -688,11 +812,26 @@ export function createTrackedWalletClient<TMetadata>(
688
812
  async function executeTrackedRawTransaction<R>(args: {
689
813
  serializedTransaction: TransactionSerialized;
690
814
  metadata: TMetadata;
815
+ correlation: string | undefined;
691
816
  execute: () => Promise<R>;
692
817
  extractHash: (result: R) => Hash;
693
818
  }): Promise<R> {
694
- const {serializedTransaction, metadata, execute, extractHash} = args;
819
+ const {
820
+ serializedTransaction,
821
+ metadata,
822
+ correlation,
823
+ execute,
824
+ extractHash,
825
+ } = args;
695
826
  const broadcastTimestampMs = clock();
827
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
828
+ // A thunk is re-evaluated on every send, never captured once.
829
+ const source = resolveSource(sourceOption);
830
+ const carried: CarriedFields<TMetadata, TSource> = {
831
+ metadata,
832
+ source,
833
+ correlation,
834
+ };
696
835
 
697
836
  const from = await recoverTransactionAddress({serializedTransaction});
698
837
 
@@ -707,7 +846,7 @@ export function createTrackedWalletClient<TMetadata>(
707
846
  parsedTx,
708
847
  from,
709
848
  hash,
710
- metadata,
849
+ carried,
711
850
  walletClient.chain?.id,
712
851
  broadcastTimestampMs,
713
852
  );
@@ -715,8 +854,11 @@ export function createTrackedWalletClient<TMetadata>(
715
854
  // Emit as KnownTrackedTransaction since we have all data
716
855
  emitter.emit('transaction:broadcasted', knownTx);
717
856
 
718
- // Also emit to transaction:fetched for consistency
719
- emitter.emit('transaction:fetched', knownTx);
857
+ // The values are final (parsed from the signed payload, not merely
858
+ // intended), which is exactly what transaction:known promises, so it
859
+ // is emitted here too: every tracked transaction reaches that event,
860
+ // and a consumer can persist on it alone.
861
+ emitter.emit('transaction:known', knownTx);
720
862
 
721
863
  return result;
722
864
  }
@@ -756,13 +898,14 @@ export function createTrackedWalletClient<TMetadata>(
756
898
  TChainOverride
757
899
  >,
758
900
  ): Promise<Hash> {
759
- const {metadata, nonce, ...writeArgs} = args;
901
+ const {metadata, correlation, nonce, ...writeArgs} = args;
760
902
  const intendedParams = extractIntendedParamsFromWriteContract(args);
761
903
 
762
904
  return executeTrackedTransaction({
763
905
  account: normalizeAccount(args.account),
764
906
  nonce,
765
907
  metadata: metadata as TMetadata,
908
+ correlation,
766
909
  restArgs: writeArgs,
767
910
  intendedParams,
768
911
  execute: (argsWithNonce) =>
@@ -781,13 +924,14 @@ export function createTrackedWalletClient<TMetadata>(
781
924
  TChainOverride
782
925
  >,
783
926
  ): Promise<Hash> {
784
- const {metadata, nonce, ...sendArgs} = args;
927
+ const {metadata, correlation, nonce, ...sendArgs} = args;
785
928
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
786
929
 
787
930
  return executeTrackedTransaction({
788
931
  account: normalizeAccount(args.account),
789
932
  nonce,
790
933
  metadata: metadata as TMetadata,
934
+ correlation,
791
935
  restArgs: sendArgs,
792
936
  intendedParams,
793
937
  execute: (argsWithNonce) =>
@@ -799,11 +943,12 @@ export function createTrackedWalletClient<TMetadata>(
799
943
  async sendRawTransaction(
800
944
  args: TrackedRawTransactionParameters<TMetadata>,
801
945
  ): Promise<Hash> {
802
- const {metadata, serializedTransaction} = args;
946
+ const {metadata, correlation, serializedTransaction} = args;
803
947
 
804
948
  return executeTrackedRawTransaction({
805
949
  serializedTransaction,
806
950
  metadata: metadata as TMetadata,
951
+ correlation,
807
952
  execute: () =>
808
953
  walletClient.sendRawTransaction({serializedTransaction}),
809
954
  extractHash: (hash) => hash,
@@ -837,13 +982,14 @@ export function createTrackedWalletClient<TMetadata>(
837
982
  TChainOverride
838
983
  >,
839
984
  ): Promise<TransactionReceipt> {
840
- const {metadata, nonce, ...writeArgs} = args;
985
+ const {metadata, correlation, nonce, ...writeArgs} = args;
841
986
  const intendedParams = extractIntendedParamsFromWriteContract(args);
842
987
 
843
988
  return executeTrackedTransaction({
844
989
  account: normalizeAccount(args.account),
845
990
  nonce,
846
991
  metadata: metadata as TMetadata,
992
+ correlation,
847
993
  restArgs: writeArgs,
848
994
  intendedParams,
849
995
  execute: (argsWithNonce) =>
@@ -862,13 +1008,14 @@ export function createTrackedWalletClient<TMetadata>(
862
1008
  TChainOverride
863
1009
  >,
864
1010
  ): Promise<TransactionReceipt> {
865
- const {metadata, nonce, ...sendArgs} = args;
1011
+ const {metadata, correlation, nonce, ...sendArgs} = args;
866
1012
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
867
1013
 
868
1014
  return executeTrackedTransaction({
869
1015
  account: normalizeAccount(args.account),
870
1016
  nonce,
871
1017
  metadata: metadata as TMetadata,
1018
+ correlation,
872
1019
  restArgs: sendArgs,
873
1020
  intendedParams,
874
1021
  execute: (argsWithNonce) =>
@@ -880,11 +1027,12 @@ export function createTrackedWalletClient<TMetadata>(
880
1027
  async sendRawTransactionSync(
881
1028
  args: TrackedRawTransactionParameters<TMetadata>,
882
1029
  ): Promise<TransactionReceipt> {
883
- const {metadata, serializedTransaction} = args;
1030
+ const {metadata, correlation, serializedTransaction} = args;
884
1031
 
885
1032
  return executeTrackedRawTransaction({
886
1033
  serializedTransaction,
887
1034
  metadata: metadata as TMetadata,
1035
+ correlation,
888
1036
  execute: () =>
889
1037
  walletClient.sendRawTransactionSync({serializedTransaction}),
890
1038
  extractHash: (receipt) => receipt.transactionHash,
@@ -906,9 +1054,10 @@ export function createTrackedWalletClient<TMetadata>(
906
1054
  * Create an auto-populate builder for TrackedWalletClient.
907
1055
  * This builder auto-populates operation, functionName and args in writeContract metadata.
908
1056
  */
909
- function createAutoPopulateBuilder<TMetadata>(
1057
+ function createAutoPopulateBuilder<TMetadata, TSource>(
910
1058
  clock: () => number,
911
- ): TrackedWalletClientAutoPopulateBuilder<TMetadata> {
1059
+ sourceOption: TSource | (() => TSource) | undefined,
1060
+ ): TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource> {
912
1061
  return {
913
1062
  using<TClient extends WalletClient>(
914
1063
  walletClient: TClient,
@@ -917,7 +1066,8 @@ function createAutoPopulateBuilder<TMetadata>(
917
1066
  TMetadata,
918
1067
  InferTransport<TClient>,
919
1068
  InferChain<TClient>,
920
- InferAccount<TClient>
1069
+ InferAccount<TClient>,
1070
+ TSource
921
1071
  > {
922
1072
  // Type aliases for internal use
923
1073
  type TTransport = InferTransport<TClient>;
@@ -926,8 +1076,8 @@ function createAutoPopulateBuilder<TMetadata>(
926
1076
 
927
1077
  // Create emitter for transaction events
928
1078
  const emitter = new Emitter<{
929
- 'transaction:broadcasted': TrackedTransaction<TMetadata>;
930
- 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
1079
+ 'transaction:broadcasted': TrackedTransaction<TMetadata, TSource>;
1080
+ 'transaction:known': KnownTrackedTransaction<TMetadata, TSource>;
931
1081
  }>();
932
1082
 
933
1083
  /**
@@ -969,27 +1119,27 @@ function createAutoPopulateBuilder<TMetadata>(
969
1119
  }
970
1120
 
971
1121
  /**
972
- * Fetch full transaction data and emit transaction:fetched event.
1122
+ * Fetch full transaction data and emit transaction:known event.
973
1123
  * Non-blocking, runs in background. Does not throw.
974
1124
  */
975
1125
  async function fetchAndEmitFullData(
976
1126
  hash: Hash,
977
- metadata: TMetadata,
1127
+ carried: CarriedFields<TMetadata, TSource>,
978
1128
  broadcastTimestampMs: number,
979
1129
  ): Promise<void> {
980
1130
  try {
981
1131
  const tx = await publicClient.getTransaction({hash});
982
1132
  const knownTx = createKnownTrackedTransaction(
983
1133
  tx,
984
- metadata,
1134
+ carried,
985
1135
  broadcastTimestampMs,
986
1136
  );
987
- emitter.emit('transaction:fetched', knownTx);
1137
+ emitter.emit('transaction:known', knownTx);
988
1138
  } catch (error) {
989
- // Log but don't throw - transaction:fetched simply won't fire
1139
+ // Log but don't throw - transaction:known simply won't fire
990
1140
  console.warn(
991
1141
  `[TrackedWalletClient] Could not fetch tx ${hash}. ` +
992
- `transaction:fetched event will not be emitted. Error: ${error}`,
1142
+ `transaction:known event will not be emitted. Error: ${error}`,
993
1143
  );
994
1144
  }
995
1145
  }
@@ -1026,19 +1176,46 @@ function createAutoPopulateBuilder<TMetadata>(
1026
1176
  /**
1027
1177
  * Common wrapper for transaction methods that broadcast.
1028
1178
  * Emits transaction:broadcasted immediately with intended values,
1029
- * then fetches and emits transaction:fetched with actual values.
1179
+ * then fetches and emits transaction:known with actual values.
1030
1180
  */
1031
1181
  async function executeTrackedTransaction<T, R>(args: {
1032
1182
  account?: Account | Address;
1033
1183
  nonce?: NonceOption;
1034
1184
  metadata: TMetadata;
1035
- restArgs: T;
1185
+ correlation: string | undefined;
1186
+ /**
1187
+ * The caller's remaining args, spread verbatim into viem's own call.
1188
+ *
1189
+ * `{correlation?: never}` is the compile-time half of the no-forwarding
1190
+ * rule: a wrapper that forgets to destructure `correlation` out fails
1191
+ * here instead of handing the wallet an unrecognised parameter. Nothing
1192
+ * downstream would catch it, since the viem call is behind an `as any`.
1193
+ */
1194
+ restArgs: T & {correlation?: never};
1036
1195
  intendedParams: IntendedTransactionParams;
1037
1196
  execute: (argsWithNonce: T & {nonce: number}) => Promise<R>;
1038
1197
  extractHash: (result: R) => Hash;
1039
1198
  }): Promise<R> {
1040
- const {metadata, restArgs, intendedParams, execute, extractHash} = args;
1199
+ const {
1200
+ metadata,
1201
+ correlation,
1202
+ restArgs,
1203
+ intendedParams,
1204
+ execute,
1205
+ extractHash,
1206
+ } = args;
1041
1207
  const broadcastTimestampMs = clock();
1208
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
1209
+ // A thunk is re-evaluated on every send, never captured once.
1210
+ const source = resolveSource(sourceOption);
1211
+ // The carried values travel as one bundle from here on: metadata (what
1212
+ // the tx means), source (which route signed) and correlation (which
1213
+ // caller-side request this send answers). None is interpreted.
1214
+ const carried: CarriedFields<TMetadata, TSource> = {
1215
+ metadata,
1216
+ source,
1217
+ correlation,
1218
+ };
1042
1219
 
1043
1220
  const {from, intendedNonce} = await extractTransactionContext(args);
1044
1221
 
@@ -1056,14 +1233,14 @@ function createAutoPopulateBuilder<TMetadata>(
1056
1233
  from,
1057
1234
  intendedNonce,
1058
1235
  walletClient.chain?.id,
1059
- metadata,
1236
+ carried,
1060
1237
  broadcastTimestampMs,
1061
1238
  intendedParams,
1062
1239
  );
1063
1240
  emitter.emit('transaction:broadcasted', unknownTx);
1064
1241
 
1065
- // Fire-and-forget: fetch full data and emit transaction:fetched
1066
- fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
1242
+ // Fire-and-forget: fetch full data and emit transaction:known
1243
+ fetchAndEmitFullData(hash, carried, broadcastTimestampMs);
1067
1244
 
1068
1245
  return result;
1069
1246
  }
@@ -1076,11 +1253,26 @@ function createAutoPopulateBuilder<TMetadata>(
1076
1253
  async function executeTrackedRawTransaction<R>(args: {
1077
1254
  serializedTransaction: TransactionSerialized;
1078
1255
  metadata: TMetadata;
1256
+ correlation: string | undefined;
1079
1257
  execute: () => Promise<R>;
1080
1258
  extractHash: (result: R) => Hash;
1081
1259
  }): Promise<R> {
1082
- const {serializedTransaction, metadata, execute, extractHash} = args;
1260
+ const {
1261
+ serializedTransaction,
1262
+ metadata,
1263
+ correlation,
1264
+ execute,
1265
+ extractHash,
1266
+ } = args;
1083
1267
  const broadcastTimestampMs = clock();
1268
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
1269
+ // A thunk is re-evaluated on every send, never captured once.
1270
+ const source = resolveSource(sourceOption);
1271
+ const carried: CarriedFields<TMetadata, TSource> = {
1272
+ metadata,
1273
+ source,
1274
+ correlation,
1275
+ };
1084
1276
 
1085
1277
  const from = await recoverTransactionAddress({serializedTransaction});
1086
1278
  const parsedTx = parseTransaction(serializedTransaction);
@@ -1094,7 +1286,7 @@ function createAutoPopulateBuilder<TMetadata>(
1094
1286
  parsedTx,
1095
1287
  from,
1096
1288
  hash,
1097
- metadata,
1289
+ carried,
1098
1290
  walletClient.chain?.id,
1099
1291
  broadcastTimestampMs,
1100
1292
  );
@@ -1102,7 +1294,11 @@ function createAutoPopulateBuilder<TMetadata>(
1102
1294
  // Emit as KnownTrackedTransaction since we have all data
1103
1295
  emitter.emit('transaction:broadcasted', knownTx);
1104
1296
 
1105
- // We do not emit fetched as the tx is already known
1297
+ // The values are final (parsed from the signed payload, not merely
1298
+ // intended), which is exactly what transaction:known promises, so it
1299
+ // is emitted here too: every tracked transaction reaches that event,
1300
+ // and a consumer can persist on it alone.
1301
+ emitter.emit('transaction:known', knownTx);
1106
1302
 
1107
1303
  return result;
1108
1304
  }
@@ -1142,7 +1338,12 @@ function createAutoPopulateBuilder<TMetadata>(
1142
1338
  TChainOverride
1143
1339
  >,
1144
1340
  ): Promise<Hash> {
1145
- const {metadata: userMetadata, nonce, ...writeArgs} = args;
1341
+ const {
1342
+ metadata: userMetadata,
1343
+ correlation,
1344
+ nonce,
1345
+ ...writeArgs
1346
+ } = args;
1146
1347
 
1147
1348
  // Validate that user didn't provide operation, functionName or args
1148
1349
  validateNoAutoPopulatedFieldsInMetadata(userMetadata);
@@ -1161,6 +1362,7 @@ function createAutoPopulateBuilder<TMetadata>(
1161
1362
  account: normalizeAccount(args.account),
1162
1363
  nonce,
1163
1364
  metadata: finalMetadata,
1365
+ correlation,
1164
1366
  restArgs: writeArgs,
1165
1367
  intendedParams,
1166
1368
  execute: (argsWithNonce) =>
@@ -1179,13 +1381,14 @@ function createAutoPopulateBuilder<TMetadata>(
1179
1381
  TChainOverride
1180
1382
  >,
1181
1383
  ): Promise<Hash> {
1182
- const {metadata, nonce, ...sendArgs} = args;
1384
+ const {metadata, correlation, nonce, ...sendArgs} = args;
1183
1385
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
1184
1386
 
1185
1387
  return executeTrackedTransaction({
1186
1388
  account: normalizeAccount(args.account),
1187
1389
  nonce,
1188
1390
  metadata: metadata as TMetadata,
1391
+ correlation,
1189
1392
  restArgs: sendArgs,
1190
1393
  intendedParams,
1191
1394
  execute: (argsWithNonce) =>
@@ -1197,11 +1400,12 @@ function createAutoPopulateBuilder<TMetadata>(
1197
1400
  async sendRawTransaction(
1198
1401
  args: TrackedRawTransactionParameters<TMetadata>,
1199
1402
  ): Promise<Hash> {
1200
- const {metadata, serializedTransaction} = args;
1403
+ const {metadata, correlation, serializedTransaction} = args;
1201
1404
 
1202
1405
  return executeTrackedRawTransaction({
1203
1406
  serializedTransaction,
1204
1407
  metadata: metadata as TMetadata,
1408
+ correlation,
1205
1409
  execute: () =>
1206
1410
  walletClient.sendRawTransaction({serializedTransaction}),
1207
1411
  extractHash: (hash) => hash,
@@ -1235,7 +1439,12 @@ function createAutoPopulateBuilder<TMetadata>(
1235
1439
  TChainOverride
1236
1440
  >,
1237
1441
  ): Promise<TransactionReceipt> {
1238
- const {metadata: userMetadata, nonce, ...writeArgs} = args;
1442
+ const {
1443
+ metadata: userMetadata,
1444
+ correlation,
1445
+ nonce,
1446
+ ...writeArgs
1447
+ } = args;
1239
1448
 
1240
1449
  // Validate that user didn't provide operation, functionName or args
1241
1450
  validateNoAutoPopulatedFieldsInMetadata(userMetadata);
@@ -1254,6 +1463,7 @@ function createAutoPopulateBuilder<TMetadata>(
1254
1463
  account: normalizeAccount(args.account),
1255
1464
  nonce,
1256
1465
  metadata: finalMetadata,
1466
+ correlation,
1257
1467
  restArgs: writeArgs,
1258
1468
  intendedParams,
1259
1469
  execute: (argsWithNonce) =>
@@ -1272,13 +1482,14 @@ function createAutoPopulateBuilder<TMetadata>(
1272
1482
  TChainOverride
1273
1483
  >,
1274
1484
  ): Promise<TransactionReceipt> {
1275
- const {metadata, nonce, ...sendArgs} = args;
1485
+ const {metadata, correlation, nonce, ...sendArgs} = args;
1276
1486
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
1277
1487
 
1278
1488
  return executeTrackedTransaction({
1279
1489
  account: normalizeAccount(args.account),
1280
1490
  nonce,
1281
1491
  metadata: metadata as TMetadata,
1492
+ correlation,
1282
1493
  restArgs: sendArgs,
1283
1494
  intendedParams,
1284
1495
  execute: (argsWithNonce) =>
@@ -1290,11 +1501,12 @@ function createAutoPopulateBuilder<TMetadata>(
1290
1501
  async sendRawTransactionSync(
1291
1502
  args: TrackedRawTransactionParameters<TMetadata>,
1292
1503
  ): Promise<TransactionReceipt> {
1293
- const {metadata, serializedTransaction} = args;
1504
+ const {metadata, correlation, serializedTransaction} = args;
1294
1505
 
1295
1506
  return executeTrackedRawTransaction({
1296
1507
  serializedTransaction,
1297
1508
  metadata: metadata as TMetadata,
1509
+ correlation,
1298
1510
  execute: () =>
1299
1511
  walletClient.sendRawTransactionSync({serializedTransaction}),
1300
1512
  extractHash: (receipt) => receipt.transactionHash,