@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.
package/README.md CHANGED
@@ -1,322 +1,251 @@
1
- # @etherkit/tx-observer
1
+ # @etherkit/viem-tx-tracker
2
2
 
3
- A TypeScript library for monitoring Ethereum onchain operations containing multiple transactions, with automatic status merging and finality tracking.
3
+ A thin wrapper around a viem `WalletClient` that records what you sent, so you can track it afterwards.
4
4
 
5
5
  ## Overview
6
6
 
7
- The Operation Processor tracks **operations** - logical groupings of transactions that belong together. This is useful when:
7
+ viem gives you a transaction hash. That is rarely enough: to show a meaningful pending state, to resume tracking after a reload, or to replace a stuck transaction, you also need to know what the transaction was for, who sent it, with which nonce, and through which signing route.
8
8
 
9
- - **Gas price bumping**: Multiple transactions with the same nonce but different gas prices
10
- - **Sequential retries**: Transactions with different nonces for the same logical action
11
- - **Multi-step operations**: Related transactions that form a single user action
9
+ This package wraps a `WalletClient` with the same method names (`sendTransaction`, `writeContract`, their `*Sync` variants, `sendRawTransaction`) and emits an event for every broadcast, carrying:
12
10
 
13
- The processor monitors all transactions in an operation and computes a merged status, emitting events when the operation status changes.
11
+ - the facts observed at dispatch: `hash`, `from`, `nonce`, `to`, `value`, `data`, `broadcastTimestampMs`, the intended gas parameters and inferred transaction type
12
+ - `metadata`: whatever your application says the transaction means, in your own type
13
+ - `source`: an opaque marker of which signing route produced it, in your own type
14
+
15
+ It also resolves the nonce for you (`pending` by default) and, once broadcast, fetches the transaction back from the chain and emits a second event with the values the chain actually recorded.
16
+
17
+ The tracker never interprets `metadata` or `source`. It carries them.
14
18
 
15
19
  ## Installation
16
20
 
17
21
  ```bash
18
- npm install @etherkit/tx-observer
22
+ npm install @etherkit/viem-tx-tracker viem
19
23
  ```
20
24
 
21
- ## Quick Start
25
+ `viem` (`^2.46.3`) is a peer dependency.
26
+
27
+ ## Quick start
22
28
 
23
29
  ```typescript
24
- import { createTransactionObserver } from '@etherkit/tx-observer';
25
- import type { OnchainOperation, BroadcastedTransaction, OnchainOperationEvent } from '@etherkit/tx-observer';
26
-
27
- // Initialize the processor
28
- const processor = createTransactionObserver({
29
- finality: 12, // blocks until considered final
30
- throttle: 5000, // optional: throttle process() calls
31
- provider: window.ethereum,
32
- });
30
+ import {createPublicClient, createWalletClient, custom, http, parseEther} from 'viem';
31
+ import {mainnet} from 'viem/chains';
32
+ import {createTrackedWalletClient} from '@etherkit/viem-tx-tracker';
33
33
 
34
- // Create an operation with one or more transactions
35
- const operation: OnchainOperation = {
36
- transactions: [
37
- {
38
- hash: '0xabc...',
39
- from: '0x123...',
40
- nonce: 5,
41
- broadcastTimestamp: Date.now(),
42
- },
43
- ],,
44
- };
34
+ // What a transaction means, in your own words
35
+ type MyMetadata = {id: string; title: string};
36
+
37
+ const walletClient = createWalletClient({chain: mainnet, transport: custom(window.ethereum)});
38
+ const publicClient = createPublicClient({chain: mainnet, transport: http()});
39
+
40
+ const tracked = createTrackedWalletClient<MyMetadata>().using(walletClient, publicClient);
45
41
 
46
- // Add the operation to tracking (ID is passed separately)
47
- processor.add('my-operation-1', operation);
48
-
49
- // Listen for operation status changes (for UI updates)
50
- processor.onOperationStatusUpdated((event: OnchainOperationEvent) => {
51
- console.log(`Operation ${event.id}: ${event.operation.state?.inclusion}`);
52
-
53
- if (event.operation.state?.inclusion === 'Included') {
54
- const winningTx = event.operation.transactions[event.operation.state.txIndex];
55
- console.log(`Status: ${event.operation.state.status}`);
56
- console.log(`Winning TX: ${winningTx.hash}`);
57
- }
58
-
59
- return () => {}; // cleanup function
42
+ tracked.on('transaction:broadcasted', (tx) => {
43
+ // Fires immediately. Save this: it is everything you need to resume tracking.
44
+ save(tx.hash, tx);
60
45
  });
61
46
 
62
- // Listen for any transaction changes (for persistence)
63
- processor.onOperationUpdated((event: OnchainOperationEvent) => {
64
- console.log(`Operation ${event.id} updated, save to storage`);
65
- return () => {}; // cleanup function
47
+ tracked.on('transaction:fetched', (tx) => {
48
+ // Fires once the transaction has been read back from the chain,
49
+ // with the values the chain actually recorded.
50
+ save(tx.hash, tx);
66
51
  });
67
52
 
68
- // Process periodically (check for status updates)
69
- setInterval(() => processor.process(), 5000);
53
+ const hash = await tracked.sendTransaction({
54
+ to: '0x...',
55
+ value: parseEther('0.1'),
56
+ metadata: {id: 'deposit-1', title: 'Deposit'},
57
+ });
70
58
  ```
