@etherkit/viem-tx-tracker 0.1.0 → 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 CHANGED
@@ -11,10 +11,11 @@ This package wraps a `WalletClient` with the same method names (`sendTransaction
11
11
  - the facts observed at dispatch: `hash`, `from`, `nonce`, `to`, `value`, `data`, `broadcastTimestampMs`, the intended gas parameters and inferred transaction type
12
12
  - `metadata`: whatever your application says the transaction means, in your own type
13
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
14
15
 
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
+ 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.
16
17
 
17
- The tracker never interprets `metadata` or `source`. It carries them.
18
+ The tracker never interprets `metadata`, `source` or `correlation`. It carries them.
18
19
 
19
20
  ## Installation
20
21
 
@@ -44,7 +45,7 @@ tracked.on('transaction:broadcasted', (tx) => {
44
45
  save(tx.hash, tx);
45
46
  });
46
47
 
47
- tracked.on('transaction:fetched', (tx) => {
48
+ tracked.on('transaction:known', (tx) => {
48
49
  // Fires once the transaction has been read back from the chain,
49
50
  // with the values the chain actually recorded.
50
51
  save(tx.hash, tx);
@@ -128,6 +129,42 @@ Points worth knowing:
128
129
  - It defaults to `undefined`, in which case `source` is simply `undefined` on every transaction.
129
130
  - The tracker never inspects it. There is no wallet, EIP-6963 or connection-library knowledge in this package: the union is yours.
130
131
 
132
+ ## Correlation: which request this send answers
133
+
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.
135
+
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.
137
+
138
+ Pass a `correlation` on the call and read it off the event:
139
+
140
+ ```typescript
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);
146
+ });
147
+
148
+ await tracked.sendTransaction({to, value, metadata: {id: 'deposit-1'}, correlation});
149
+ ```
150
+
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:
152
+
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 |
158
+
159
+ Points worth knowing:
160
+
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.
167
+
131
168
  ## Nonces
132
169
 
133
170
  Every method takes `nonce` as either an exact number or a block tag:
@@ -145,7 +182,7 @@ The resolved nonce is injected into the request and recorded on the emitted tran
145
182
  | Event | Payload | When |
146
183
  |-------|---------|------|
147
184
  | `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 |
185
+ | `transaction:known` | `KnownTrackedTransaction<TMetadata, TSource>` | Once the values are known to be final rather than intended |
149
186
 
150
187
  ```typescript
151
188
  const unsubscribe = tracked.on('transaction:broadcasted', listener);
@@ -154,9 +191,16 @@ unsubscribe();
154
191
  tracked.off('transaction:broadcasted', listener);
155
192
  ```
156
193
 
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.
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.
195
+
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.
197
+
198
+ The two paths differ in reliability, which the name cannot express, so it is worth stating plainly:
199
+
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.
158
202
 
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
+ Treat `transaction:broadcasted` as the event you must handle, and `transaction:known` as a refinement that is usually but not always delivered.
160
204
 
161
205
  ## The transaction shape
162
206
 
@@ -178,6 +222,7 @@ type CommonFields = {
178
222
  readonly broadcastTimestampMs: number;
179
223
  readonly metadata: TMetadata;
180
224
  readonly source: TSource;
225
+ readonly correlation?: string;
181
226
  };
182
227
  ```
183
228
 
@@ -192,9 +237,9 @@ if (tx.txType === 'eip1559') {
192
237
  }
193
238
  ```
194
239
 
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.
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.
196
241
 
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.
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.
198
243
 
199
244
  ## API
200
245
 
@@ -214,20 +259,20 @@ Returns a builder with a single method, `.using(walletClient, publicClient)`.
214
259
  |--------|---------|
215
260
  | `writeContract(args)` | `Promise<Hash>` |
216
261
  | `sendTransaction(args)` | `Promise<Hash>` |
