@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/README.md +196 -222
- package/dist/TrackedWalletClient.d.ts +38 -6
- package/dist/TrackedWalletClient.d.ts.map +1 -1
- package/dist/TrackedWalletClient.js +123 -49
- package/dist/TrackedWalletClient.js.map +1 -1
- package/dist/types.d.ts +142 -26
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrackedWalletClient.ts +294 -82
- package/src/types.ts +248 -106
package/README.md
CHANGED
|
@@ -1,322 +1,296 @@
|
|
|
1
|
-
# @etherkit/tx-
|
|
1
|
+
# @etherkit/viem-tx-tracker
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
- `correlation`: an opaque marker of which caller-side request a send answers, supplied per call
|
|
15
|
+
|
|
16
|
+
It also resolves the nonce for you (`pending` by default) and, once broadcast, reads the transaction back from the chain and emits a second event with the values that were actually recorded.
|
|
17
|
+
|
|
18
|
+
The tracker never interprets `metadata`, `source` or `correlation`. It carries them.
|
|
14
19
|
|
|
15
20
|
## Installation
|
|
16
21
|
|
|
17
22
|
```bash
|
|
18
|
-
npm install @etherkit/tx-
|
|
23
|
+
npm install @etherkit/viem-tx-tracker viem
|
|
19
24
|
```
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
`viem` (`^2.46.3`) is a peer dependency.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
22
29
|
|
|
23
30
|
```typescript
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
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
|
-
});
|
|
31
|
+
import {createPublicClient, createWalletClient, custom, http, parseEther} from 'viem';
|
|
32
|
+
import {mainnet} from 'viem/chains';
|
|
33
|
+
import {createTrackedWalletClient} from '@etherkit/viem-tx-tracker';
|
|
33
34
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
from: '0x123...',
|
|
40
|
-
nonce: 5,
|
|
41
|
-
broadcastTimestamp: Date.now(),
|
|
42
|
-
},
|
|
43
|
-
],,
|
|
44
|
-
};
|
|
35
|
+
// What a transaction means, in your own words
|
|
36
|
+
type MyMetadata = {id: string; title: string};
|
|
37
|
+
|
|
38
|
+
const walletClient = createWalletClient({chain: mainnet, transport: custom(window.ethereum)});
|
|
39
|
+
const publicClient = createPublicClient({chain: mainnet, transport: http()});
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
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
|
|
41
|
+
const tracked = createTrackedWalletClient<MyMetadata>().using(walletClient, publicClient);
|
|
42
|
+
|
|
43
|
+
tracked.on('transaction:broadcasted', (tx) => {
|
|
44
|
+
// Fires immediately. Save this: it is everything you need to resume tracking.
|
|
45
|
+
save(tx.hash, tx);
|
|
60
46
|
});
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
tracked.on('transaction:known', (tx) => {
|
|
49
|
+
// Fires once the transaction has been read back from the chain,
|
|
50
|
+
// with the values the chain actually recorded.
|
|
51
|
+
save(tx.hash, tx);
|
|
66
52
|
});
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
const hash = await tracked.sendTransaction({
|
|
55
|
+
to: '0x...',
|
|
56
|
+
value: parseEther('0.1'),
|
|
57
|
+
metadata: {id: 'deposit-1', title: 'Deposit'},
|
|
58
|
+
});
|
|
70
59
|
```
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
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.
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
## Metadata
|
|
75
64
|
|
|
76
|
-
|
|
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:
|
|
87
|
-
|
|
88
|
-
#### `add(id: string, operation: OnchainOperation)`
|
|
89
|
-
|
|
90
|
-
Add an operation to track. If an operation with the same ID already exists, the transactions are merged into the existing operation.
|
|
65
|
+
`TMetadata` is yours and it is mandatory to declare. Whether the `metadata` argument is required follows from the type:
|
|
91
66
|
|
|
92
67
|
```typescript
|
|
93
|
-
//
|
|
94
|
-
|
|
68
|
+
// metadata is required on every call
|
|
69
|
+
createTrackedWalletClient<{id: string}>();
|
|
95
70
|
|
|
96
|
-
//
|
|
97
|
-
|
|
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);
|
|
71
|
+
// metadata may be omitted
|
|
72
|
+
createTrackedWalletClient<{id: string} | undefined>();
|
|
104
73
|
```
|
|
105
74
|
|
|
106
|
-
|
|
75
|
+
### Auto-populated metadata
|
|
107
76
|
|
|
108
|
-
|
|
77
|
+
With `populateMetadata: true`, `writeContract` and `writeContractSync` fill in the call they made:
|
|
109
78
|
|
|
110
79
|
```typescript
|
|
111
|
-
|
|
112
|
-
'operation-1': operation1,
|
|
113
|
-
'operation-2': operation2,
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
#### `remove(operationId: string)`
|
|
80
|
+
import type {PopulatedMetadata} from '@etherkit/viem-tx-tracker';
|
|
118
81
|
|
|
119
|
-
|
|
82
|
+
const tracked = createTrackedWalletClient({populateMetadata: true}).using(
|
|
83
|
+
walletClient,
|
|
84
|
+
publicClient,
|
|
85
|
+
);
|
|
120
86
|
|
|
121
|
-
|
|
122
|
-
|
|
87
|
+
await tracked.writeContract({address, abi, functionName: 'transfer', args: [to, amount]});
|
|
88
|
+
// metadata: {type: 'functionCall', functionName: 'transfer', args: [to, amount]}
|
|
123
89
|
```
|
|
124
90
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Remove all operations.
|
|
91
|
+
`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
92
|
|
|
129
93
|
```typescript
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### `process(): Promise<void>`
|
|
94
|
+
type MyMetadata = FunctionCallMetadata & {purpose: string};
|
|
134
95
|
|
|
135
|
-
|
|
96
|
+
const tracked = createTrackedWalletClient<MyMetadata>({populateMetadata: true}).using(
|
|
97
|
+
walletClient,
|
|
98
|
+
publicClient,
|
|
99
|
+
);
|
|
136
100
|
|
|
137
|
-
|
|
138
|
-
await processor.process();
|
|
101
|
+
await tracked.writeContract({address, abi, functionName: 'transfer', args, metadata: {purpose: 'checkout'}});
|
|
139
102
|
```
|
|
140
103
|
|
|
141
|
-
|
|
104
|
+
`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.
|
|
142
105
|
|
|
143
|
-
|
|
106
|
+
## Source: which route signed
|
|
144
107
|
|
|
145
|
-
|
|
146
|
-
processor.setProvider(newProvider);
|
|
147
|
-
```
|
|
108
|
+
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.
|
|
148
109
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Subscribe to any operation changes (when any transaction in the operation changes). Useful for persistence.
|
|
110
|
+
Declare a `TSource` and the tracker will stamp it on every transaction:
|
|
152
111
|
|
|
153
112
|
```typescript
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
113
|
+
type MySource = 'connected-wallet' | 'local-signer' | 'payer-wallet';
|
|
114
|
+
|
|
115
|
+
const tracked = createTrackedWalletClient<MyMetadata, MySource>({
|
|
116
|
+
source: 'local-signer',
|
|
117
|
+
}).using(walletClient, publicClient);
|
|
118
|
+
|
|
119
|
+
tracked.on('transaction:broadcasted', (tx) => {
|
|
120
|
+
tx.source; // 'local-signer'
|
|
157
121
|
});
|
|
158
122
|
```
|
|
159
123
|
|
|
160
|
-
|
|
124
|
+
Points worth knowing:
|
|
125
|
+
|
|
126
|
+
- 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.
|
|
127
|
+
- 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.
|
|
128
|
+
- 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.
|
|
129
|
+
- It defaults to `undefined`, in which case `source` is simply `undefined` on every transaction.
|
|
130
|
+
- The tracker never inspects it. There is no wallet, EIP-6963 or connection-library knowledge in this package: the union is yours.
|
|
131
|
+
|
|
132
|
+
## Correlation: which request this send answers
|
|
161
133
|
|
|
162
|
-
|
|
134
|
+
A send is asynchronous in a way that matters: `transaction:broadcasted` fires after the wallet round trip, which includes a popup the user may read for a while, during which the page stays interactive and can issue further sends. So the handler has to work out which of its own in-flight requests the event belongs to.
|
|
163
135
|
|
|
164
|
-
|
|
136
|
+
`(from, nonce)` cannot answer that. Replacing or cancelling a stuck transaction deliberately creates a second operation at the *same* nonce, so two concurrent replacements collide and the handler silently attaches a broadcast to the wrong one.
|
|
165
137
|
|
|
166
|
-
|
|
138
|
+
Pass a `correlation` on the call and read it off the event:
|
|
167
139
|
|
|
168
140
|
```typescript
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
141
|
+
const correlation = crypto.randomUUID();
|
|
142
|
+
|
|
143
|
+
tracked.on('transaction:broadcasted', (tx) => {
|
|
144
|
+
// route this broadcast to the request that issued it, not to a (from, nonce) guess
|
|
145
|
+
if (tx.correlation) pendingRequests.get(tx.correlation)?.resolve(tx);
|
|
172
146
|
});
|
|
147
|
+
|
|
148
|
+
await tracked.sendTransaction({to, value, metadata: {id: 'deposit-1'}, correlation});
|
|
173
149
|
```
|
|
174
150
|
|
|
175
|
-
|
|
151
|
+
It is a sibling of `metadata` and `nonce` on every tracked send, never nested inside metadata. Three fields are now carried on a tracked transaction and they answer three different questions:
|
|
176
152
|
|
|
177
|
-
|
|
153
|
+
| Field | Question it answers | Supplied | Lifetime |
|
|
154
|
+
|-------|---------------------|----------|----------|
|
|
155
|
+
| `metadata` | What the application says the transaction **means** | Per call, in your own type | Persisted by consumers |
|
|
156
|
+
| `source` | **Which signing route** produced it, a fact observed at dispatch | Once, at client construction, in your own type | Persisted by consumers |
|
|
157
|
+
| `correlation` | Which **caller-side request** this send answers | Per call, as a `string` | Ephemeral plumbing, not persisted |
|
|
178
158
|
|
|
179
|
-
|
|
159
|
+
Points worth knowing:
|
|
180
160
|
|
|
181
|
-
|
|
161
|
+
- It is **not intended to be persisted**. That is the whole reason it is not part of `metadata`: metadata is what consumers store, so plumbing written there ends up in every stored record forever.
|
|
162
|
+
- It is **not an identity** for the transaction. `hash` is that.
|
|
163
|
+
- It is **meaningless outside the session that issued the send**. A value rehydrated from storage carries no information, because the requests it referred to are gone.
|
|
164
|
+
- It is typed `correlation?: string`, deliberately, rather than a generic type parameter: identity is all a consumer needs to route an event, and the value is not `unknown`, which would invite smuggling structured data into a field the tracker will never interpret.
|
|
165
|
+
- The tracker never inspects, interprets, validates or defaults it, exactly as with `source`. It is also removed from the arguments before they reach viem, so the wallet never sees an unrecognised field.
|
|
166
|
+
- It is optional everywhere. Omit it and `correlation` is simply `undefined` on the emitted transaction.
|
|
182
167
|
|
|
183
|
-
|
|
168
|
+
## Nonces
|
|
184
169
|
|
|
185
|
-
|
|
186
|
-
type BroadcastedTransaction = {
|
|
187
|
-
readonly hash: `0x${string}`;
|
|
188
|
-
readonly from: `0x${string}`;
|
|
189
|
-
nonce?: number;
|
|
190
|
-
readonly broadcastTimestamp: number;
|
|
191
|
-
state?: BroadcastedTransactionState;
|
|
192
|
-
};
|
|
170
|
+
Every method takes `nonce` as either an exact number or a block tag:
|
|
193
171
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
172
|
+
```typescript
|
|
173
|
+
await tracked.sendTransaction({to, value, nonce: 42}); // exact
|
|
174
|
+
await tracked.sendTransaction({to, value, nonce: 'latest'}); // fetch with this block tag
|
|
175
|
+
await tracked.sendTransaction({to, value}); // fetch with 'pending' (default)
|
|
198
176
|
```
|
|
199
177
|
|
|
200
|
-
|
|
178
|
+
The resolved nonce is injected into the request and recorded on the emitted transaction, so a replacement can reuse it.
|
|
201
179
|
|
|
202
|
-
|
|
180
|
+
## Events
|
|
181
|
+
|
|
182
|
+
| Event | Payload | When |
|
|
183
|
+
|-------|---------|------|
|
|
184
|
+
| `transaction:broadcasted` | `TrackedTransaction<TMetadata, TSource>` | Immediately after the broadcast succeeds |
|
|
185
|
+
| `transaction:known` | `KnownTrackedTransaction<TMetadata, TSource>` | Once the values are known to be final rather than intended |
|
|
203
186
|
|
|
204
187
|
```typescript
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
188
|
+
const unsubscribe = tracked.on('transaction:broadcasted', listener);
|
|
189
|
+
unsubscribe();
|
|
190
|
+
// or
|
|
191
|
+
tracked.off('transaction:broadcasted', listener);
|
|
209
192
|
```
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
- Get the winning tx hash via: `operation.transactions[operation.state.txIndex].hash`
|
|
194
|
+
`transaction:known` says the values are the final ones, not the ones you intended. It deliberately names that promise rather than the mechanism, because the mechanism differs: for a normal send the transaction is read back from the chain, and for `sendRawTransaction` the values are parsed from the signed payload, which needs no network call at all. Both produce final values, so both emit the event, and you can persist on it alone.
|
|
213
195
|
|
|
214
|
-
|
|
196
|
+
It does not say the transaction was mined. It fires while the transaction is still in the mempool, so it is not a substitute for waiting on a receipt.
|
|
215
197
|
|
|
216
|
-
|
|
198
|
+
The two paths differ in reliability, which the name cannot express, so it is worth stating plainly:
|
|
217
199
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
200
|
+
- **Normal sends**: best effort. If the read fails (not in the mempool yet, network trouble) the event simply never fires and a warning is logged.
|
|
201
|
+
- **Raw sends**: always fires, immediately, since nothing can fail. The same object is delivered to `transaction:broadcasted` first, so a handler subscribed to both sees that transaction twice.
|
|
202
|
+
|
|
203
|
+
Treat `transaction:broadcasted` as the event you must handle, and `transaction:known` as a refinement that is usually but not always delivered.
|
|
204
|
+
|
|
205
|
+
## The transaction shape
|
|
206
|
+
|
|
207
|
+
A tracked transaction is discriminated by `known`:
|
|
224
208
|
|
|
225
|
-
|
|
209
|
+
- `known: false` means the values are what was intended and provided. The wallet may still change them, typically gas and occasionally the nonce.
|
|
210
|
+
- `known: true` means the values are confirmed, read from the chain or parsed from a signed transaction.
|
|
226
211
|
|
|
227
|
-
|
|
212
|
+
Both share the same field paths, so `tx.hash`, `tx.metadata`, `tx.source`, `tx.gasParameters` are always reachable:
|
|
228
213
|
|
|
229
214
|
```typescript
|
|
230
|
-
type
|
|
231
|
-
|
|
232
|
-
|
|
215
|
+
type CommonFields = {
|
|
216
|
+
readonly hash: `0x${string}`;
|
|
217
|
+
readonly from: `0x${string}`;
|
|
218
|
+
readonly nonce: number;
|
|
219
|
+
readonly to: `0x${string}` | null;
|
|
220
|
+
readonly value: bigint;
|
|
221
|
+
readonly data: `0x${string}`;
|
|
222
|
+
readonly broadcastTimestampMs: number;
|
|
223
|
+
readonly metadata: TMetadata;
|
|
224
|
+
readonly source: TSource;
|
|
225
|
+
readonly correlation?: string;
|
|
233
226
|
};
|
|
234
227
|
```
|
|
235
228
|
|
|
236
|
-
|
|
229
|
+
On top of that, a second discriminant, `txType`, carries the gas parameters that belong to that transaction type:
|
|
237
230
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
231
|
+
```typescript
|
|
232
|
+
if (tx.txType === 'eip1559') {
|
|
233
|
+
tx.gasParameters.maxFeePerGas;
|
|
234
|
+
tx.gasParameters.maxPriorityFeePerGas;
|
|
235
|
+
} else if (tx.txType === 'legacy' || tx.txType === 'eip2930') {
|
|
236
|
+
tx.gasParameters.gasPrice;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
244
239
|
|
|
245
|
-
|
|
240
|
+
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 known transaction (`known: true`) the type and its gas values are the final ones, and every gas field is present.
|
|
246
241
|
|
|
247
|
-
|
|
242
|
+
`from` and `nonce` are re-read from the chain on `transaction:known`. `metadata`, `source` and `correlation` are not observable on chain, so they are carried through from dispatch unchanged, on every event that carries a transaction.
|
|
248
243
|
|
|
249
|
-
|
|
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`
|
|
244
|
+
## API
|
|
253
245
|
|
|
254
|
-
###
|
|
246
|
+
### `createTrackedWalletClient<TMetadata, TSource?>(options?)`
|
|
255
247
|
|
|
256
|
-
|
|
257
|
-
- If **all** included transactions failed → `status: 'Failure'`
|
|
258
|
-
- `txIndex` points to the first successful tx, or first failure if all failed
|
|
248
|
+
Returns a builder with a single method, `.using(walletClient, publicClient)`.
|
|
259
249
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
250
|
+
| Option | Type | Description |
|
|
251
|
+
|--------|------|-------------|
|
|
252
|
+
| `source` | `TSource \| (() => TSource)` | The signing route this client is. Required when `TSource` excludes `undefined`, absent by default |
|
|
253
|
+
| `populateMetadata` | `boolean` | Auto-populate `type`, `functionName` and `args` in `writeContract` metadata |
|
|
254
|
+
| `clock` | `() => number` | Current time in milliseconds, for `broadcastTimestampMs`. Defaults to `Date.now` |
|
|
263
255
|
|
|
264
|
-
|
|
256
|
+
### Client methods
|
|
265
257
|
|
|
266
|
-
|
|
258
|
+
| Method | Returns |
|
|
259
|
+
|--------|---------|
|
|
260
|
+
| `writeContract(args)` | `Promise<Hash>` |
|
|
261
|
+
| `sendTransaction(args)` | `Promise<Hash>` |
|
|
262
|
+
| `sendRawTransaction({serializedTransaction, metadata, correlation})` | `Promise<Hash>` |
|
|
263
|
+
| `writeContractSync(args)` | `Promise<TransactionReceipt>` |
|
|
264
|
+
| `sendTransactionSync(args)` | `Promise<TransactionReceipt>` |
|
|
265
|
+
| `sendRawTransactionSync({serializedTransaction, metadata, correlation})` | `Promise<TransactionReceipt>` |
|
|
266
|
+
| `on(event, listener)` | `() => void` (unsubscribe) |
|
|
267
|
+
| `off(event, listener)` | `void` |
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
The arguments are viem's, with `nonce` widened to accept a block tag and two fields added: `metadata` and the optional `correlation`. Both are stripped from the arguments before they are handed to viem. The underlying clients stay reachable as `tracked.walletClient` and `tracked.publicClient`.
|
|
269
270
|
|
|
270
|
-
|
|
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
|
-
};
|
|
271
|
+
The account comes from the call (`account`) or from the wallet client. If neither has one, the call throws.
|
|
283
272
|
|
|
284
|
-
|
|
285
|
-
transactions: [tx1],
|
|
286
|
-
state: {
|
|
287
|
-
inclusion: 'InMemPool',
|
|
288
|
-
status: undefined,
|
|
289
|
-
final: undefined,
|
|
290
|
-
txIndex: undefined,
|
|
291
|
-
},
|
|
292
|
-
});
|
|
273
|
+
### Types
|
|
293
274
|
|
|
294
|
-
|
|
295
|
-
const tx2: BroadcastedTransaction = {
|
|
296
|
-
...tx1,
|
|
297
|
-
hash: '0x222...', // Different hash
|
|
298
|
-
};
|
|
275
|
+
`TrackedTransaction`, `KnownTrackedTransaction`, `UnknownTrackedTransaction`, `TrackedWalletClient`, `TrackedWalletClientAutoPopulate`, `TrackedWalletClientEvents`, `PopulatedMetadata`, `FunctionCallMetadata`, `UnknownTypeMetadata`, `NonceOption`, `BlockTag`, `AccessList`, `IntendedGasParameters`, `CreateTrackedWalletClientOptions`, `CorrelationField`.
|
|
299
276
|
|
|
300
|
-
|
|
301
|
-
transactions: [tx2],
|
|
302
|
-
});
|
|
277
|
+
`TrackedWalletClientType` is a convenience for declaring a client variable without spelling out the whole interface:
|
|
303
278
|
|
|
304
|
-
|
|
305
|
-
|
|
279
|
+
```typescript
|
|
280
|
+
type MyClient = TrackedWalletClientType<MyMetadata>;
|
|
281
|
+
type MyAutoClient = TrackedWalletClientType<PopulatedMetadata, true>;
|
|
282
|
+
type MySourcedClient = TrackedWalletClientType<
|
|
283
|
+
MyMetadata,
|
|
284
|
+
false,
|
|
285
|
+
Transport,
|
|
286
|
+
Chain,
|
|
287
|
+
Account,
|
|
288
|
+
MySource
|
|
289
|
+
>;
|
|
306
290
|
```
|
|
307
291
|
|
|
308
|
-
|
|
292
|
+
`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.
|
|
309
293
|
|
|
310
|
-
|
|
294
|
+
## License
|
|
311
295
|
|
|
312
|
-
|
|
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
|
-
});
|
|
319
|
-
|
|
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
|
-
```
|
|
296
|
+
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>(
|
|
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
|
|
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;
|
|
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;AAsVvD;;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"}
|