71
59
 
72
- ## API Reference
73
-
74
- ### `createTransactionObserver(config)`
75
-
76
- Creates a new operation processor instance.
77
-
78
- **Config:**
79
-
80
- | Field | Type | Description |
81
- |-------|------|-------------|
82
- | `finality` | `number` | Number of blocks until a transaction is considered final |
83
- | `throttle` | `number?` | Optional: throttle interval in ms for `process()` calls |
84
- | `provider` | `EIP1193ProviderWithoutEvents?` | Optional Ethereum provider (can be set later) |
85
-
86
- **Returns:** Processor instance with the following methods:
60
+ The client is built in two steps. `createTrackedWalletClient<TMetadata>(options?)` fixes the types and the options, `.using(walletClient, publicClient)` binds the clients. This is what lets `TMetadata` be explicit while the transport, chain and account types stay inferred from your wallet client.
87
61
 
88
- #### `add(id: string, operation: OnchainOperation)`
62
+ ## Metadata
89
63
 
90
- Add an operation to track. If an operation with the same ID already exists, the transactions are merged into the existing operation.
64
+ `TMetadata` is yours and it is mandatory to declare. Whether the `metadata` argument is required follows from the type:
91
65
 
92
66
  ```typescript
93
- // Add new operation
94
- processor.add('my-operation-1', operation);
67
+ // metadata is required on every call
68
+ createTrackedWalletClient<{id: string}>();
95
69
 
96
- // Add another transaction to existing operation (same ID merges)
97
- processor.add('my-operation-1', {
98
- transactions: [bumpedTx], // New tx with higher gas
99
- // ... state fields
100
- });
101
- // and in case where you track the txs already you can simply re-add
102
- operation.transactions.push(bumpedTx);
103
- processor.add('my-operation-1', operation);
70
+ // metadata may be omitted
71
+ createTrackedWalletClient<{id: string} | undefined>();
104
72
  ```
105
73
 
106
- #### `addMultiple(operations: {[id: string]: OnchainOperation})`
74
+ ### Auto-populated metadata
107
75
 
108
- Add multiple operations at once.
76
+ With `populateMetadata: true`, `writeContract` and `writeContractSync` fill in the call they made:
109
77
 
110
78
  ```typescript
111
- processor.addMultiple({
112
- 'operation-1': operation1,
113
- 'operation-2': operation2,
114
- });
115
- ```
116
-
117
- #### `remove(operationId: string)`
79
+ import type {PopulatedMetadata} from '@etherkit/viem-tx-tracker';
118
80
 
119
- Remove an operation by ID and stop tracking it.
81
+ const tracked = createTrackedWalletClient({populateMetadata: true}).using(
82
+ walletClient,
83
+ publicClient,
84
+ );
120
85
 
121
- ```typescript
122
- processor.remove('my-operation-1');
86
+ await tracked.writeContract({address, abi, functionName: 'transfer', args: [to, amount]});
87
+ // metadata: {type: 'functionCall', functionName: 'transfer', args: [to, amount]}
123
88
  ```
124
89
 
125
- #### `clear()`
126
-
127
- Remove all operations.
90
+ `type`, `functionName` and `args` are then owned by the tracker: passing any of them yourself throws. You can still extend the rest of the type, in which case your own fields stay required:
128
91
 
129
92
  ```typescript
130
- processor.clear();
131
- ```
93
+ type MyMetadata = FunctionCallMetadata & {purpose: string};
132
94
 