217
- | `sendRawTransaction({serializedTransaction, metadata})` | `Promise<Hash>` |
262
+ | `sendRawTransaction({serializedTransaction, metadata, correlation})` | `Promise<Hash>` |
218
263
  | `writeContractSync(args)` | `Promise<TransactionReceipt>` |
219
264
  | `sendTransactionSync(args)` | `Promise<TransactionReceipt>` |
220
- | `sendRawTransactionSync({serializedTransaction, metadata})` | `Promise<TransactionReceipt>` |
265
+ | `sendRawTransactionSync({serializedTransaction, metadata, correlation})` | `Promise<TransactionReceipt>` |
221
266
  | `on(event, listener)` | `() => void` (unsubscribe) |
222
267
  | `off(event, listener)` | `void` |
223
268
 
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`.
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`.
225
270
 
226
271
  The account comes from the call (`account`) or from the wallet client. If neither has one, the call throws.
227
272
 
228
273
  ### Types
229
274
 
230
- `TrackedTransaction`, `KnownTrackedTransaction`, `UnknownTrackedTransaction`, `TrackedWalletClient`, `TrackedWalletClientAutoPopulate`, `TrackedWalletClientEvents`, `PopulatedMetadata`, `FunctionCallMetadata`, `UnknownTypeMetadata`, `NonceOption`, `BlockTag`, `AccessList`, `IntendedGasParameters`, `CreateTrackedWalletClientOptions`.
275
+ `TrackedTransaction`, `KnownTrackedTransaction`, `UnknownTrackedTransaction`, `TrackedWalletClient`, `TrackedWalletClientAutoPopulate`, `TrackedWalletClientEvents`, `PopulatedMetadata`, `FunctionCallMetadata`, `UnknownTypeMetadata`, `NonceOption`, `BlockTag`, `AccessList`, `IntendedGasParameters`, `CreateTrackedWalletClientOptions`, `CorrelationField`.
231
276
 
232
277
  `TrackedWalletClientType` is a convenience for declaring a client variable without spelling out the whole interface:
233
278
 
