@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.
package/src/types.ts CHANGED
@@ -47,12 +47,31 @@ export type UnknownTypeMetadata = {
47
47
  */
48
48
  export type PopulatedMetadata = FunctionCallMetadata | UnknownTypeMetadata;
49
49
 
50
+ /**
51
+ * Conditional type that makes the `source` construction option required or
52
+ * optional based on TSource. Mirrors {@link MetadataField}.
53
+ *
54
+ * If TSource includes undefined (the default), `source` is optional.
55
+ * Otherwise a client cannot be constructed without supplying one.
56
+ *
57
+ * The value can be given directly or as a thunk. The thunk is evaluated at
58
+ * broadcast time, once per send, so a client whose signing route can change
59
+ * over its lifetime always stamps the route that actually signed.
60
+ *
61
+ * Note: a TSource that is itself a function type cannot be distinguished from
62
+ * a thunk at runtime - wrap it (e.g. `{fn: ...}`) if you need one.
63
+ */
64
+ export type SourceField<TSource> = undefined extends TSource
65
+ ? {source?: TSource | (() => TSource)}
66
+ : {source: TSource | (() => TSource)};
67
+
50
68
  /**
51
69
  * Options for creating a tracked wallet client.
52
70
  */
53
- export interface CreateTrackedWalletClientOptions<
71
+ export type CreateTrackedWalletClientOptions<
54
72
  TPopulate extends boolean = false,
55
- > {
73
+ TSource = undefined,
74
+ > = {
56
75
  /**
57
76
  * When true, writeContract and writeContractSync automatically populate
58
77
  * operation, functionName and args in the metadata from the contract call parameters.
@@ -65,7 +84,7 @@ export interface CreateTrackedWalletClientOptions<
65
84
  * Defaults to Date.now.
66
85
  */
67
86
  clock?: () => number;
68
- }
87
+ } & SourceField<TSource>;
69
88
 
70
89
  /**
71
90
  * Block tags that can be used to specify nonce fetching strategy
@@ -89,6 +108,46 @@ export type MetadataField<TMetadata> = undefined extends TMetadata
89
108
  ? {metadata?: TMetadata}
90
109
  : {metadata: TMetadata};
91
110
 
111
+ /**
112
+ * The optional, opaque, per-call correlation marker.
113
+ *
114
+ * It answers "which caller-side request does this send answer?", so the code
115
+ * that receives `transaction:broadcasted` can match the event to the in-flight
116
+ * send it issued, without an out-of-band map keyed on something like
117
+ * `from:nonce` (which is not unique: cancelling a stuck transaction creates a
118
+ * second operation at the same nonce by design).
119
+ *
120
+ * Three fields are carried on a tracked transaction and they answer three
121
+ * different questions:
122
+ *
123
+ * | field | question | lifetime |
124
+ * | ------------- | -------------------------------------------- | ------------------- |
125
+ * | `metadata` | what the application says the tx MEANS | persisted |
126
+ * | `source` | WHICH SIGNING ROUTE produced it | persisted |
127
+ * | `correlation` | which CALLER-SIDE REQUEST this send answers | ephemeral plumbing |
128
+ *
129
+ * `correlation` is deliberately NOT any of the following:
130
+ *
131
+ * - It is not intended to be persisted. It is routing plumbing for the session
132
+ * that issued the send, not a property of the transaction.
133
+ * - It is not an identity for the transaction. `hash` is that.
134
+ * - It is meaningless outside the session that issued the send, so a value
135
+ * rehydrated from storage carries no information.
136
+ *
137
+ * The tracker never inspects, interprets, validates or defaults it, exactly as
138
+ * with `source`. It is a sibling of `metadata`, never nested inside it,
139
+ * precisely because metadata is what gets persisted.
140
+ */
141
+ export type CorrelationField = {
142
+ /**
143
+ * Opaque per-call marker tying this send back to the caller-side request
144
+ * that issued it. Carried verbatim onto the emitted transaction. Ephemeral:
145
+ * not intended to be persisted, not an identity, meaningless outside the
146
+ * issuing session. See {@link CorrelationField}.
147
+ */
148
+ correlation?: string;
149
+ };
150
+
92
151
  /**
93
152
  * Access list type used in EIP-2930 and EIP-1559 transactions.
94
153
  */
@@ -113,11 +172,40 @@ export type IntendedGasParameters = {
113
172
  * Common fields for all tracked transactions.
114
173
  * All field paths are stable - you can always access tx.field
115
174
  */
116
- type CommonTrackedFields<TMetadata> = {
175
+ type CommonTrackedFields<TMetadata, TSource = undefined> = {
117
176
  readonly hash: `0x${string}`;
118
177
  readonly from: `0x${string}`;
119
178
  readonly broadcastTimestampMs: number;
120
179
  readonly metadata: TMetadata;
180
+ /**
181
+ * Opaque marker of which signing route produced this transaction.
182
+ * Supplied once at client construction and carried verbatim: the tracker
183
+ * never inspects or interprets it. Sibling of (never nested inside)
184
+ * metadata, since it is a fact observed at dispatch rather than something
185
+ * the application said the transaction means.
186
+ */
187
+ readonly source: TSource;
188
+ /**
189
+ * Opaque marker of which caller-side request this send answers, supplied per
190
+ * call and carried verbatim: the tracker never inspects or interprets it.
191
+ * Carried on every emitted transaction, so it reaches `transaction:known`
192
+ * as well as `transaction:broadcasted`.
193
+ *
194
+ * Ephemeral plumbing, unlike its two siblings: `metadata` is what the
195
+ * application says the transaction MEANS and `source` is WHICH SIGNING ROUTE
196
+ * produced it, both of which consumers persist. `correlation` is not
197
+ * intended to be persisted, is not an identity for the transaction, and is
198
+ * meaningless outside the session that issued the send.
199
+ * See {@link CorrelationField} for the full three-way distinction.
200
+ *
201
+ * Optional here, unlike the required `source` above, and deliberately so.
202
+ * The property is in fact always present at runtime, so `string | undefined`
203
+ * would be the more faithful type, but making it required would stop every
204
+ * consumer-authored tracked-transaction literal (mocks, fixtures, values
205
+ * rehydrated from storage) from compiling, turning an additive change into a
206
+ * breaking one. Do not "tidy" this into a required property.
207
+ */
208
+ readonly correlation?: string;
121
209
  readonly to: `0x${string}` | null;
122
210
  readonly value: bigint;
123
211
  readonly data: `0x${string}`;
@@ -125,9 +213,10 @@ type CommonTrackedFields<TMetadata> = {
125
213
  };
126
214
 
127
215
  /**
128
- * A fully known tracked transaction with all fields confirmed from chain.
129
- * Emitted via transaction:fetched when tx data is fetched from chain,
130
- * or immediately for sendRawTransaction where we can parse the tx.
216
+ * A fully known tracked transaction: every field is a final value.
217
+ * Emitted via transaction:known, either once the tx has been read back from
218
+ * the chain, or immediately for sendRawTransaction where the signed payload
219
+ * can be parsed.
131
220
  *
132
221
  * When known=true, all values are the actual confirmed values used by the chain.
133
222
  * Uses discriminated union by txType to enforce correct field combinations.
@@ -135,39 +224,41 @@ type CommonTrackedFields<TMetadata> = {
135
224
  * - accessList at top level, presence depends on txType
136
225
  * - gasParameters at top level with txType-specific fields
137
226
  */
138
- export type KnownTrackedTransaction<TMetadata> =
139
- CommonTrackedFields<TMetadata> & {
140
- readonly known: true;
141
- } & (
142
- | {
143
- readonly txType: 'eip1559';
144
- readonly chainId: number;
145
- readonly accessList?: AccessList;
146
- readonly gasParameters: {
147
- readonly gas: bigint;
148
- readonly maxFeePerGas: bigint;
149
- readonly maxPriorityFeePerGas: bigint;
150
- };
151
- }
152
- | {
153
- readonly txType: 'legacy';
154
- readonly chainId?: number;
155
- // accessList not available for legacy
156
- readonly gasParameters: {
157
- readonly gas: bigint;
158
- readonly gasPrice: bigint;
159
- };
160
- }
161
- | {
162
- readonly txType: 'eip2930';
163
- readonly chainId: number;
164
- readonly accessList: AccessList;
165
- readonly gasParameters: {
166
- readonly gas: bigint;
167
- readonly gasPrice: bigint;
168
- };
169
- }
170
- );
227
+ export type KnownTrackedTransaction<
228
+ TMetadata,
229
+ TSource = undefined,
230
+ > = CommonTrackedFields<TMetadata, TSource> & {
231
+ readonly known: true;
232
+ } & (
233
+ | {
234
+ readonly txType: 'eip1559';
235
+ readonly chainId: number;
236
+ readonly accessList?: AccessList;
237
+ readonly gasParameters: {
238
+ readonly gas: bigint;
239
+ readonly maxFeePerGas: bigint;
240
+ readonly maxPriorityFeePerGas: bigint;
241
+ };
242
+ }
243
+ | {
244
+ readonly txType: 'legacy';
245
+ readonly chainId?: number;
246
+ // accessList not available for legacy
247
+ readonly gasParameters: {
248
+ readonly gas: bigint;
249
+ readonly gasPrice: bigint;
250
+ };
251
+ }
252
+ | {
253
+ readonly txType: 'eip2930';
254
+ readonly chainId: number;
255
+ readonly accessList: AccessList;
256
+ readonly gasParameters: {
257
+ readonly gas: bigint;
258
+ readonly gasPrice: bigint;
259
+ };
260
+ }
261
+ );
171
262
 
172
263
  /**
173
264
  * A partially known tracked transaction with intended/provided values.
@@ -184,58 +275,61 @@ export type KnownTrackedTransaction<TMetadata> =
184
275
  * - gasPrice only → 'legacy'
185
276
  * - undefined → wallet will determine type
186
277
  */
187
- export type UnknownTrackedTransaction<TMetadata> =
188
- CommonTrackedFields<TMetadata> & {
189
- readonly known: false;
190
- } & (
191
- | {
192
- // txType inferred as eip1559
193
- readonly txType: 'eip1559';
194
- readonly chainId?: number;
195
- readonly accessList?: AccessList;
196
- readonly gasParameters: {
197
- readonly gas?: bigint;
198
- readonly maxFeePerGas?: bigint;
199
- readonly maxPriorityFeePerGas?: bigint;
200
- };
201
- }
202
- | {
203
- // txType inferred as legacy
204
- readonly txType: 'legacy';
205
- readonly chainId?: number;
206
- // no accessList for legacy
207
- readonly gasParameters: {
208
- readonly gas?: bigint;
209
- readonly gasPrice?: bigint;
210
- };
211
- }
212
- | {
213
- // txType inferred as eip2930
214
- readonly txType: 'eip2930';
215
- readonly chainId?: number;
216
- readonly accessList?: AccessList;
217
- readonly gasParameters: {
218
- readonly gas?: bigint;
219
- readonly gasPrice?: bigint;
220
- };
221
- }
222
- | {
223
- // txType unknown - wallet will decide
224
- readonly txType?: undefined;
225
- readonly chainId?: number;
226
- readonly accessList?: AccessList;
227
- readonly gasParameters: IntendedGasParameters;
228
- }
229
- );
278
+ export type UnknownTrackedTransaction<
279
+ TMetadata,
280
+ TSource = undefined,
281
+ > = CommonTrackedFields<TMetadata, TSource> & {
282
+ readonly known: false;
283
+ } & (
284
+ | {
285
+ // txType inferred as eip1559
286
+ readonly txType: 'eip1559';
287
+ readonly chainId?: number;
288
+ readonly accessList?: AccessList;
289
+ readonly gasParameters: {
290
+ readonly gas?: bigint;
291
+ readonly maxFeePerGas?: bigint;
292
+ readonly maxPriorityFeePerGas?: bigint;
293
+ };
294
+ }
295
+ | {
296
+ // txType inferred as legacy
297
+ readonly txType: 'legacy';
298
+ readonly chainId?: number;
299
+ // no accessList for legacy
300
+ readonly gasParameters: {
301
+ readonly gas?: bigint;
302
+ readonly gasPrice?: bigint;
303
+ };
304
+ }
305
+ | {
306
+ // txType inferred as eip2930
307
+ readonly txType: 'eip2930';
308
+ readonly chainId?: number;
309
+ readonly accessList?: AccessList;
310
+ readonly gasParameters: {
311
+ readonly gas?: bigint;
312
+ readonly gasPrice?: bigint;
313
+ };
314
+ }
315
+ | {
316
+ // txType unknown - wallet will decide
317
+ readonly txType?: undefined;
318
+ readonly chainId?: number;
319
+ readonly accessList?: AccessList;
320
+ readonly gasParameters: IntendedGasParameters;
321
+ }
322
+ );
230
323
 
231
324
  /**
232
325
  * A tracked transaction - discriminated by 'known' field.
233
- * - known=true: Values are confirmed from chain fetch
326
+ * - known=true: Values are final, read back from chain or parsed from a
327
+ * signed payload
234
328
  * - known=false: Values are intended/provided, may differ from actual
235
329
  */
236
- export type TrackedTransaction<TMetadata> =
237
- | KnownTrackedTransaction<TMetadata>
238
- | UnknownTrackedTransaction<TMetadata>;
330
+ export type TrackedTransaction<TMetadata, TSource = undefined> =
331
+ | KnownTrackedTransaction<TMetadata, TSource>
332
+ | UnknownTrackedTransaction<TMetadata, TSource>;
239
333
 
240
334
  /**
241
335
  * Extended WriteContractParameters with metadata and flexible nonce.
@@ -272,7 +366,8 @@ export type TrackedWriteContractParameters<
272
366
  * - undefined: fetch nonce using 'pending' (default)
273
367
  */
274
368
  nonce?: NonceOption;
275
- } & MetadataField<TMetadata>;
369
+ } & MetadataField<TMetadata> &
370
+ CorrelationField;
276
371
 
277
372
  /**
278
373
  * The fields that are auto-populated by writeContract.
@@ -364,7 +459,8 @@ export type TrackedWriteContractAutoPopulateParameters<
364
459
  * - undefined: fetch nonce using 'pending' (default)
365
460
  */
366
461
  nonce?: NonceOption;
367
- } & WriteContractAutoPopulateMetadataField<TMetadata>;
462
+ } & WriteContractAutoPopulateMetadataField<TMetadata> &
463
+ CorrelationField;
368
464
 