133
- #### `process(): Promise<void>`
95
+ const tracked = createTrackedWalletClient<MyMetadata>({populateMetadata: true}).using(
96
+ walletClient,
97
+ publicClient,
98
+ );
134
99
 
135
- Check and update the status of all tracked operations. This queries the Ethereum provider for transaction receipts and updates statuses accordingly.
136
-
137
- ```typescript
138
- await processor.process();
100
+ await tracked.writeContract({address, abi, functionName: 'transfer', args, metadata: {purpose: 'checkout'}});
139
101
  ```
140
102
 
141
- #### `setProvider(provider: EIP1193Provider)`
142
-
143
- Update the Ethereum provider.
103
+ `sendTransaction` and `sendRawTransaction` have no contract call to read, so they still take metadata in full. The default `PopulatedMetadata` type includes an `UnknownTypeMetadata` variant for them.
144
104
 
145
- ```typescript
146
- processor.setProvider(newProvider);
147
- ```
105
+ ## Source: which route signed
148
106
 
149
- #### `onOperationUpdated(listener): void`
107
+ An application can have several ways to send: the wallet the user is connected with, a local signer whose key it holds, a separate wallet connection used only to pay. When one of those transactions gets stuck, replacing or cancelling it means reactivating the route that signed it and reusing that nonce. `from` is not enough to find that route again: it may be dormant, and a locked wallet will not tell you which addresses it holds.
150
108
 
151
- Subscribe to any operation changes (when any transaction in the operation changes). Useful for persistence.
109
+ Declare a `TSource` and the tracker will stamp it on every transaction:
152
110
 
153
111
  ```typescript
154
- processor.onOperationUpdated((event: OnchainOperationEvent) => {
155
- console.log(`Operation ${event.id} changed:`, event.operation);
156
- return () => {}; // cleanup
112
+ type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
113
+
114
+ const tracked = createTrackedWalletClient<MyMetadata, MySource>({
115
+ source: 'local-signer',
116
+ }).using(walletClient, publicClient);
117
+
118
+ tracked.on('transaction:broadcasted', (tx) => {
119
+ tx.source; // 'local-signer'
157
120
  });
158
121
  ```
159
122
 
160
- #### `offOperationUpdated(listener): void`
123
+ Points worth knowing:
161
124
 
162
- Unsubscribe from operation changes.
125
+ - It is supplied once, at construction, and never per call. The source is a property of the route, not of an individual transaction, and a per-call argument is something a call site can forget or state wrongly.
126
+ - It can be a thunk, `() => TSource`, evaluated at every broadcast, in the same step that stamps `from`, `nonce` and `broadcastTimestampMs`. Use this when one client can serve different wallets or accounts over its life: a value captured at construction would be silently wrong in exactly that case.
127
+ - Declaring a `TSource` that excludes `undefined` makes the option mandatory, so a route cannot be built without saying what it is. `TSource` must always be written explicitly: it is never inferred from the option value.
128
+ - It defaults to `undefined`, in which case `source` is simply `undefined` on every transaction.
129
+ - The tracker never inspects it. There is no wallet, EIP-6963 or connection-library knowledge in this package: the union is yours.
163
130
 
164
- #### `onOperationStatusUpdated(listener): void`
131
+ ## Nonces
165
132
 
166
- Subscribe to operation status changes only (when the merged status changes). Useful for UI updates.
133
+ Every method takes `nonce` as either an exact number or a block tag:
167
134
 
168
135
  ```typescript
169
- processor.onOperationStatusUpdated((event: OnchainOperationEvent) => {
170
- console.log(`Operation ${event.id} status:`, event.operation.state?.inclusion);
171
- return () => {}; // cleanup
172
- });
136
+ await tracked.sendTransaction({to, value, nonce: 42}); // exact
137
+ await tracked.sendTransaction({to, value, nonce: 'latest'}); // fetch with this block tag
138
+ await tracked.sendTransaction({to, value}); // fetch with 'pending' (default)
173
139
  ```
174
140
 
175
- #### `offOperationStatusUpdated(listener): void`
176
-
177
- Unsubscribe from operation status changes.
178
-
179
- ## Types
141
+ The resolved nonce is injected into the request and recorded on the emitted transaction, so a replacement can reuse it.
180
142
 
181
- ### `BroadcastedTransaction`
143
+ ## Events
182
144
 
