@etherkit/viem-tx-tracker 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ export type * from './types.js';
5
5
  export {
6
6
  createTrackedWalletClient,
7
7
  type TrackedWalletClientBuilder,
8
+ type TrackedWalletClientAutoPopulateBuilder,
8
9
  } from './TrackedWalletClient.js';
package/src/types.ts CHANGED
@@ -15,6 +15,58 @@ import type {
15
15
  WriteContractParameters,
16
16
  } from 'viem';
17
17
 
18
+ /**
19
+ * Metadata for contract function calls.
20
+ * Auto-populated by writeContract when populateMetadata: true.
21
+ */
22
+ export type FunctionCallMetadata = {
23
+ type: 'functionCall';
24
+ functionName: string;
25
+ args?: readonly unknown[];
26
+ };
27
+
28
+ /**
29
+ * Metadata for unknown/untyped operations.
30
+ * Used as a fallback for sendTransaction/sendRawTransaction
31
+ * when using the default PopulatedMetadata type.
32
+ */
33
+ export type UnknownTypeMetadata = {
34
+ type: 'unknown';
35
+ name: string;
36
+ data: any[];
37
+ };
38
+
39
+ /**
40
+ * Default metadata type when using populateMetadata: true.
41
+ * A discriminated union that allows either:
42
+ * - FunctionCallMetadata (auto-populated by writeContract)
43
+ * - UnknownTypeMetadata (for sendTransaction/sendRawTransaction)
44
+ *
45
+ * Users can provide their own TMetadata type that excludes 'unknown'
46
+ * if they want to enforce specific operation types.
47
+ */
48
+ export type PopulatedMetadata = FunctionCallMetadata | UnknownTypeMetadata;
49
+
50
+ /**
51
+ * Options for creating a tracked wallet client.
52
+ */
53
+ export interface CreateTrackedWalletClientOptions<
54
+ TPopulate extends boolean = false,
55
+ > {
56
+ /**
57
+ * When true, writeContract and writeContractSync automatically populate
58
+ * operation, functionName and args in the metadata from the contract call parameters.
59
+ * TMetadata must be a type where FunctionCallMetadata is assignable to it
60
+ * (e.g., PopulatedMetadata, FunctionCallMetadata, or a union including FunctionCallMetadata).
61
+ */
62
+ populateMetadata?: TPopulate;
63
+ /**
64
+ * A clock function that returns the current time in milliseconds.
65
+ * Defaults to Date.now.
66
+ */
67
+ clock?: () => number;
68
+ }
69
+
18
70
  /**
19
71
  * Block tags that can be used to specify nonce fetching strategy
20
72
  */
@@ -28,29 +80,6 @@ export type BlockTag = 'latest' | 'pending' | 'earliest' | 'safe' | 'finalized';
28
80
  */
29
81
  export type NonceOption = number | BlockTag;
30
82
 
31
- export type ExpectedUpdate =
32
- | {
33
- address: `0x${string}`;
34
- event: {topics: `0x${string}`[]};
35
- }
36
- | {
37
- address: `0x${string}`;
38
- call: {data: `0x${string}`; result: `0x${string}`};
39
- };
40
-
41
- /**
42
- * Metadata that can be attached to a transaction for tracking purposes.
43
- * All fields are optional and extensible.
44
- */
45
- export interface TransactionMetadata {
46
- id?: string;
47
- name?: string;
48
- args?: any[];
49
- description?: string;
50
- expectedUpdate?: ExpectedUpdate;
51
- [key: string]: unknown;
52
- }
53
-
54
83
  /**
55
84
  * Conditional type that makes metadata required or optional based on TMetadata.
56
85
  * If TMetadata includes undefined (e.g., `MyMeta | undefined`), metadata is optional.
@@ -61,17 +90,97 @@ export type MetadataField<TMetadata> = undefined extends TMetadata
61
90
  : {metadata: TMetadata};
62
91
 
63
92
  /**
64
- * A tracked transaction record with all relevant information for tracking.
65
- * The metadata field type matches what was provided to the TrackedWalletClient.
93
+ * Access list type used in EIP-2930 and EIP-1559 transactions.
66
94
  */