@@ -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;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"}
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"}
@@ -55,7 +55,7 @@ function resolveSource(source) {
55
55
  * Create an UnknownTrackedTransaction for immediate emission.
56
56
  * Populates all known intended values from the transaction parameters.
57
57
  */
58
- function createUnknownTrackedTransaction(hash, from, nonce, chainId, metadata, source, broadcastTimestampMs, params) {
58
+ function createUnknownTrackedTransaction(hash, from, nonce, chainId, carried, broadcastTimestampMs, params) {
59
59
  const base = {
60
60
  known: false,
61
61
  chainId,
@@ -63,8 +63,7 @@ function createUnknownTrackedTransaction(hash, from, nonce, chainId, metadata, s
63
63
  from,
64
64
  nonce,
65
65
  broadcastTimestampMs,
66
- metadata,
67
- source,
66
+ ...carried,
68
67
  to: params.to,
69
68
  value: params.value,
70
69
  data: params.data,
@@ -158,10 +157,10 @@ function extractTransactionTypeFields(tx) {
158
157
  * Create a KnownTrackedTransaction from a fetched transaction.
159
158
  *
160
159
  * Note: this object is rebuilt from the chain fetch, not copied from the
161
- * broadcasted one, so every carried-but-not-observed field (metadata, source)
162
- * must be threaded in explicitly.
160
+ * broadcasted one, so every carried-but-not-observed field (metadata, source,
161
+ * correlation) must be threaded in explicitly.
163
162
  */
164
- function createKnownTrackedTransaction(tx, metadata, source, broadcastTimestampMs) {
163
+ function createKnownTrackedTransaction(tx, carried, broadcastTimestampMs) {
165
164
  const base = {
166
165
  known: true,
167
166
  hash: tx.hash,
@@ -171,8 +170,7 @@ function createKnownTrackedTransaction(tx, metadata, source, broadcastTimestampM
171
170
  value: tx.value,
172
171
  data: tx.input,
173
172
  broadcastTimestampMs,
174
- metadata,
175
- source,
173
+ ...carried,
176
174
  };
177
175
  const typeFields = extractTransactionTypeFields(tx);
178
176
  return {
@@ -183,7 +181,7 @@ function createKnownTrackedTransaction(tx, metadata, source, broadcastTimestampM
183
181
  /**
184
182
  * Create a KnownTrackedTransaction from a parsed raw transaction.
185
183
  */
186
- function createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, source, chainId, broadcastTimestampMs) {
184
+ function createKnownTrackedTransactionFromRaw(parsedTx, from, hash, carried, chainId, broadcastTimestampMs) {
187
185
  const base = {
188
186
  known: true,
189
187
  hash,
@@ -193,8 +191,7 @@ function createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, so
193
191
  value: parsedTx.value ?? 0n,
194
192
  data: parsedTx.data ?? '0x',
195
193
  broadcastTimestampMs,
196
- metadata,
197
- source,
194
+ ...carried,
198
195
  };
199
196
  // Determine transaction type from parsed tx
200
197
  if ('maxFeePerGas' in parsedTx && parsedTx.maxFeePerGas !== undefined) {
@@ -334,32 +331,40 @@ export function createTrackedWalletClient(options) {
334
331
  return { from, intendedNonce };
335
332
  }
336
333
  /**
337
- * Fetch full transaction data and emit transaction:fetched event.
334
+ * Fetch full transaction data and emit transaction:known event.
338
335
  * Non-blocking, runs in background. Does not throw.
339
336
  */
340
- async function fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs) {
337
+ async function fetchAndEmitFullData(hash, carried, broadcastTimestampMs) {
341
338
  try {
342
339
  const tx = await publicClient.getTransaction({ hash });
343
- const knownTx = createKnownTrackedTransaction(tx, metadata, source, broadcastTimestampMs);
344
- emitter.emit('transaction:fetched', knownTx);
340
+ const knownTx = createKnownTrackedTransaction(tx, carried, broadcastTimestampMs);
341
+ emitter.emit('transaction:known', knownTx);
345
342
  }
346
343
  catch (error) {
347
- // Log but don't throw - transaction:fetched simply won't fire
344
+ // Log but don't throw - transaction:known simply won't fire
348
345
  console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
349
- `transaction:fetched event will not be emitted. Error: ${error}`);
346
+ `transaction:known event will not be emitted. Error: ${error}`);
350
347
  }
351
348
  }
352
349
  /**
353
350
  * Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
354
351
  * Emits transaction:broadcasted immediately with intended values,
355
- * then fetches and emits transaction:fetched with actual values.
352
+ * then fetches and emits transaction:known with actual values.
356
353
  */
357
354
  async function executeTrackedTransaction(args) {
358
- const { metadata, restArgs, intendedParams, execute, extractHash } = args;
355
+ const { metadata, correlation, restArgs, intendedParams, execute, extractHash, } = args;
359
356
  const broadcastTimestampMs = clock();
360
357
  // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
361
358
  // A thunk is re-evaluated on every send, never captured once.
362
359
  const source = resolveSource(sourceOption);
360
+ // The carried values travel as one bundle from here on: metadata (what
361
+ // the tx means), source (which route signed) and correlation (which
362
+ // caller-side request this send answers). None is interpreted.
363
+ const carried = {
364
+ metadata,
365
+ source,
366
+ correlation,
367
+ };
363
368
  // Extract common context
364
369
  const { from, intendedNonce } = await extractTransactionContext(args);
365
370
  // Execute the underlying transaction with nonce injected
@@ -369,10 +374,10 @@ export function createTrackedWalletClient(options) {
369
374
  });
370
375
  const hash = extractHash(result);
371
376
  // Emit transaction:broadcasted immediately with intended values
372
- const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, source, broadcastTimestampMs, intendedParams);
377
+ const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, carried, broadcastTimestampMs, intendedParams);
373
378
  emitter.emit('transaction:broadcasted', unknownTx);
374
- // Fire-and-forget: fetch full data and emit transaction:fetched
375
- fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs);
379
+ // Fire-and-forget: fetch full data and emit transaction:known
380
+ fetchAndEmitFullData(hash, carried, broadcastTimestampMs);
376
381
  return result;
377
382
  }
378
383
  /**
@@ -381,22 +386,30 @@ export function createTrackedWalletClient(options) {
381
386
  * Emits KnownTrackedTransaction directly to transaction:broadcasted.
382
387
  */
383
388
  async function executeTrackedRawTransaction(args) {
384
- const { serializedTransaction, metadata, execute, extractHash } = args;
389
+ const { serializedTransaction, metadata, correlation, execute, extractHash, } = args;
385
390
  const broadcastTimestampMs = clock();
386
391
  // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
387
392
  // A thunk is re-evaluated on every send, never captured once.
388
393
  const source = resolveSource(sourceOption);
394
+ const carried = {
395
+ metadata,
396
+ source,
397
+ correlation,
398
+ };
389
399
  const from = await recoverTransactionAddress({ serializedTransaction });
390
400
  const parsedTx = parseTransaction(serializedTransaction);
391
401
  // Execute the broadcast
392
402
  const result = await execute();
393
403
  const hash = extractHash(result);
394
404
  // For raw transactions, we can parse full data immediately
395
- const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, source, walletClient.chain?.id, broadcastTimestampMs);
405
+ const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, carried, walletClient.chain?.id, broadcastTimestampMs);
396
406
  // Emit as KnownTrackedTransaction since we have all data
397
407
  emitter.emit('transaction:broadcasted', knownTx);
398
- // Also emit to transaction:fetched for consistency
399
- emitter.emit('transaction:fetched', knownTx);
408
+ // The values are final (parsed from the signed payload, not merely
409
+ // intended), which is exactly what transaction:known promises, so it
410
+ // is emitted here too: every tracked transaction reaches that event,
411
+ // and a consumer can persist on it alone.
412
+ emitter.emit('transaction:known', knownTx);
400
413
  return result;
401
414
  }
402
415
  return {
@@ -406,12 +419,13 @@ export function createTrackedWalletClient(options) {
406
419
  // Async methods (return hash)
407
420
  // ============================================
408
421
  async writeContract(args) {
409
- const { metadata, nonce, ...writeArgs } = args;
422
+ const { metadata, correlation, nonce, ...writeArgs } = args;
410
423
  const intendedParams = extractIntendedParamsFromWriteContract(args);
411
424
  return executeTrackedTransaction({
412
425
  account: normalizeAccount(args.account),
413
426
  nonce,
414
427
  metadata: metadata,
428
+ correlation,
415
429
  restArgs: writeArgs,
416
430
  intendedParams,
417
431
  execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
@@ -419,12 +433,13 @@ export function createTrackedWalletClient(options) {
419
433
  });
420
434
  },
421
435
  async sendTransaction(args) {
422
- const { metadata, nonce, ...sendArgs } = args;
436
+ const { metadata, correlation, nonce, ...sendArgs } = args;
423
437
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
424
438
  return executeTrackedTransaction({
425
439
  account: normalizeAccount(args.account),
426
440
  nonce,
427
441
  metadata: metadata,
442
+ correlation,
428
443
  restArgs: sendArgs,
429
444
  intendedParams,
430
445
  execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
@@ -432,10 +447,11 @@ export function createTrackedWalletClient(options) {
432
447
  });
433
448
  },
434
449
  async sendRawTransaction(args) {
435
- const { metadata, serializedTransaction } = args;
450
+ const { metadata, correlation, serializedTransaction } = args;
436
451
  return executeTrackedRawTransaction({
437
452
  serializedTransaction,
438
453
  metadata: metadata,
454
+ correlation,
439
455
  execute: () => walletClient.sendRawTransaction({ serializedTransaction }),
440
456
  extractHash: (hash) => hash,
441
457
  });
@@ -444,12 +460,13 @@ export function createTrackedWalletClient(options) {
444
460
  // Sync methods (return receipt, wait for confirmation)
445
461
  // ============================================
446
462
  async writeContractSync(args) {
447
- const { metadata, nonce, ...writeArgs } = args;
463
+ const { metadata, correlation, nonce, ...writeArgs } = args;
448
464
  const intendedParams = extractIntendedParamsFromWriteContract(args);
449
465
  return executeTrackedTransaction({
450
466
  account: normalizeAccount(args.account),
451
467
  nonce,
452
468
  metadata: metadata,
469
+ correlation,
453
470
  restArgs: writeArgs,
454
471
  intendedParams,
455
472
  execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
@@ -457,12 +474,13 @@ export function createTrackedWalletClient(options) {
457
474
  });
458
475
  },
459
476
  async sendTransactionSync(args) {
460
- const { metadata, nonce, ...sendArgs } = args;
477
+ const { metadata, correlation, nonce, ...sendArgs } = args;
461
478
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
462
479
  return executeTrackedTransaction({
463
480
  account: normalizeAccount(args.account),
464
481
  nonce,
465
482
  metadata: metadata,
483
+ correlation,
466
484
  restArgs: sendArgs,
467
485
  intendedParams,
468
486
  execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
@@ -470,10 +488,11 @@ export function createTrackedWalletClient(options) {
470
488
  });
471
489
  },
472
490
  async sendRawTransactionSync(args) {
473
- const { metadata, serializedTransaction } = args;
491
+ const { metadata, correlation, serializedTransaction } = args;
474
492
  return executeTrackedRawTransaction({
475
493
  serializedTransaction,
476
494
  metadata: metadata,
495
+ correlation,
477
496
  execute: () => walletClient.sendRawTransactionSync({ serializedTransaction }),
478
497
  extractHash: (receipt) => receipt.transactionHash,
479
498
  });
@@ -523,19 +542,19 @@ function createAutoPopulateBuilder(clock, sourceOption) {
523
542
  return { from, intendedNonce };
524
543
  }
525
544
  /**
526
- * Fetch full transaction data and emit transaction:fetched event.
545
+ * Fetch full transaction data and emit transaction:known event.
527
546
  * Non-blocking, runs in background. Does not throw.
528
547
  */
529
- async function fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs) {
548
+ async function fetchAndEmitFullData(hash, carried, broadcastTimestampMs) {
530
549
  try {
531
550
  const tx = await publicClient.getTransaction({ hash });
532
- const knownTx = createKnownTrackedTransaction(tx, metadata, source, broadcastTimestampMs);
533
- emitter.emit('transaction:fetched', knownTx);
551
+ const knownTx = createKnownTrackedTransaction(tx, carried, broadcastTimestampMs);
552
+ emitter.emit('transaction:known', knownTx);
534
553
  }
535
554
  catch (error) {
536
- // Log but don't throw - transaction:fetched simply won't fire
555
+ // Log but don't throw - transaction:known simply won't fire
537
556
  console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
538
- `transaction:fetched event will not be emitted. Error: ${error}`);
557
+ `transaction:known event will not be emitted. Error: ${error}`);
539
558
  }
540
559
  }
541
560
  /**
@@ -561,14 +580,22 @@ function createAutoPopulateBuilder(clock, sourceOption) {
561
580
  /**
562
581
  * Common wrapper for transaction methods that broadcast.
563
582
  * Emits transaction:broadcasted immediately with intended values,
564
- * then fetches and emits transaction:fetched with actual values.
583
+ * then fetches and emits transaction:known with actual values.
565
584
  */
566
585
  async function executeTrackedTransaction(args) {
567
- const { metadata, restArgs, intendedParams, execute, extractHash } = args;
586
+ const { metadata, correlation, restArgs, intendedParams, execute, extractHash, } = args;
568
587
  const broadcastTimestampMs = clock();
569
588
  // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
570
589
  // A thunk is re-evaluated on every send, never captured once.
571
590
  const source = resolveSource(sourceOption);
591
+ // The carried values travel as one bundle from here on: metadata (what
592
+ // the tx means), source (which route signed) and correlation (which
593
+ // caller-side request this send answers). None is interpreted.
594
+ const carried = {
595
+ metadata,
596
+ source,
597
+ correlation,
598
+ };
572
599
  const { from, intendedNonce } = await extractTransactionContext(args);
573
600
  const result = await execute({
574
601
  ...restArgs,
@@ -576,10 +603,10 @@ function createAutoPopulateBuilder(clock, sourceOption) {
576
603
  });
577
604
  const hash = extractHash(result);
578
605
  // Emit transaction:broadcasted immediately with intended values
579
- const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, source, broadcastTimestampMs, intendedParams);
606
+ const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, carried, broadcastTimestampMs, intendedParams);
580
607
  emitter.emit('transaction:broadcasted', unknownTx);
581
- // Fire-and-forget: fetch full data and emit transaction:fetched
582
- fetchAndEmitFullData(hash, metadata, source, broadcastTimestampMs);
608
+ // Fire-and-forget: fetch full data and emit transaction:known
609
+ fetchAndEmitFullData(hash, carried, broadcastTimestampMs);
583
610
  return result;
584
611
  }
585
612
  /**
@@ -588,21 +615,30 @@ function createAutoPopulateBuilder(clock, sourceOption) {
588
615
  * Emits KnownTrackedTransaction directly to transaction:broadcasted.
589
616
  */
590
617
  async function executeTrackedRawTransaction(args) {
591
- const { serializedTransaction, metadata, execute, extractHash } = args;
618
+ const { serializedTransaction, metadata, correlation, execute, extractHash, } = args;
592
619
  const broadcastTimestampMs = clock();
593
620
  // Stamped here, in the same step as from/nonce/broadcastTimestampMs.
594
621
  // A thunk is re-evaluated on every send, never captured once.
595
622
  const source = resolveSource(sourceOption);
623
+ const carried = {
624
+ metadata,
625
+ source,
626
+ correlation,
627
+ };
596
628
  const from = await recoverTransactionAddress({ serializedTransaction });
597
629
  const parsedTx = parseTransaction(serializedTransaction);
598
630
  // Execute the broadcast
599
631
  const result = await execute();
600
632
  const hash = extractHash(result);
601
633
  // For raw transactions, we can parse full data immediately
602
- const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, source, walletClient.chain?.id, broadcastTimestampMs);
634
+ const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, carried, walletClient.chain?.id, broadcastTimestampMs);
603
635
  // Emit as KnownTrackedTransaction since we have all data
604
636
  emitter.emit('transaction:broadcasted', knownTx);
605
- // We do not emit fetched as the tx is already known
637
+ // The values are final (parsed from the signed payload, not merely
638
+ // intended), which is exactly what transaction:known promises, so it
639
+ // is emitted here too: every tracked transaction reaches that event,
640
+ // and a consumer can persist on it alone.
641
+ emitter.emit('transaction:known', knownTx);
606
642
  return result;
607
643
  }
608
644
  return {
@@ -612,7 +648,7 @@ function createAutoPopulateBuilder(clock, sourceOption) {
612
648
  // Async methods (return hash)
613
649
  // ============================================
614
650
  async writeContract(args) {
615
- const { metadata: userMetadata, nonce, ...writeArgs } = args;
651
+ const { metadata: userMetadata, correlation, nonce, ...writeArgs } = args;
616
652
  // Validate that user didn't provide operation, functionName or args
617
653
  validateNoAutoPopulatedFieldsInMetadata(userMetadata);
618
654
  // Auto-populate type, functionName and args
@@ -627,6 +663,7 @@ function createAutoPopulateBuilder(clock, sourceOption) {
627
663
  account: normalizeAccount(args.account),
628
664
  nonce,
629
665
  metadata: finalMetadata,
666
+ correlation,
630
667
  restArgs: writeArgs,
631
668
  intendedParams,
632
669
  execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
@@ -634,12 +671,13 @@ function createAutoPopulateBuilder(clock, sourceOption) {
634
671
  });
635
672
  },
636
673
  async sendTransaction(args) {
637
- const { metadata, nonce, ...sendArgs } = args;
674
+ const { metadata, correlation, nonce, ...sendArgs } = args;
638
675
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
639
676
  return executeTrackedTransaction({
640
677
  account: normalizeAccount(args.account),
641
678
  nonce,
642
679
  metadata: metadata,
680
+ correlation,
643
681
  restArgs: sendArgs,
644
682
  intendedParams,
645
683
  execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
@@ -647,10 +685,11 @@ function createAutoPopulateBuilder(clock, sourceOption) {
647
685
  });
648
686
  },
649
687
  async sendRawTransaction(args) {
650
- const { metadata, serializedTransaction } = args;
688
+ const { metadata, correlation, serializedTransaction } = args;
651
689
  return executeTrackedRawTransaction({
652
690
  serializedTransaction,
653
691
  metadata: metadata,
692
+ correlation,
654
693
  execute: () => walletClient.sendRawTransaction({ serializedTransaction }),
655
694
  extractHash: (hash) => hash,
656
695
  });
@@ -659,7 +698,7 @@ function createAutoPopulateBuilder(clock, sourceOption) {
659
698
  // Sync methods (return receipt, wait for confirmation)
660
699
  // ============================================
661
700
  async writeContractSync(args) {
662
- const { metadata: userMetadata, nonce, ...writeArgs } = args;
701
+ const { metadata: userMetadata, correlation, nonce, ...writeArgs } = args;
663
702
  // Validate that user didn't provide operation, functionName or args
664
703
  validateNoAutoPopulatedFieldsInMetadata(userMetadata);
665
704
  // Auto-populate type, functionName and args
@@ -674,6 +713,7 @@ function createAutoPopulateBuilder(clock, sourceOption) {
674
713
  account: normalizeAccount(args.account),
675
714
  nonce,
676
715
  metadata: finalMetadata,
716
+ correlation,
677
717
  restArgs: writeArgs,
678
718
  intendedParams,
679
719
  execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
@@ -681,12 +721,13 @@ function createAutoPopulateBuilder(clock, sourceOption) {
681
721
  });
682
722
  },
683
723
  async sendTransactionSync(args) {
684
- const { metadata, nonce, ...sendArgs } = args;
724
+ const { metadata, correlation, nonce, ...sendArgs } = args;
685
725
  const intendedParams = extractIntendedParamsFromSendTransaction(args);
686
726
  return executeTrackedTransaction({
687
727
  account: normalizeAccount(args.account),
688
728
  nonce,
689
729
  metadata: metadata,
730
+ correlation,
690
731
  restArgs: sendArgs,
691
732
  intendedParams,
692
733
  execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
@@ -694,10 +735,11 @@ function createAutoPopulateBuilder(clock, sourceOption) {
694
735
  });
695
736
  },
696
737
  async sendRawTransactionSync(args) {
697
- const { metadata, serializedTransaction } = args;
738
+ const { metadata, correlation, serializedTransaction } = args;
698
739
  return executeTrackedRawTransaction({
699
740
  serializedTransaction,
700
741
  metadata: metadata,
742
+ correlation,
701
743
  execute: () => walletClient.sendRawTransactionSync({ serializedTransaction }),
702
744
  extractHash: (receipt) => receipt.transactionHash,
703
745
  });