183
- Represents a single broadcasted transaction.
145
+ | Event | Payload | When |
146
+ |-------|---------|------|
147
+ | `transaction:broadcasted` | `TrackedTransaction<TMetadata, TSource>` | Immediately after the broadcast succeeds |
148
+ | `transaction:fetched` | `KnownTrackedTransaction<TMetadata, TSource>` | Once the transaction has been read back from the chain |
184
149
 
185
150
  ```typescript
186
- type BroadcastedTransaction = {
187
- readonly hash: `0x${string}`;
188
- readonly from: `0x${string}`;
189
- nonce?: number;
190
- readonly broadcastTimestamp: number;
191
- state?: BroadcastedTransactionState;
192
- };
193
-
194
- type BroadcastedTransactionState =
195
- | { inclusion: 'InMemPool' | 'NotFound'; final: undefined; status: undefined }
196
- | { inclusion: 'Dropped'; final?: number; status: undefined }
197
- | { inclusion: 'Included'; status: 'Failure' | 'Success'; final?: number };
151
+ const unsubscribe = tracked.on('transaction:broadcasted', listener);
152
+ unsubscribe();
153
+ // or
154
+ tracked.off('transaction:broadcasted', listener);
198
155
  ```
199
156
 
200
- ### `OnchainOperationStatus`
157
+ `transaction:fetched` is best effort: if the fetch fails (not in the mempool yet, network trouble) it simply does not fire, and a warning is logged. Treat `transaction:broadcasted` as the event you must handle and `transaction:fetched` as a refinement.
201
158
 
202
- The merged status of all transactions in an operation.
159
+ For `sendRawTransaction` the full transaction can be parsed from the signed payload, so `transaction:broadcasted` already carries confirmed values (`known: true`) and no separate fetch is needed.
203
160
 
204
- ```typescript
205
- type OnchainOperationStatus =
206
- | { inclusion: 'InMemPool' | 'NotFound'; final: undefined; status: undefined; txIndex: undefined }
207
- | { inclusion: 'Dropped'; final?: number; status: undefined; txIndex: undefined }
208
- | { inclusion: 'Included'; status: 'Failure' | 'Success'; final?: number; txIndex: number };
209
- ```
161
+ ## The transaction shape
210
162
 
211
- - `txIndex`: Index into `transactions[]` for the "winning" transaction (first success, or first failure if all failed)
212
- - Get the winning tx hash via: `operation.transactions[operation.state.txIndex].hash`
163
+ A tracked transaction is discriminated by `known`:
213
164
 
214
- ### `OnchainOperation`
165
+ - `known: false` means the values are what was intended and provided. The wallet may still change them, typically gas and occasionally the nonce.
166
+ - `known: true` means the values are confirmed, read from the chain or parsed from a signed transaction.
215
167
 
216
- An operation containing multiple transactions.
168
+ Both share the same field paths, so `tx.hash`, `tx.metadata`, `tx.source`, `tx.gasParameters` are always reachable:
217
169
 
218
170
  ```typescript
219
- type OnchainOperation = {
220
- transactions: BroadcastedTransaction[];
221
- state?: OnchainOperationStatus;
171
+ type CommonFields = {
172
+ readonly hash: `0x${string}`;
173
+ readonly from: `0x${string}`;
174
+ readonly nonce: number;
175
+ readonly to: `0x${string}` | null;
176
+ readonly value: bigint;
177
+ readonly data: `0x${string}`;
178
+ readonly broadcastTimestampMs: number;
179
+ readonly metadata: TMetadata;
180
+ readonly source: TSource;
222
181
  };
223
182
  ```
224
183
 
225
- ### `OnchainOperationEvent`
226
-
227
- Event payload emitted by listeners, includes both the operation ID and operation data.
184
+ On top of that, a second discriminant, `txType`, carries the gas parameters that belong to that transaction type:
228
185
 
229
186
  ```typescript
230
- type OnchainOperationEvent = {
231
- id: string;
232
- operation: OnchainOperation;
233
- };
187
+ if (tx.txType === 'eip1559') {
188
+ tx.gasParameters.maxFeePerGas;
189
+ tx.gasParameters.maxPriorityFeePerGas;
190
+ } else if (tx.txType === 'legacy' || tx.txType === 'eip2930') {
191
+ tx.gasParameters.gasPrice;
192
+ }
234
193
  ```
235
194
 