369
465
  /**
370
466
  * Extended SendTransactionParameters with metadata and flexible nonce.
@@ -386,7 +482,8 @@ export type TrackedSendTransactionParameters<
386
482
  * - undefined: fetch nonce using 'pending' (default)
387
483
  */
388
484
  nonce?: NonceOption;
389
- } & MetadataField<TMetadata>;
485
+ } & MetadataField<TMetadata> &
486
+ CorrelationField;
390
487
 
391
488
  /**
392
489
  * Parameters for sendRawTransaction with metadata.
@@ -398,7 +495,8 @@ export type TrackedRawTransactionParameters<TMetadata> = {
398
495
  * The RLP-encoded signed transaction.
399
496
  */
400
497
  serializedTransaction: TransactionSerialized;
401
- } & MetadataField<TMetadata>;
498
+ } & MetadataField<TMetadata> &
499
+ CorrelationField;
402
500
 
403
501
  /**
404
502
  * A wallet client wrapper that tracks transactions with metadata.
@@ -410,6 +508,7 @@ export interface TrackedWalletClient<
410
508
  TTransport extends Transport = Transport,
411
509
  TChain extends Chain | undefined = Chain | undefined,
412
510
  TAccount extends Account | undefined = Account | undefined,
511
+ TSource = undefined,
413
512
  > {
414
513
  /**
415
514
  * The underlying wallet client.
@@ -532,9 +631,11 @@ export interface TrackedWalletClient<
532
631
  * @param listener - Callback function receiving the event data
533
632
  * @returns Unsubscribe function
534
633
  */