67
- export interface TrackedTransaction<TMetadata> {
68
- chainId?: number;
95
+ export type AccessList = readonly {
96
+ address: Address;
97
+ storageKeys: readonly `0x${string}`[];
98
+ }[];
99
+
100
+ /**
101
+ * Base transaction fields present in all tracked transactions.
102
+ */
103
+ export type BaseTrackedTransaction<TMetadata> = {
104
+ readonly chainId?: number;
69
105
  readonly hash: `0x${string}`;
70
106
  readonly from: `0x${string}`;
71
- nonce?: number;
72
107
  readonly broadcastTimestampMs: number;
73
108
  readonly metadata: TMetadata;
74
- }
109
+ };
110
+
111
+ /**
112
+ * A fully known tracked transaction with all fields confirmed from chain.
113
+ * Emitted via transaction:fetched when tx data is fetched from chain,
114
+ * or immediately for sendRawTransaction where we can parse the tx.
115
+ *
116
+ * When known=true, all values are the actual confirmed values used by the chain.
117
+ */
118
+ export type KnownTrackedTransaction<TMetadata> =
119
+ BaseTrackedTransaction<TMetadata> & {
120
+ readonly known: true;
121
+ readonly to: `0x${string}` | null;
122
+ readonly nonce: number;
123
+ readonly value: bigint;
124
+ readonly data: `0x${string}`;
125
+ readonly gas: bigint;
126
+ } & (
127
+ | {
128
+ readonly txType: 'eip1559';
129
+ readonly chainId: number; // Required for EIP-1559
130
+ readonly maxFeePerGas: bigint;
131
+ readonly maxPriorityFeePerGas: bigint;
132
+ readonly accessList?: AccessList; // EIP-1559 can also have access lists
133
+ }
134
+ | {
135
+ readonly txType: 'legacy';
136
+ readonly chainId?: number; // Optional for legacy (pre-EIP-155 txs don't have it)
137
+ readonly gasPrice: bigint;
138
+ }
139
+ | {
140
+ readonly txType: 'eip2930';
141
+ readonly chainId: number; // Required for EIP-2930
142
+ readonly gasPrice: bigint;
143
+ readonly accessList: AccessList; // Required for EIP-2930
144
+ }
145
+ );
146
+
147
+ /**
148
+ * A partially known tracked transaction with intended/provided values.
149
+ * Emitted immediately via transaction:broadcasted.
150
+ *
151
+ * When known=false, values are what we intended/provided, but the wallet
152
+ * may have modified them (e.g., gas estimation, nonce override).
153
+ * All optional fields are populated if we have the data.
154
+ *
155
+ * txType is inferred from provided params:
156
+ * - maxFeePerGas provided → 'eip1559'
157
+ * - gasPrice + accessList provided → 'eip2930'
158
+ * - gasPrice only → 'legacy'
159
+ * - undefined → wallet will determine type
160
+ */
161
+ export type UnknownTrackedTransaction<TMetadata> =
162
+ BaseTrackedTransaction<TMetadata> & {
163
+ readonly known: false;
164
+ readonly txType?: 'eip1559' | 'legacy' | 'eip2930'; // Inferred from params if possible
165
+ readonly to?: `0x${string}` | null;
166
+ readonly nonce?: number;
167
+ readonly value?: bigint;
168
+ readonly data?: `0x${string}`;
169
+ readonly gas?: bigint;
170
+ readonly gasPrice?: bigint;
171
+ readonly maxFeePerGas?: bigint;
172
+ readonly maxPriorityFeePerGas?: bigint;
173
+ readonly accessList?: AccessList;
174
+ };
175
+
176
+ /**
177
+ * A tracked transaction - discriminated by 'known' field.
178
+ * - known=true: Values are confirmed from chain fetch
179
+ * - known=false: Values are intended/provided, may differ from actual
180
+ */
181
+ export type TrackedTransaction<TMetadata> =
182
+ | KnownTrackedTransaction<TMetadata>
183
+ | UnknownTrackedTransaction<TMetadata>;
75
184
 