236
- ## Status States
195
+ For a broadcast transaction (`known: false`) the type is inferred from what you provided: `maxFeePerGas` gives `eip1559`, `gasPrice` with an `accessList` gives `eip2930`, `gasPrice` alone gives `legacy`, and nothing gives `txType: undefined`, meaning the wallet will decide. On a fetched transaction (`known: true`) the type and its gas values are the ones the chain recorded, and every gas field is present.
237
196
 
238
- | Inclusion | Description |
239
- |-----------|-------------|
240
- | `Broadcasted` | At least one transaction is visible in the mempool |
241
- | `NotFound` | No transactions visible in mempool (may be temporary) |
242
- | `Dropped` | All transactions dropped (nonce was used by external tx) |
243
- | `Included` | At least one transaction was included in a block |
197
+ `from` and `nonce` are re-read from the chain on `transaction:fetched`. `metadata` and `source` are not observable on chain, so they are carried through from dispatch unchanged.
244
198
 
245
- ## Status Merging Logic
199
+ ## API
246
200
 
247
- When an operation contains multiple transactions, their statuses are merged using the following priority (highest wins):
201
+ ### `createTrackedWalletClient<TMetadata, TSource?>(options?)`
248
202
 
249
- 1. **Included** - Any tx included in a block operation is `Included`
250
- 2. **Broadcasted** - Any tx in mempool → operation is `Broadcasted`
251
- 3. **NotFound** - None visible → operation is `NotFound`
252
- 4. **Dropped** - ALL txs dropped → operation is `Dropped`
203
+ Returns a builder with a single method, `.using(walletClient, publicClient)`.
253
204
 
254
- ### For `Included` Operations
205
+ | Option | Type | Description |
206
+ |--------|------|-------------|
207
+ | `source` | `TSource \| (() => TSource)` | The signing route this client is. Required when `TSource` excludes `undefined`, absent by default |
208
+ | `populateMetadata` | `boolean` | Auto-populate `type`, `functionName` and `args` in `writeContract` metadata |
209
+ | `clock` | `() => number` | Current time in milliseconds, for `broadcastTimestampMs`. Defaults to `Date.now` |
255
210
 
256
- - If **any** transaction succeeded → `status: 'Success'`
257
- - If **all** included transactions failed → `status: 'Failure'`
258
- - `txIndex` points to the first successful tx, or first failure if all failed
211
+ ### Client methods
259
212
 
260
- This allows scenarios like:
261
- - Tx A (nonce 5) succeeds, Tx B (nonce 6) fails due to nonce conflict → Operation is **Success**
262
- - Tx A (nonce 5, low gas) dropped, Tx B (nonce 5, high gas) succeeds → Operation is **Success**
213
+ | Method | Returns |
214
+ |--------|---------|
215
+ | `writeContract(args)` | `Promise<Hash>` |
216
+ | `sendTransaction(args)` | `Promise<Hash>` |
217
+ | `sendRawTransaction({serializedTransaction, metadata})` | `Promise<Hash>` |
218
+ | `writeContractSync(args)` | `Promise<TransactionReceipt>` |
219
+ | `sendTransactionSync(args)` | `Promise<TransactionReceipt>` |
220
+ | `sendRawTransactionSync({serializedTransaction, metadata})` | `Promise<TransactionReceipt>` |
221
+ | `on(event, listener)` | `() => void` (unsubscribe) |
222
+ | `off(event, listener)` | `void` |
263
223
 
264
- ## Use Cases
224
+ The arguments are viem's, with `nonce` widened to accept a block tag and a `metadata` field added. The underlying clients stay reachable as `tracked.walletClient` and `tracked.publicClient`.
265
225
 
266
- ### Gas Price Bumping
226
+ The account comes from the call (`account`) or from the wallet client. If neither has one, the call throws.
267
227
 
268
- When network congestion increases, submit a replacement transaction with the same nonce but higher gas price:
228
+ ### Types
269
229
 