535
- on<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
634
+ on<TEvent extends keyof TrackedWalletClientEvents<TMetadata, TSource>>(
536
635
  event: TEvent,
537
- listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
636
+ listener: (
637
+ data: TrackedWalletClientEvents<TMetadata, TSource>[TEvent],
638
+ ) => void,
538
639
  ): () => void;
539
640
 
540
641
  /**
@@ -542,25 +643,42 @@ export interface TrackedWalletClient<
542
643
  * @param event - The event type to unsubscribe from
543
644
  * @param listener - The same listener function passed to on
544
645
  */
545
- off<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
646
+ off<TEvent extends keyof TrackedWalletClientEvents<TMetadata, TSource>>(
546
647
  event: TEvent,
547
- listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
648
+ listener: (
649
+ data: TrackedWalletClientEvents<TMetadata, TSource>[TEvent],
650
+ ) => void,
548
651
  ): void;
549
652
  }
550
653
 
551
654
  /**
552
655
  * Event map for TrackedWalletClient events.
553
656
  */
554
- export type TrackedWalletClientEvents<TMetadata> = {
657
+ export type TrackedWalletClientEvents<TMetadata, TSource = undefined> = {
555
658
  /**
556
659
  * Emitted immediately after a transaction is successfully broadcast.
557
660
  */
558
- 'transaction:broadcasted': TrackedTransaction<TMetadata>;
661
+ 'transaction:broadcasted': TrackedTransaction<TMetadata, TSource>;
559
662
  /**
560
- * Emitted when full transaction data is successfully fetched from chain.
561
- * Not guaranteed to fire if fetch fails (tx not in mempool yet, network issues, etc.)
663
+ * Emitted when the transaction's values are known to be the final ones
664
+ * rather than the ones that were merely intended.
665
+ *
666
+ * The name states the promise, not the mechanism, because the mechanism
667
+ * differs by path: the values are read back from the chain for a normal
668
+ * send, and parsed from the signed payload for a raw send. Both yield final
669
+ * values, so both emit this event, and a consumer can persist on it alone.
670
+ *
671
+ * Note the two paths differ in RELIABILITY, which the name cannot express:
672
+ *
673
+ * - Normal sends: best effort. If the read fails (not in the mempool yet,
674
+ * network trouble) this simply never fires and a warning is logged.
675
+ * - Raw sends: always fires, immediately, since nothing can fail. The same
676
+ * object is delivered to `transaction:broadcasted` first.
677
+ *
678
+ * So treat `transaction:broadcasted` as the event you must handle, and this
679
+ * one as a refinement that is usually but not always delivered.
562
680
  */
563
- 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
681
+ 'transaction:known': KnownTrackedTransaction<TMetadata, TSource>;
564
682
  };
565
683
 
566
684
  /**
@@ -573,6 +691,7 @@ export interface TrackedWalletClientAutoPopulate<
573
691
  TTransport extends Transport = Transport,
574
692
  TChain extends Chain | undefined = Chain | undefined,
575
693
  TAccount extends Account | undefined = Account | undefined,
694
+ TSource = undefined,
576
695
  > {
577
696
  /**
578
697
  * The underlying wallet client.
@@ -697,9 +816,11 @@ export interface TrackedWalletClientAutoPopulate<
697
816
  * @param listener - Callback function receiving the event data
698
817
  * @returns Unsubscribe function
699
818
  */
700
- on<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
819
+ on<TEvent extends keyof TrackedWalletClientEvents<TMetadata, TSource>>(
701
820
  event: TEvent,
702
- listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
821
+ listener: (
822
+ data: TrackedWalletClientEvents<TMetadata, TSource>[TEvent],
823
+ ) => void,
703
824
  ): () => void;
704
825
 
705
826
  /**
@@ -707,9 +828,11 @@ export interface TrackedWalletClientAutoPopulate<
707
828
  * @param event - The event type to unsubscribe from
708
829
  * @param listener - The same listener function passed to on
709
830
  */
710
- off<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
831
+ off<TEvent extends keyof TrackedWalletClientEvents<TMetadata, TSource>>(
711
832
  event: TEvent,
712
- listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
833
+ listener: (
834
+ data: TrackedWalletClientEvents<TMetadata, TSource>[TEvent],
835
+ ) => void,
713
836
  ): void;
714
837
  }
