@etherkit/viem-tx-tracker 0.0.9 → 0.1.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,47 @@ 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
+
112
139
  /**
113
140
  * Create an UnknownTrackedTransaction for immediate emission.
114
141
  * Populates all known intended values from the transaction parameters.
115
142
  */
116
- function createUnknownTrackedTransaction<TMetadata>(
143
+ function createUnknownTrackedTransaction<TMetadata, TSource>(
117
144
  hash: Hash,
118
145
  from: Address,
119
146
  nonce: number,
120
147
  chainId: number | undefined,
121
148
  metadata: TMetadata,
149
+ source: TSource,
122
150
  broadcastTimestampMs: number,
123
151
  params: IntendedTransactionParams,
124
- ): UnknownTrackedTransaction<TMetadata> {
152
+ ): UnknownTrackedTransaction<TMetadata, TSource> {
125
153
  const base = {
126
154
  known: false as const,
127
155
  chainId,
@@ -130,6 +158,7 @@ function createUnknownTrackedTransaction<TMetadata>(
130
158
  nonce,
131
159
  broadcastTimestampMs,
132
160
  metadata,
161
+ source,
133
162
  to: params.to,
134
163
  value: params.value,
135
164
  data: params.data,
@@ -247,12 +276,17 @@ function extractTransactionTypeFields(tx: Transaction):
247
276
 
248
277
  /**
249
278
  * Create a KnownTrackedTransaction from a fetched transaction.
279
+ *
280
+ * Note: this object is rebuilt from the chain fetch, not copied from the
281
+ * broadcasted one, so every carried-but-not-observed field (metadata, source)
282
+ * must be threaded in explicitly.
250
283
  */
251
- function createKnownTrackedTransaction<TMetadata>(
284
+ function createKnownTrackedTransaction<TMetadata, TSource>(
252
285
  tx: Transaction,
253
286
  metadata: TMetadata,
287
+ source: TSource,
254
288
  broadcastTimestampMs: number,
255
- ): KnownTrackedTransaction<TMetadata> {
289
+ ): KnownTrackedTransaction<TMetadata, TSource> {
256
290
  const base = {
257
291
  known: true as const,
258
292
  hash: tx.hash,
@@ -263,6 +297,7 @@ function createKnownTrackedTransaction<TMetadata>(
263
297
  data: tx.input,
264
298
  broadcastTimestampMs,
265
299
  metadata,
300
+ source,
266
301
  };
267
302
 
268
303
  const typeFields = extractTransactionTypeFields(tx);
@@ -270,20 +305,21 @@ function createKnownTrackedTransaction<TMetadata>(
270
305
  return {
271
306
  ...base,
272
307
  ...typeFields,
273
- } as KnownTrackedTransaction<TMetadata>;
308
+ } as KnownTrackedTransaction<TMetadata, TSource>;
274
309
  }
275
310
 
276
311
  /**
277
312
  * Create a KnownTrackedTransaction from a parsed raw transaction.
278
313
  */
279
- function createKnownTrackedTransactionFromRaw<TMetadata>(
314
+ function createKnownTrackedTransactionFromRaw<TMetadata, TSource>(
280
315
  parsedTx: ParseTransactionReturnType<`0x${string}`>,
281
316
  from: `0x${string}`,
282
317
  hash: Hash,
283
318
  metadata: TMetadata,
319
+ source: TSource,
284
320
  chainId: number | undefined,
285
321
  broadcastTimestampMs: number,
286
- ): KnownTrackedTransaction<TMetadata> {
322
+ ): KnownTrackedTransaction<TMetadata, TSource> {
287
323
  const base = {
288
324
  known: true as const,
289
325
  hash,
@@ -294,6 +330,7 @@ function createKnownTrackedTransactionFromRaw<TMetadata>(
294
330
  data: parsedTx.data ?? '0x',
295
331
  broadcastTimestampMs,
296
332
  metadata,
333
+ source,
297
334
  };
298
335
 
299
336
  // Determine transaction type from parsed tx
@@ -428,7 +465,7 @@ type InferAccount<T> =
428
465
  /**
429
466
  * Builder interface returned by createTrackedWalletClient for the curried API.
430
467
  */
431
- export interface TrackedWalletClientBuilder<TMetadata> {
468
+ export interface TrackedWalletClientBuilder<TMetadata, TSource = undefined> {
432
469
  /**
433
470
  * Create the tracked wallet client using the provided wallet and public clients.
434
471
  *
@@ -443,7 +480,8 @@ export interface TrackedWalletClientBuilder<TMetadata> {
443
480
  TMetadata,
444
481
  InferTransport<TClient>,
445
482
  InferChain<TClient>,
446
- InferAccount<TClient>
483
+ InferAccount<TClient>,
484
+ TSource
447
485
  >;
448
486
  }
449
487
 
@@ -452,7 +490,10 @@ export interface TrackedWalletClientBuilder<TMetadata> {
452
490
  * This builder returns a TrackedWalletClientAutoPopulate that auto-populates operation, functionName and args.
453
491
  * TMetadata must be a type where FunctionCallMetadata is assignable to it.
454
492
  */
455
- export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
493
+ export interface TrackedWalletClientAutoPopulateBuilder<
494
+ TMetadata,
495
+ TSource = undefined,
496
+ > {
456
497
  /**
457
498
  * Create the tracked wallet client using the provided wallet and public clients.
458
499
  * writeContract and writeContractSync will automatically populate operation, functionName and args.
@@ -468,7 +509,8 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
468
509
  TMetadata,
469
510
  InferTransport<TClient>,
470
511
  InferChain<TClient>,
471
- InferAccount<TClient>
512
+ InferAccount<TClient>,
513
+ TSource
472
514
  >;
473
515
  }
474
516
 
@@ -482,6 +524,12 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
482
524
  * - Event emission for tracking
483
525
  *
484
526
  * @typeParam TMetadata - The metadata type. Use `MyMeta | undefined` to make metadata optional.
527
+ * @typeParam TSource - An opaque marker of which signing route this client is.
528
+ * Defaults to `undefined` (no source). When it does not include `undefined`,
529
+ * the `source` option is mandatory. It must always be passed explicitly:
530
+ * inference from the option value is deliberately blocked, so passing a
531
+ * `source` without also declaring TSource is an error rather than a silently
532
+ * widened type.
485
533
  * @returns A builder with a `.using()` method to provide the wallet and public clients
486
534
  *
487
535
  * @example
@@ -502,12 +550,32 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
502
550
  * type MyMetadata = OperationMetadata & { purpose: string };
503
551
  * const tracked = createTrackedWalletClient<MyMetadata>({ populateMetadata: true })
504
552
  * .using(walletClient, publicClient);
553
+ *
554
+ * // With an opaque source marking which signing route this client is.
555
+ * // Non-undefined TSource makes the `source` option mandatory.
556
+ * type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
557
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
558
+ * source: 'local-signer',
559
+ * }).using(walletClient, publicClient);
560
+ *
561
+ * // ...or as a thunk, re-evaluated at every broadcast
562
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
563
+ * source: () => currentRoute(),
564
+ * }).using(walletClient, publicClient);
505
565
  * ```
506
566
  */
507
- // Overload 1: Standard mode, no options
508
- export function createTrackedWalletClient<
509
- TMetadata,
510
- >(): TrackedWalletClientBuilder<TMetadata>;
567
+ // Overload 1: Standard mode. The options argument is required only when
568
+ // TSource does not include undefined.
569
+ export function createTrackedWalletClient<TMetadata, TSource = undefined>(
570
+ ...args: undefined extends TSource
571
+ ? [
572
+ options?: CreateTrackedWalletClientOptions<
573
+ false,
574
+ NoInferSource<TSource>
575
+ >,
576
+ ]
577
+ : [options: CreateTrackedWalletClientOptions<false, NoInferSource<TSource>>]
578
+ ): TrackedWalletClientBuilder<TMetadata, TSource>;
511
579
 
512
580
  // Overload 2: Auto-populate mode with default PopulatedMetadata
513
581
  export function createTrackedWalletClient(
@@ -515,23 +583,31 @@ export function createTrackedWalletClient(
515
583
  ): TrackedWalletClientAutoPopulateBuilder<PopulatedMetadata>;
516
584
 
517
585
  // Overload 3: Auto-populate mode with custom metadata (must allow FunctionCallMetadata)
518
- export function createTrackedWalletClient<TMetadata>(
519
- options: CreateTrackedWalletClientOptions<true>,
520
- ): TrackedWalletClientAutoPopulateBuilder<TMetadata>;
586
+ export function createTrackedWalletClient<TMetadata, TSource = undefined>(
587
+ options: CreateTrackedWalletClientOptions<true, NoInferSource<TSource>>,
588
+ ): TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource>;
521
589
 
522
590
  // Implementation
523
- export function createTrackedWalletClient<TMetadata>(
524
- options?: CreateTrackedWalletClientOptions<boolean>,
525
- ):
526
- | TrackedWalletClientBuilder<TMetadata>
527
- | TrackedWalletClientAutoPopulateBuilder<TMetadata> {
591
+ export function createTrackedWalletClient<
592
+ TMetadata,
593
+ TSource = undefined,
594
+ >(options?: {
595
+ populateMetadata?: boolean;
596
+ clock?: () => number;
597
+ source?: TSource | (() => TSource);
598
+ }):
599
+ | TrackedWalletClientBuilder<TMetadata, TSource>
600
+ | TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource> {
528
601
  const populateMetadata = options?.populateMetadata ?? false;
529
602
  const clock = options?.clock ?? Date.now;
603
+ // Kept unresolved: a thunk is evaluated at each broadcast, not here.
604
+ const sourceOption = options?.source;
530
605
 
531
606
  if (populateMetadata) {
532
- return createAutoPopulateBuilder<TMetadata>(
607
+ return createAutoPopulateBuilder<TMetadata, TSource>(
533
608
  clock,
534
- ) as TrackedWalletClientAutoPopulateBuilder<TMetadata>;
609
+ sourceOption,
610
+ ) as TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource>;
535
611
  }
536
612
 
537
613
  return {
@@ -542,7 +618,8 @@ export function createTrackedWalletClient<TMetadata>(
542
618
  TMetadata,
543
619
  InferTransport<TClient>,
544
620
  InferChain<TClient>,
545
- InferAccount<TClient>
621
+ InferAccount<TClient>,
622
+ TSource
546
623
  > {
547
624
  // Type aliases for internal use
548
625
  type TTransport = InferTransport<TClient>;
@@ -551,8 +628,8 @@ export function createTrackedWalletClient<TMetadata>(
551
628
 
552
629
  // Create emitter for transaction events
553
630
  const emitter = new Emitter<{
554
- 'transaction:broadcasted': TrackedTransaction<TMetadata>;
555
- 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
631
+ 'transaction:broadcasted': TrackedTransaction<TMetadata, TSource>;
632
+ 'transaction:fetched': KnownTrackedTransaction<TMetadata, TSource>;
556
633
  }>();
557
634
 
558
635
  /**
@@ -614,6 +691,7 @@ export function createTrackedWalletClient<TMetadata>(
614
691
  async function fetchAndEmitFullData(
615
692
  hash: Hash,
616
693
  metadata: TMetadata,
694
+ source: TSource,
617
695
  broadcastTimestampMs: number,
618
696
  ): Promise<void> {
619
697
  try {
@@ -621,6 +699,7 @@ export function createTrackedWalletClient<TMetadata>(
621
699
  const knownTx = createKnownTrackedTransaction(
622
700
  tx,
623
701
  metadata,
702
+ source,
624
703
  broadcastTimestampMs,
625
704
  );
626
705
  emitter.emit('transaction:fetched', knownTx);
@@ -649,6 +728,9 @@ export function createTrackedWalletClient<TMetadata>(
649
728
  }): Promise<R> {
650
729
  const {metadata, restArgs, intendedParams, execute, extractHash} = args;
651
730
  const broadcastTimestampMs = clock();
731
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
732
+ // A thunk is re-evaluated on every send, never captured once.
733
+ const source = resolveSource(sourceOption);
652
734
 
653
735
  // Extract common context
654
736
  const {from, intendedNonce} = await extractTransactionContext(args);
@@ -669,13 +751,14 @@ export function createTrackedWalletClient<TMetadata>(
669
751
  intendedNonce,
670
752
  walletClient.chain?.id,
671
753
  metadata,
754
+ source,
672
755
  broadcastTimestampMs,
673
756
  intendedParams,
674
757
  );
675
758
  emitter.emit('transaction:broadcasted', unknownTx);
676
759
 
677
760
  // Fire-and-forget: fetch full data and emit transaction:fetched
678
- fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
761
+ fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs);
679
762
 
680
763
  return result;
681
764
  }
@@ -693,6 +776,9 @@ export function createTrackedWalletClient<TMetadata>(
693
776
  }): Promise<R> {
694
777
  const {serializedTransaction, metadata, execute, extractHash} = args;
695
778
  const broadcastTimestampMs = clock();
779
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
780
+ // A thunk is re-evaluated on every send, never captured once.
781
+ const source = resolveSource(sourceOption);
696
782
 
697
783
  const from = await recoverTransactionAddress({serializedTransaction});
698
784
 
@@ -708,6 +794,7 @@ export function createTrackedWalletClient<TMetadata>(
708
794
  from,
709
795
  hash,
710
796
  metadata,
797
+ source,
711
798
  walletClient.chain?.id,
712
799
  broadcastTimestampMs,
713
800
  );
@@ -906,9 +993,10 @@ export function createTrackedWalletClient<TMetadata>(
906
993
  * Create an auto-populate builder for TrackedWalletClient.
907
994
  * This builder auto-populates operation, functionName and args in writeContract metadata.
908
995
  */
909
- function createAutoPopulateBuilder<TMetadata>(
996
+ function createAutoPopulateBuilder<TMetadata, TSource>(
910
997
  clock: () => number,
911
- ): TrackedWalletClientAutoPopulateBuilder<TMetadata> {
998
+ sourceOption: TSource | (() => TSource) | undefined,
999
+ ): TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource> {
912
1000
  return {
913
1001
  using<TClient extends WalletClient>(
914
1002
  walletClient: TClient,
@@ -917,7 +1005,8 @@ function createAutoPopulateBuilder<TMetadata>(
917
1005
  TMetadata,
918
1006
  InferTransport<TClient>,
919
1007
  InferChain<TClient>,
920
- InferAccount<TClient>
1008
+ InferAccount<TClient>,
1009
+ TSource
921
1010
  > {
922
1011
  // Type aliases for internal use
923
1012
  type TTransport = InferTransport<TClient>;
@@ -926,8 +1015,8 @@ function createAutoPopulateBuilder<TMetadata>(
926
1015
 
927
1016
  // Create emitter for transaction events
928
1017
  const emitter = new Emitter<{
929
- 'transaction:broadcasted': TrackedTransaction<TMetadata>;
930
- 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
1018
+ 'transaction:broadcasted': TrackedTransaction<TMetadata, TSource>;
1019
+ 'transaction:fetched': KnownTrackedTransaction<TMetadata, TSource>;
931
1020
  }>();
932
1021
 
933
1022
  /**
@@ -975,6 +1064,7 @@ function createAutoPopulateBuilder<TMetadata>(
975
1064
  async function fetchAndEmitFullData(
976
1065
  hash: Hash,
977
1066
  metadata: TMetadata,
1067
+ source: TSource,
978
1068
  broadcastTimestampMs: number,
979
1069
  ): Promise<void> {
980
1070
  try {
@@ -982,6 +1072,7 @@ function createAutoPopulateBuilder<TMetadata>(
982
1072
  const knownTx = createKnownTrackedTransaction(
983
1073
  tx,
984
1074
  metadata,
1075
+ source,
985
1076
  broadcastTimestampMs,
986
1077
  );
987
1078
  emitter.emit('transaction:fetched', knownTx);
@@ -1039,6 +1130,9 @@ function createAutoPopulateBuilder<TMetadata>(
1039
1130
  }): Promise<R> {
1040
1131
  const {metadata, restArgs, intendedParams, execute, extractHash} = args;
1041
1132
  const broadcastTimestampMs = clock();
1133
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
1134
+ // A thunk is re-evaluated on every send, never captured once.
1135
+ const source = resolveSource(sourceOption);
1042
1136
 
1043
1137
  const {from, intendedNonce} = await extractTransactionContext(args);
1044
1138
 
@@ -1057,13 +1151,14 @@ function createAutoPopulateBuilder<TMetadata>(
1057
1151
  intendedNonce,
1058
1152
  walletClient.chain?.id,
1059
1153
  metadata,
1154
+ source,
1060
1155
  broadcastTimestampMs,
1061
1156
  intendedParams,
1062
1157
  );
1063
1158
  emitter.emit('transaction:broadcasted', unknownTx);
1064
1159
 
1065
1160
  // Fire-and-forget: fetch full data and emit transaction:fetched
1066
- fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
1161
+ fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs);
1067
1162
 
1068
1163
  return result;
1069
1164
  }
@@ -1081,6 +1176,9 @@ function createAutoPopulateBuilder<TMetadata>(
1081
1176
  }): Promise<R> {
1082
1177
  const {serializedTransaction, metadata, execute, extractHash} = args;
1083
1178
  const broadcastTimestampMs = clock();
1179
+ // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
1180
+ // A thunk is re-evaluated on every send, never captured once.
1181
+ const source = resolveSource(sourceOption);
1084
1182
 
1085
1183
  const from = await recoverTransactionAddress({serializedTransaction});
1086
1184
  const parsedTx = parseTransaction(serializedTransaction);
@@ -1095,6 +1193,7 @@ function createAutoPopulateBuilder<TMetadata>(
1095
1193
  from,
1096
1194
  hash,
1097
1195
  metadata,
1196
+ source,
1098
1197
  walletClient.chain?.id,
1099
1198
  broadcastTimestampMs,
1100
1199
  );