270
- ```typescript
271
- // Initial transaction
272
- const tx1: BroadcastedTransaction = {
273
- hash: '0x111...',
274
- from: '0xabc...',
275
- nonce: 5,
276
- broadcastTimestamp: Date.now(),
277
- state: {
278
- inclusion: 'InMemPool',
279
- status: undefined,
280
- final: undefined,
281
- },
282
- };
230
+ `TrackedTransaction`, `KnownTrackedTransaction`, `UnknownTrackedTransaction`, `TrackedWalletClient`, `TrackedWalletClientAutoPopulate`, `TrackedWalletClientEvents`, `PopulatedMetadata`, `FunctionCallMetadata`, `UnknownTypeMetadata`, `NonceOption`, `BlockTag`, `AccessList`, `IntendedGasParameters`, `CreateTrackedWalletClientOptions`.
283
231
 
284
- processor.add('transfer-1', {
285
- transactions: [tx1],
286
- state: {
287
- inclusion: 'InMemPool',
288
- status: undefined,
289
- final: undefined,
290
- txIndex: undefined,
291
- },
292
- });
232
+ `TrackedWalletClientType` is a convenience for declaring a client variable without spelling out the whole interface:
293
233
 
294
- // Later, bump gas price (same nonce)
295
- const tx2: BroadcastedTransaction = {
296
- ...tx1,
297
- hash: '0x222...', // Different hash
298
- };
299
-
300
- processor.add('transfer-1', { // Same ID → merges
301
- transactions: [tx2],
302
- });
303
-
304
- // Operation now tracks both txs
305
- // Whichever is included first determines the operation result
234
+ ```typescript
235
+ type MyClient = TrackedWalletClientType<MyMetadata>;
236
+ type MyAutoClient = TrackedWalletClientType<PopulatedMetadata, true>;
237
+ type MySourcedClient = TrackedWalletClientType<
238
+ MyMetadata,
239
+ false,
240
+ Transport,
241
+ Chain,
242
+ Account,
243
+ MySource
244
+ >;
306
245
  ```
307
246
 
308
- ### Sequential Retry
309
-
310
- If a transaction is stuck, retry with a new nonce:
247
+ `TSource` is the last type parameter on the client types and the second on the transaction types, so existing code that spelled out transport, chain and account keeps compiling untouched.
311
248
 
312
- ```typescript
313
- processor.add('my-action', {
314
- transactions: [
315
- { hash: '0x1...', from: '0xabc...', nonce: 5, broadcastTimestamp: Date.now() }, // Original
316
- { hash: '0x2...', from: '0xabc...', nonce: 6, broadcastTimestamp: Date.now() }, // Retry with new nonce
317
- ],
318
- });
249
+ ## License
319
250
 