76
185
  /**
77
186
  * Extended WriteContractParameters with metadata and flexible nonce.
@@ -110,6 +219,98 @@ export type TrackedWriteContractParameters<
110
219
  nonce?: NonceOption;
111
220
  } & MetadataField<TMetadata>;
112
221
 
222
+ /**
223
+ * The fields that are auto-populated by writeContract.
224
+ * These are excluded from user-provided metadata for writeContract.
225
+ */
226
+ export type AutoPopulatedFields = 'type' | 'functionName' | 'args';
227
+
228
+ /**
229
+ * Type that explicitly forbids auto-populated fields.
230
+ * Uses never to make TypeScript error when these properties are provided.
231
+ */
232
+ export type ForbiddenAutoPopulateFields = {
233
+ type?: never;
234
+ functionName?: never;
235
+ args?: never;
236
+ };
237
+
238
+ /**
239
+ * Metadata type for writeContract when auto-population is enabled.
240
+ * Excludes operation, functionName and args since they will be auto-populated,
241
+ * and explicitly forbids them to cause TypeScript errors if provided.
242
+ *
243
+ * User provides the remaining fields (e.g., purpose, priority), and the
244
+ * auto-populated fields (operation, functionName, args) are added at runtime.
245
+ */
246
+ export type WriteContractAutoPopulateMetadata<TMetadata> = Omit<
247
+ TMetadata,
248
+ AutoPopulatedFields
249
+ > &
250
+ ForbiddenAutoPopulateFields;
251
+
252
+ /**
253
+ * Metadata field for writeContract when auto-population is enabled.
254
+ * Excludes operation, functionName and args since they will be auto-populated.
255
+ *
256
+ * If the remaining fields (after removing auto-populated fields) are all optional,
257
+ * the metadata field itself becomes optional.
258
+ */
259
+ export type WriteContractAutoPopulateMetadataField<TMetadata> =
260
+ // Check if there are any remaining required keys after removing auto-populated fields
261
+ keyof Omit<TMetadata, AutoPopulatedFields> extends never
262
+ ? // No other fields - metadata is optional (can be omitted entirely)
263
+ {metadata?: WriteContractAutoPopulateMetadata<TMetadata>}
264
+ : // Check if all remaining fields are optional
265
+ Partial<Omit<TMetadata, AutoPopulatedFields>> extends Omit<
266
+ TMetadata,
267
+ AutoPopulatedFields
268
+ >
269
+ ? // All remaining fields are optional - metadata field is optional
270
+ {metadata?: WriteContractAutoPopulateMetadata<TMetadata>}
271
+ : // Some fields are required - metadata field is required
272
+ {metadata: WriteContractAutoPopulateMetadata<TMetadata>};
273
+
274
+ /**
275
+ * Extended WriteContractParameters with auto-populated metadata.
276
+ * operation, functionName and args are excluded from the metadata since they're auto-populated.
277
+ *
278
+ * User provides any additional fields required by their TMetadata (e.g., purpose, priority),
279
+ * and the auto-populated fields are merged at runtime.
280
+ */
281
+ export type TrackedWriteContractAutoPopulateParameters<
282
+ TMetadata,
283
+ TAbi extends Abi | readonly unknown[] = Abi,
284
+ TFunctionName extends ContractFunctionName<TAbi, 'nonpayable' | 'payable'> =
285
+ ContractFunctionName<TAbi, 'nonpayable' | 'payable'>,
286
+ TArgs extends ContractFunctionArgs<
287
+ TAbi,
288
+ 'nonpayable' | 'payable',
289
+ TFunctionName
290
+ > = ContractFunctionArgs<TAbi, 'nonpayable' | 'payable', TFunctionName>,
291
+ TChain extends Chain | undefined = Chain | undefined,
292
+ TAccount extends Account | undefined = Account | undefined,
293
+ TChainOverride extends Chain | undefined = Chain | undefined,
294
+ > = Omit<
295
+ WriteContractParameters<
296
+ TAbi,
297
+ TFunctionName,
298
+ TArgs,
299
+ TChain,
300
+ TAccount,
301
+ TChainOverride
302
+ >,
303
+ 'nonce'
304
+ > & {
305
+ /**
306
+ * Nonce option:
307
+ * - number: exact nonce to use
308
+ * - BlockTag ('latest', 'pending', etc.): fetch nonce using this block tag
309
+ * - undefined: fetch nonce using 'pending' (default)
310
+ */
311
+ nonce?: NonceOption;
312
+ } & WriteContractAutoPopulateMetadataField<TMetadata>;
313
+
113
314
  /**
114
315
  * Extended SendTransactionParameters with metadata and flexible nonce.
115
316
  * Metadata is required unless TMetadata includes undefined.
@@ -271,20 +472,188 @@ export interface TrackedWalletClient<
271
472
  // ============================================
272
473
 
273
474
  /**
274
- * Subscribe to transaction broadcast events.
275
- * Called immediately after a transaction is successfully broadcast.
276
- * @param listener - Callback function receiving TrackedTransaction with TMetadata
475
+ * Subscribe to transaction events.
476
+ * @param event - The event type to subscribe to
477
+ * @param listener - Callback function receiving the event data
478
+ * @returns Unsubscribe function
479
+ */
480
+ on<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
481
+ event: TEvent,
482
+ listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
483
+ ): () => void;
484
+
485
+ /**
486
+ * Unsubscribe from transaction events.
487
+ * @param event - The event type to unsubscribe from
488
+ * @param listener - The same listener function passed to on
489
+ */
490
+ off<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
491
+ event: TEvent,
492
+ listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
493
+ ): void;
494
+ }
495
+
496
+ /**
497
+ * Event map for TrackedWalletClient events.
498
+ */
499
+ export type TrackedWalletClientEvents<TMetadata> = {
500
+ /**
501
+ * Emitted immediately after a transaction is successfully broadcast.
502
+ */
503
+ 'transaction:broadcasted': TrackedTransaction<TMetadata>;
504
+ /**
505
+ * Emitted when full transaction data is successfully fetched from chain.
506
+ * Not guaranteed to fire if fetch fails (tx not in mempool yet, network issues, etc.)
507
+ */
508
+ 'transaction:fetched': KnownTrackedTransaction<TMetadata>;
509
+ };
510
+
511
+ /**
512
+ * A wallet client wrapper that tracks transactions with auto-populated metadata.
513
+ * TMetadata must be a type where FunctionCallMetadata is assignable to it.
514
+ * writeContract and writeContractSync automatically populate operation, functionName and args.
515
+ */
516
+ export interface TrackedWalletClientAutoPopulate<
517
+ TMetadata,
518
+ TTransport extends Transport = Transport,
519
+ TChain extends Chain | undefined = Chain | undefined,
520
+ TAccount extends Account | undefined = Account | undefined,
521
+ > {
522
+ /**
523
+ * The underlying wallet client.
524
+ */
525
+ readonly walletClient: WalletClient<TTransport, TChain, TAccount>;
526
+
527
+ /**
528
+ * The public client used for nonce fetching and tx verification.
529
+ */
530
+ readonly publicClient: PublicClient;
531
+
532
+ // ============================================
533
+ // Async methods (return hash immediately after broadcast)
534
+ // ============================================
535
+
536
+ /**
537
+ * Write to a contract with metadata tracking.
538
+ * functionName and args are automatically populated from the contract call.
539
+ * Returns immediately after broadcast with the transaction hash.
540
+ */
541
+ writeContract<
542
+ const TAbi extends Abi | readonly unknown[],
543
+ TFunctionName extends ContractFunctionName<TAbi, 'nonpayable' | 'payable'>,
544
+ TArgs extends ContractFunctionArgs<
545
+ TAbi,
546
+ 'nonpayable' | 'payable',
547
+ TFunctionName
548
+ >,
549
+ TChainOverride extends Chain | undefined = undefined,
550
+ >(
551
+ args: TrackedWriteContractAutoPopulateParameters<
552
+ TMetadata,
553
+ TAbi,
554
+ TFunctionName,
555
+ TArgs,
556
+ TChain,
557
+ TAccount,
558
+ TChainOverride
559
+ >,
560
+ ): Promise<Hash>;
561
+
562
+ /**
563
+ * Send a transaction with metadata tracking.
564
+ * Returns immediately after broadcast with the transaction hash.
565
+ */
566
+ sendTransaction<TChainOverride extends Chain | undefined = undefined>(
567
+ args: TrackedSendTransactionParameters<
568
+ TMetadata,
569
+ TChain,
570
+ TAccount,
571
+ TChainOverride
572
+ >,
573
+ ): Promise<Hash>;
574
+
575
+ /**
576
+ * Send a signed raw transaction with metadata tracking.
577
+ * The nonce and from address are decoded from the serialized transaction.
578
+ * Returns immediately after broadcast with the transaction hash.
579
+ */
580
+ sendRawTransaction(
581
+ args: TrackedRawTransactionParameters<TMetadata>,
582
+ ): Promise<Hash>;
583
+
584
+ // ============================================
585
+ // Sync methods (wait for confirmation, return receipt)
586
+ // ============================================
587
+
588
+ /**
589
+ * Write to a contract and wait for confirmation.
590
+ * functionName and args are automatically populated from the contract call.
591
+ * Returns the transaction receipt after the transaction is confirmed.
592
+ */
593
+ writeContractSync<
594
+ const TAbi extends Abi | readonly unknown[],
595
+ TFunctionName extends ContractFunctionName<TAbi, 'nonpayable' | 'payable'>,
596
+ TArgs extends ContractFunctionArgs<
597
+ TAbi,
598
+ 'nonpayable' | 'payable',
599
+ TFunctionName
600
+ >,
601
+ TChainOverride extends Chain | undefined = undefined,
602
+ >(
603
+ args: TrackedWriteContractAutoPopulateParameters<
604
+ TMetadata,
605
+ TAbi,
606
+ TFunctionName,
607
+ TArgs,
608
+ TChain,
609
+ TAccount,
610
+ TChainOverride
611
+ >,
612
+ ): Promise<TransactionReceipt>;
613
+
614
+ /**
615
+ * Send a transaction and wait for confirmation.
616
+ * Returns the transaction receipt after the transaction is confirmed.
617
+ */
618
+ sendTransactionSync<TChainOverride extends Chain | undefined = undefined>(
619
+ args: TrackedSendTransactionParameters<
620
+ TMetadata,
621
+ TChain,
622
+ TAccount,
623
+ TChainOverride
624
+ >,
625
+ ): Promise<TransactionReceipt>;
626
+
627
+ /**
628
+ * Send a signed raw transaction and wait for confirmation.
629
+ * Returns the transaction receipt after the transaction is confirmed.
630
+ */
631
+ sendRawTransactionSync(
632
+ args: TrackedRawTransactionParameters<TMetadata>,
633
+ ): Promise<TransactionReceipt>;
634
+
635
+ // ============================================
636
+ // Event subscription methods
637
+ // ============================================
638
+
639
+ /**
640
+ * Subscribe to transaction events.
641
+ * @param event - The event type to subscribe to
642
+ * @param listener - Callback function receiving the event data
277
643
  * @returns Unsubscribe function
278
644
  */
279
- onTransactionBroadcasted(
280
- listener: (event: TrackedTransaction<TMetadata>) => void,
645
+ on<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
646
+ event: TEvent,
647
+ listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
281
648
  ): () => void;
282
649
 
283
650
  /**
284
- * Unsubscribe from transaction broadcast events.
285
- * @param listener - The same listener function passed to onTransactionBroadcasted
651
+ * Unsubscribe from transaction events.
652
+ * @param event - The event type to unsubscribe from
653
+ * @param listener - The same listener function passed to on
286
654
  */
287
- offTransactionBroadcasted(
288
- listener: (event: TrackedTransaction<TMetadata>) => void,
655
+ off<TEvent extends keyof TrackedWalletClientEvents<TMetadata>>(
656
+ event: TEvent,
657
+ listener: (data: TrackedWalletClientEvents<TMetadata>[TEvent]) => void,
289
658
  ): void;
290
659
  }