715
838
 
@@ -724,6 +847,7 @@ export interface TrackedWalletClientAutoPopulate<
724
847
  * @typeParam TTransport - The transport type (default: Transport)
725
848
  * @typeParam TChain - The chain type (default: Chain | undefined)
726
849
  * @typeParam TAccount - The account type (default: Account | undefined)
850
+ * @typeParam TSource - The opaque signing-route marker (default: undefined)
727
851
  *
728
852
  * @example
729
853
  * ```typescript
@@ -741,6 +865,17 @@ export interface TrackedWalletClientAutoPopulate<
741
865
  * type MyExtendedMetadata = FunctionCallMetadata & { priority: number };
742
866
  * type MyExtendedClient = TrackedWalletClientType<MyExtendedMetadata, true>;
743
867
  *
868
+ * // With an opaque source describing which signing route produced the tx
869
+ * type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
870
+ * type MySourcedClient = TrackedWalletClientType<
871
+ * {purpose: string},
872
+ * false,
873
+ * Transport,
874
+ * Chain | undefined,
875
+ * Account | undefined,
876
+ * MySource
877
+ * >;
878
+ *
744
879
  * // With specific wallet client types
745
880
  * type MySpecificClient = TrackedWalletClientType<
746
881
  * {purpose: string},
@@ -757,6 +892,13 @@ export type TrackedWalletClientType<
757
892
  TTransport extends Transport = Transport,
758
893
  TChain extends Chain | undefined = Chain | undefined,
759
894
  TAccount extends Account | undefined = Account | undefined,
895
+ TSource = undefined,
760
896
  > = TAutoPopulate extends true
761
- ? TrackedWalletClientAutoPopulate<TMetadata, TTransport, TChain, TAccount>
762
- : TrackedWalletClient<TMetadata, TTransport, TChain, TAccount>;
897
+ ? TrackedWalletClientAutoPopulate<
898
+ TMetadata,
899
+ TTransport,
900
+ TChain,
901
+ TAccount,
902
+ TSource
903
+ >
904
+ : TrackedWalletClient<TMetadata, TTransport, TChain, TAccount, TSource>;