320
- // If tx with nonce 5 succeeds, operation is Success
321
- // Even if tx with nonce 6 fails (nonce conflict), operation is still Success
322
- ```
251
+ MIT
@@ -1,5 +1,17 @@
1
1
  import { type Account, type Chain, type PublicClient, type Transport, type WalletClient } from 'viem';
2
2
  import type { CreateTrackedWalletClientOptions, PopulatedMetadata, TrackedWalletClient, TrackedWalletClientAutoPopulate } from './types.js';
3
+ /**
4
+ * Blocks inference of TSource from the `source` option value.
5
+ *
6
+ * Without this, a call that passes no explicit type arguments would infer
7
+ * TSource from the value (widening `'local-signer'` to `string`) and silently
8
+ * collapse TMetadata to `unknown`, which turns metadata typing off. TSource is
9
+ * meant to be declared, never guessed.
10
+ *
11
+ * This is TypeScript 5.4's built-in `NoInfer`, spelled out so the package keeps
12
+ * working for consumers on older TypeScript versions.
13
+ */
14
+ type NoInferSource<T> = [T][T extends any ? 0 : never];
3
15
  /**
4
16
  * Infer transport type from WalletClient
5
17
  */
@@ -15,7 +27,7 @@ type InferAccount<T> = T extends WalletClient<any, any, infer TAccount> ? TAccou
15
27
  /**
16
28
  * Builder interface returned by createTrackedWalletClient for the curried API.
17
29
  */
18
- export interface TrackedWalletClientBuilder<TMetadata> {
30
+ export interface TrackedWalletClientBuilder<TMetadata, TSource = undefined> {
19
31
  /**
20
32
  * Create the tracked wallet client using the provided wallet and public clients.
21
33
  *
@@ -23,14 +35,14 @@ export interface TrackedWalletClientBuilder<TMetadata> {
23
35
  * @param publicClient - A PublicClient for nonce fetching and tx verification
24
36
  * @returns A TrackedWalletClient instance
25
37
  */
26
- using<TClient extends WalletClient>(walletClient: TClient, publicClient: PublicClient): TrackedWalletClient<TMetadata, InferTransport<TClient>, InferChain<TClient>, InferAccount<TClient>>;
38
+ using<TClient extends WalletClient>(walletClient: TClient, publicClient: PublicClient): TrackedWalletClient<TMetadata, InferTransport<TClient>, InferChain<TClient>, InferAccount<TClient>, TSource>;
27
39
  }
28
40
  /**
29
41
  * Builder interface returned by createTrackedWalletClient with populateMetadata: true.
30
42
  * This builder returns a TrackedWalletClientAutoPopulate that auto-populates operation, functionName and args.
31
43
  * TMetadata must be a type where FunctionCallMetadata is assignable to it.
32
44
  */
33
- export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
45
+ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource = undefined> {
34
46
  /**
35
47
  * Create the tracked wallet client using the provided wallet and public clients.
36
48
  * writeContract and writeContractSync will automatically populate operation, functionName and args.
@@ -39,7 +51,7 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
39
51
  * @param publicClient - A PublicClient for nonce fetching and tx verification
40
52
  * @returns A TrackedWalletClientAutoPopulate instance
41
53
  */
42
- using<TClient extends WalletClient>(walletClient: TClient, publicClient: PublicClient): TrackedWalletClientAutoPopulate<TMetadata, InferTransport<TClient>, InferChain<TClient>, InferAccount<TClient>>;
54
+ using<TClient extends WalletClient>(walletClient: TClient, publicClient: PublicClient): TrackedWalletClientAutoPopulate<TMetadata, InferTransport<TClient>, InferChain<TClient>, InferAccount<TClient>, TSource>;
43
55
  }
44
56
  /**
45
57
  * Create a tracked wallet client that wraps a viem WalletClient.
@@ -51,6 +63,12 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
51
63
  * - Event emission for tracking
52
64
  *
53
65
  * @typeParam TMetadata - The metadata type. Use `MyMeta | undefined` to make metadata optional.
66
+ * @typeParam TSource - An opaque marker of which signing route this client is.
67
+ * Defaults to `undefined` (no source). When it does not include `undefined`,
68
+ * the `source` option is mandatory. It must always be passed explicitly:
69
+ * inference from the option value is deliberately blocked, so passing a
70
+ * `source` without also declaring TSource is an error rather than a silently
71
+ * widened type.
54
72
  * @returns A builder with a `.using()` method to provide the wallet and public clients
55
73
  *
56
74
  * @example
@@ -71,10 +89,24 @@ export interface TrackedWalletClientAutoPopulateBuilder<TMetadata> {
71
89
  * type MyMetadata = OperationMetadata & { purpose: string };
72
90
  * const tracked = createTrackedWalletClient<MyMetadata>({ populateMetadata: true })
73
91
  * .using(walletClient, publicClient);
92
+ *
93
+ * // With an opaque source marking which signing route this client is.
94
+ * // Non-undefined TSource makes the `source` option mandatory.
95
+ * type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
96
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
97
+ * source: 'local-signer',
98
+ * }).using(walletClient, publicClient);
99
+ *
100
+ * // ...or as a thunk, re-evaluated at every broadcast
101
+ * const tracked = createTrackedWalletClient<{purpose: string}, MySource>({
102
+ * source: () => currentRoute(),
103
+ * }).using(walletClient, publicClient);
74
104
  * ```
75
105
  */
76
- export declare function createTrackedWalletClient<TMetadata>(): TrackedWalletClientBuilder<TMetadata>;
106
+ export declare function createTrackedWalletClient<TMetadata, TSource = undefined>(...args: undefined extends TSource ? [
107
+ options?: CreateTrackedWalletClientOptions<false, NoInferSource<TSource>>
108
+ ] : [options: CreateTrackedWalletClientOptions<false, NoInferSource<TSource>>]): TrackedWalletClientBuilder<TMetadata, TSource>;
77
109
  export declare function createTrackedWalletClient(options: CreateTrackedWalletClientOptions<true>): TrackedWalletClientAutoPopulateBuilder<PopulatedMetadata>;
78
- export declare function createTrackedWalletClient<TMetadata>(options: CreateTrackedWalletClientOptions<true>): TrackedWalletClientAutoPopulateBuilder<TMetadata>;
110
+ export declare function createTrackedWalletClient<TMetadata, TSource = undefined>(options: CreateTrackedWalletClientOptions<true, NoInferSource<TSource>>): TrackedWalletClientAutoPopulateBuilder<TMetadata, TSource>;
79
111
  export {};
80
112
  //# sourceMappingURL=TrackedWalletClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TrackedWalletClient.d.ts","sourceRoot":"","sources":["../src/TrackedWalletClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAMN,KAAK,OAAO,EAEZ,KAAK,KAAK,EAIV,KAAK,YAAY,EAIjB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAGX,gCAAgC,EAIhC,iBAAiB,EAIjB,mBAAmB,EACnB,+BAA+B,EAI/B,MAAM,YAAY,CAAC;AAmXpB;;GAEG;AACH,KAAK,cAAc,CAAC,CAAC,IACpB,CAAC,SAAS,YAAY,CAAC,MAAM,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;AAE7E;;GAEG;AACH,KAAK,UAAU,CAAC,CAAC,IAChB,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE7E;;GAEG;AACH,KAAK,YAAY,CAAC,CAAC,IAClB,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC,GAC7C,QAAQ,GACR,OAAO,GAAG,SAAS,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,SAAS;IACpD;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,SAAS,YAAY,EACjC,YAAY,EAAE,OAAO,EACrB,YAAY,EAAE,YAAY,GACxB,mBAAmB,CACrB,SAAS,EACT,cAAc,CAAC,OAAO,CAAC,EACvB,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,CAAC,OAAO,CAAC,CACrB,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,WAAW,sCAAsC,CAAC,SAAS;IAChE;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,SAAS,YAAY,EACjC,YAAY,EAAE,OAAO,EACrB,YAAY,EAAE,YAAY,GACxB,+BAA+B,CACjC,SAAS,EACT,cAAc,CAAC,OAAO,CAAC,EACvB,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,CAAC,OAAO,CAAC,CACrB,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,wBAAgB,yBAAyB,CACxC,SAAS,KACL,0BAA0B,CAAC,SAAS,CAAC,CAAC;AAG3C,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,gCAAgC,CAAC,IAAI,CAAC,GAC7C,sCAAsC,CAAC,iBAAiB,CAAC,CAAC;AAG7D,wBAAgB,yBAAyB,CAAC,SAAS,EAClD,OAAO,EAAE,gCAAgC,CAAC,IAAI,CAAC,GAC7C,sCAAsC,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"TrackedWalletClient.d.ts","sourceRoot":"","sources":["../src/TrackedWalletClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAMN,KAAK,OAAO,EAEZ,KAAK,KAAK,EAIV,KAAK,YAAY,EAIjB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAGX,gCAAgC,EAIhC,iBAAiB,EAIjB,mBAAmB,EACnB,+BAA+B,EAI/B,MAAM,YAAY,CAAC;AA2EpB;;;;;;;;;;GAUG;AACH,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;AAkUvD;;GAEG;AACH,KAAK,cAAc,CAAC,CAAC,IACpB,CAAC,SAAS,YAAY,CAAC,MAAM,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;AAE7E;;GAEG;AACH,KAAK,UAAU,CAAC,CAAC,IAChB,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE7E;;GAEG;AACH,KAAK,YAAY,CAAC,CAAC,IAClB,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC,GAC7C,QAAQ,GACR,OAAO,GAAG,SAAS,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS;IACzE;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,SAAS,YAAY,EACjC,YAAY,EAAE,OAAO,EACrB,YAAY,EAAE,YAAY,GACxB,mBAAmB,CACrB,SAAS,EACT,cAAc,CAAC,OAAO,CAAC,EACvB,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,CAAC,OAAO,CAAC,EACrB,OAAO,CACP,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,WAAW,sCAAsC,CACtD,SAAS,EACT,OAAO,GAAG,SAAS;IAEnB;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,SAAS,YAAY,EACjC,YAAY,EAAE,OAAO,EACrB,YAAY,EAAE,YAAY,GACxB,+BAA+B,CACjC,SAAS,EACT,cAAc,CAAC,OAAO,CAAC,EACvB,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,CAAC,OAAO,CAAC,EACrB,OAAO,CACP,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAGH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,EACvE,GAAG,IAAI,EAAE,SAAS,SAAS,OAAO,GAC/B;IACA,OAAO,CAAC,EAAE,gCAAgC,CACzC,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,CACtB;CACD,GACA,CAAC,OAAO,EAAE,gCAAgC,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,GAC3E,0BAA0B,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAGlD,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,gCAAgC,CAAC,IAAI,CAAC,GAC7C,sCAAsC,CAAC,iBAAiB,CAAC,CAAC;AAG7D,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,EACvE,OAAO,EAAE,gCAAgC,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,GACrE,sCAAsC,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC"}