@etherkit/viem-tx-tracker 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"TrackedWalletClient.d.ts","sourceRoot":"","sources":["../src/TrackedWalletClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,OAAO,EAEZ,KAAK,KAAK,EAIV,KAAK,YAAY,EAGjB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAIX,iBAAiB,EAIjB,mBAAmB,EACnB,+BAA+B,EAG/B,MAAM,YAAY,CAAC;AA+CpB;;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,CAAC,OAAO,EAAE;IAClD,gBAAgB,EAAE,IAAI,CAAC;CACvB,GAAG,sCAAsC,CAAC,iBAAiB,CAAC,CAAC;AAG9D,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,EAAE;IAC7D,gBAAgB,EAAE,IAAI,CAAC;CACvB,GAAG,sCAAsC,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"TrackedWalletClient.d.ts","sourceRoot":"","sources":["../src/TrackedWalletClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,OAAO,EAEZ,KAAK,KAAK,EAIV,KAAK,YAAY,EAIjB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAMX,iBAAiB,EAIjB,mBAAmB,EACnB,+BAA+B,EAI/B,MAAM,YAAY,CAAC;AA+RpB;;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,CAAC,OAAO,EAAE;IAClD,gBAAgB,EAAE,IAAI,CAAC;CACvB,GAAG,sCAAsC,CAAC,iBAAiB,CAAC,CAAC;AAG9D,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,EAAE;IAC7D,gBAAgB,EAAE,IAAI,CAAC;CACvB,GAAG,sCAAsC,CAAC,SAAS,CAAC,CAAC"}
@@ -24,20 +24,189 @@ function normalizeAccount(account) {
24
24
  return account === null ? undefined : account;
25
25
  }
26
26
  /**
27
- * Generate a unique tracking ID if not provided in metadata
27
+ * Infer transaction type from provided params.
28
+ * Returns undefined if can't be determined (wallet will decide).
28
29
  */
29
- function generateTrackingId() {
30
- return crypto.randomUUID();
30
+ function inferTxType(params) {
31
+ if (params.maxFeePerGas !== undefined) {
32
+ return 'eip1559';
33
+ }
34
+ if (params.gasPrice !== undefined && params.accessList !== undefined) {
35
+ return 'eip2930';
36
+ }
37
+ if (params.gasPrice !== undefined) {
38
+ return 'legacy';
39
+ }
40
+ return undefined; // Wallet will determine
41
+ }
42
+ /**
43
+ * Create an UnknownTrackedTransaction for immediate emission.
44
+ * Populates all known intended values from the transaction parameters.
45
+ */
46
+ function createUnknownTrackedTransaction(hash, from, nonce, chainId, metadata, broadcastTimestampMs, params) {
47
+ const txType = inferTxType(params);
48
+ return {
49
+ known: false,
50
+ chainId,
51
+ hash,
52
+ from,
53
+ nonce,
54
+ broadcastTimestampMs,
55
+ metadata,
56
+ ...(txType !== undefined && { txType }),
57
+ ...(params.to !== undefined && { to: params.to }),
58
+ ...(params.value !== undefined && { value: params.value }),
59
+ ...(params.data !== undefined && { data: params.data }),
60
+ ...(params.gas !== undefined && { gas: params.gas }),
61
+ ...(params.gasPrice !== undefined && { gasPrice: params.gasPrice }),
62
+ ...(params.maxFeePerGas !== undefined && {
63
+ maxFeePerGas: params.maxFeePerGas,
64
+ }),
65
+ ...(params.maxPriorityFeePerGas !== undefined && {
66
+ maxPriorityFeePerGas: params.maxPriorityFeePerGas,
67
+ }),
68
+ ...(params.accessList !== undefined && { accessList: params.accessList }),
69
+ };
70
+ }
71
+ /**
72
+ * Extract transaction type-specific fields from a fetched transaction.
73
+ */
74
+ function extractTransactionTypeFields(tx) {
75
+ if (tx.type === 'eip1559') {
76
+ return {
77
+ txType: 'eip1559',
78
+ maxFeePerGas: tx.maxFeePerGas,
79
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
80
+ ...(tx.accessList && { accessList: tx.accessList }),
81
+ };
82
+ }
83
+ else if (tx.type === 'eip2930') {
84
+ return {
85
+ txType: 'eip2930',
86
+ gasPrice: tx.gasPrice,
87
+ accessList: (tx.accessList ?? []),
88
+ };
89
+ }
90
+ else {
91
+ // Legacy or unknown - treat as legacy
92
+ return {
93
+ txType: 'legacy',
94
+ gasPrice: tx.gasPrice,
95
+ };
96
+ }
97
+ }
98
+ /**
99
+ * Create a KnownTrackedTransaction from a fetched transaction.
100
+ */
101
+ function createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs) {
102
+ const typeFields = extractTransactionTypeFields(tx);
103
+ return {
104
+ known: true,
105
+ chainId: tx.chainId,
106
+ hash: tx.hash,
107
+ from: tx.from,
108
+ to: tx.to,
109
+ nonce: tx.nonce,
110
+ value: tx.value,
111
+ data: tx.input,
112
+ gas: tx.gas,
113
+ broadcastTimestampMs,
114
+ metadata,
115
+ ...typeFields,
116
+ };
117
+ }
118
+ /**
119
+ * Create a KnownTrackedTransaction from a parsed raw transaction.
120
+ */
121
+ function createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, chainId, broadcastTimestampMs) {
122
+ // Determine transaction type from parsed tx
123
+ let typeFields;
124
+ if ('maxFeePerGas' in parsedTx && parsedTx.maxFeePerGas !== undefined) {
125
+ typeFields = {
126
+ txType: 'eip1559',
127
+ maxFeePerGas: parsedTx.maxFeePerGas,
128
+ maxPriorityFeePerGas: parsedTx.maxPriorityFeePerGas,
129
+ ...('accessList' in parsedTx &&
130
+ parsedTx.accessList && {
131
+ accessList: parsedTx.accessList,
132
+ }),
133
+ };
134
+ }
135
+ else if ('accessList' in parsedTx && parsedTx.accessList) {
136
+ typeFields = {
137
+ txType: 'eip2930',
138
+ gasPrice: parsedTx.gasPrice,
139
+ accessList: parsedTx.accessList,
140
+ };
141
+ }
142
+ else {
143
+ typeFields = {
144
+ txType: 'legacy',
145
+ gasPrice: parsedTx.gasPrice,
146
+ };
147
+ }
148
+ return {
149
+ known: true,
150
+ chainId: parsedTx.chainId ?? chainId,
151
+ hash,
152
+ from,
153
+ to: parsedTx.to ?? null,
154
+ nonce: parsedTx.nonce,
155
+ value: parsedTx.value ?? 0n,
156
+ data: parsedTx.data ?? '0x',
157
+ gas: parsedTx.gas,
158
+ broadcastTimestampMs,
159
+ metadata,
160
+ ...typeFields,
161
+ };
162
+ }
163
+ /**
164
+ * Extract intended params from sendTransaction args.
165
+ * Uses 'unknown' for 'to' since viem's SendTransactionParameters has a complex type for it.
166
+ */
167
+ function extractIntendedParamsFromSendTransaction(args) {
168
+ // Normalize 'to' to either a hex address, null, or undefined
169
+ const to = args.to === null || args.to === undefined
170
+ ? args.to
171
+ : typeof args.to === 'string'
172
+ ? args.to
173
+ : undefined;
174
+ return {
175
+ to,
176
+ value: args.value ?? 0n,
177
+ data: args.data,
178
+ gas: args.gas,
179
+ gasPrice: args.gasPrice,
180
+ maxFeePerGas: args.maxFeePerGas,
181
+ maxPriorityFeePerGas: args.maxPriorityFeePerGas,
182
+ accessList: args.accessList,
183
+ };
184
+ }
185
+ /**
186
+ * Extract intended params from writeContract args.
187
+ */
188
+ function extractIntendedParamsFromWriteContract(args) {
189
+ return {
190
+ to: args.address,
191
+ value: args.value ?? 0n,
192
+ // Note: data could be encoded using encodeFunctionData(args) if desired
193
+ gas: args.gas,
194
+ gasPrice: args.gasPrice,
195
+ maxFeePerGas: args.maxFeePerGas,
196
+ maxPriorityFeePerGas: args.maxPriorityFeePerGas,
197
+ accessList: args.accessList,
198
+ };
31
199
  }
32
200
  // Implementation
33
201
  export function createTrackedWalletClient(options) {
34
202
  const populateMetadata = options?.populateMetadata ?? false;
203
+ const clock = options?.clock ?? Date.now;
35
204
  if (populateMetadata) {
36
- return createAutoPopulateBuilder();
205
+ return createAutoPopulateBuilder(clock);
37
206
  }
38
207
  return {
39
208
  using(walletClient, publicClient) {
40
- // Create emitter for transaction broadcast events
209
+ // Create emitter for transaction events
41
210
  const emitter = new Emitter();
42
211
  /**
43
212
  * Resolve the nonce to use for a transaction.
@@ -78,71 +247,29 @@ export function createTrackedWalletClient(options) {
78
247
  return { from, intendedNonce };
79
248
  }
80
249
  /**
81
- * Extract transaction context from a serialized (signed) transaction.
82
- * Parses the transaction and recovers the sender address.
83
- *
84
- * @param serializedTransaction - The RLP-encoded signed transaction
85
- * @returns TransactionContext with from address and nonce
86
- */
87
- async function extractRawTransactionContext(serializedTransaction) {
88
- // Parse the serialized transaction to get the nonce
89
- const parsedTx = parseTransaction(serializedTransaction);
90
- if (parsedTx.nonce === undefined) {
91
- throw new Error('[TrackedWalletClient] Could not extract nonce from serialized transaction.');
92
- }
93
- // Recover the sender address from the signature
94
- const from = await recoverTransactionAddress({
95
- serializedTransaction,
96
- });
97
- return {
98
- from,
99
- intendedNonce: parsedTx.nonce,
100
- };
101
- }
102
- /**
103
- * Fetch the transaction after broadcast to verify nonce.
104
- * Logs a warning if the nonce was overridden or if tx cannot be found.
105
- *
106
- * @param hash - The transaction hash
107
- * @param intendedNonce - The nonce we intended to use
108
- * @returns The actual nonce, or the intended nonce if fetch failed
250
+ * Fetch full transaction data and emit transaction:fetched event.
251
+ * Non-blocking, runs in background. Does not throw.
109
252
  */
110
- async function verifyTransactionNonce(hash, intendedNonce) {
253
+ async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
111
254
  try {
112
255
  const tx = await publicClient.getTransaction({ hash });
113
- const actualNonce = tx.nonce;
114
- if (actualNonce !== intendedNonce) {
115
- console.warn(`[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
116
- `Wallet may have overridden the nonce.`);
117
- }
118
- return actualNonce;
256
+ const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
257
+ emitter.emit('transaction:fetched', knownTx);
119
258
  }
120
- catch (fetchError) {
121
- // Transaction not found in mempool/chain yet
122
- console.warn(`[TrackedWalletClient] Could not fetch tx ${hash} after broadcast. ` +
123
- `It may not be in the mempool yet.`);
124
- return intendedNonce;
259
+ catch (error) {
260
+ // Log but don't throw - transaction:fetched simply won't fire
261
+ console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
262
+ `transaction:fetched event will not be emitted. Error: ${error}`);
125
263
  }
126
264
  }
127
- /**
128
- * Create a tracked transaction record.
129
- */
130
- function createTrackedTransactionRecord(txHash, from, nonce, metadata) {
131
- return {
132
- hash: txHash,
133
- from,
134
- nonce,
135
- chainId: walletClient.chain?.id,
136
- metadata,
137
- broadcastTimestampMs: Date.now(),
138
- };
139
- }
140
265
  /**
141
266
  * Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
142
- * Handles nonce resolution, underlying call, post-broadcast verification, and tracking record creation.
267
+ * Emits transaction:broadcasted immediately with intended values,
268
+ * then fetches and emits transaction:fetched with actual values.
143
269
  */
144
270
  async function executeTrackedTransaction(args) {
145
- const { metadata, restArgs, execute, extractHash } = args;
271
+ const { metadata, restArgs, intendedParams, execute, extractHash } = args;
272
+ const broadcastTimestampMs = clock();
146
273
  // Extract common context
147
274
  const { from, intendedNonce } = await extractTransactionContext(args);
148
275
  // Execute the underlying transaction with nonce injected
@@ -151,31 +278,32 @@ export function createTrackedWalletClient(options) {
151
278
  nonce: intendedNonce,
152
279
  });
153
280
  const hash = extractHash(result);
154
- // Verify transaction and get actual nonce
155
- const actualNonce = await verifyTransactionNonce(hash, intendedNonce);
156
- // Create tracked transaction record
157
- const trackedTx = createTrackedTransactionRecord(hash, from, actualNonce, metadata);
158
- // Emit transaction broadcasted event
159
- emitter.emit('transaction:broadcasted', trackedTx);
281
+ // Emit transaction:broadcasted immediately with intended values
282
+ const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, broadcastTimestampMs, intendedParams);
283
+ emitter.emit('transaction:broadcasted', unknownTx);
284
+ // Fire-and-forget: fetch full data and emit transaction:fetched
285
+ fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
160
286
  return result;
161
287
  }
162
288
  /**
163
289
  * Common wrapper for raw transaction broadcasts (sendRawTransaction).
164
- * Decodes the transaction to extract from/nonce, broadcasts, and creates tracking record.
290
+ * For raw transactions, we can parse full data immediately.
291
+ * Emits KnownTrackedTransaction directly to transaction:broadcasted.
165
292
  */
166
293
  async function executeTrackedRawTransaction(args) {
167
294
  const { serializedTransaction, metadata, execute, extractHash } = args;
168
- // Extract context from the serialized transaction
169
- const { from, intendedNonce } = await extractRawTransactionContext(serializedTransaction);
295
+ const broadcastTimestampMs = clock();
296
+ const from = await recoverTransactionAddress({ serializedTransaction });
297
+ const parsedTx = parseTransaction(serializedTransaction);
170
298
  // Execute the broadcast
171
299
  const result = await execute();
172
300
  const hash = extractHash(result);
173
- // For raw transactions, the nonce is already embedded, so no verification needed
174
- // (wallet cannot override nonce in an already-signed transaction)
175
- // Create tracked transaction record
176
- const trackedTx = createTrackedTransactionRecord(hash, from, intendedNonce, metadata);
177
- // Emit transaction broadcasted event
178
- emitter.emit('transaction:broadcasted', trackedTx);
301
+ // For raw transactions, we can parse full data immediately
302
+ const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, walletClient.chain?.id, broadcastTimestampMs);
303
+ // Emit as KnownTrackedTransaction since we have all data
304
+ emitter.emit('transaction:broadcasted', knownTx);
305
+ // Also emit to transaction:fetched for consistency
306
+ emitter.emit('transaction:fetched', knownTx);
179
307
  return result;
180
308
  }
181
309
  return {
@@ -186,22 +314,26 @@ export function createTrackedWalletClient(options) {
186
314
  // ============================================
187
315
  async writeContract(args) {
188
316
  const { metadata, nonce, ...writeArgs } = args;
317
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
189
318
  return executeTrackedTransaction({
190
319
  account: normalizeAccount(args.account),
191
320
  nonce,
192
321
  metadata: metadata,
193
322
  restArgs: writeArgs,
323
+ intendedParams,
194
324
  execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
195
325
  extractHash: (hash) => hash,
196
326
  });
197
327
  },
198
328
  async sendTransaction(args) {
199
329
  const { metadata, nonce, ...sendArgs } = args;
330
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
200
331
  return executeTrackedTransaction({
201
332
  account: normalizeAccount(args.account),
202
333
  nonce,
203
334
  metadata: metadata,
204
335
  restArgs: sendArgs,
336
+ intendedParams,
205
337
  execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
206
338
  extractHash: (hash) => hash,
207
339
  });
@@ -220,22 +352,26 @@ export function createTrackedWalletClient(options) {
220
352
  // ============================================
221
353
  async writeContractSync(args) {
222
354
  const { metadata, nonce, ...writeArgs } = args;
355
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
223
356
  return executeTrackedTransaction({
224
357
  account: normalizeAccount(args.account),
225
358
  nonce,
226
359
  metadata: metadata,
227
360
  restArgs: writeArgs,
361
+ intendedParams,
228
362
  execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
229
363
  extractHash: (receipt) => receipt.transactionHash,
230
364
  });
231
365
  },
232
366
  async sendTransactionSync(args) {
233
367
  const { metadata, nonce, ...sendArgs } = args;
368
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
234
369
  return executeTrackedTransaction({
235
370
  account: normalizeAccount(args.account),
236
371
  nonce,
237
372
  metadata: metadata,
238
373
  restArgs: sendArgs,
374
+ intendedParams,
239
375
  execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
240
376
  extractHash: (receipt) => receipt.transactionHash,
241
377
  });
@@ -252,8 +388,8 @@ export function createTrackedWalletClient(options) {
252
388
  // ============================================
253
389
  // Event subscription methods
254
390
  // ============================================
255
- onTransactionBroadcasted: (listener) => emitter.on('transaction:broadcasted', listener),
256
- offTransactionBroadcasted: (listener) => emitter.off('transaction:broadcasted', listener),
391
+ on: emitter.on.bind(emitter),
392
+ off: emitter.off.bind(emitter),
257
393
  };
258
394
  },
259
395
  };
@@ -262,10 +398,10 @@ export function createTrackedWalletClient(options) {
262
398
  * Create an auto-populate builder for TrackedWalletClient.
263
399
  * This builder auto-populates operation, functionName and args in writeContract metadata.
264
400
  */
265
- function createAutoPopulateBuilder() {
401
+ function createAutoPopulateBuilder(clock) {
266
402
  return {
267
403
  using(walletClient, publicClient) {
268
- // Create emitter for transaction broadcast events
404
+ // Create emitter for transaction events
269
405
  const emitter = new Emitter();
270
406
  /**
271
407
  * Resolve the nonce to use for a transaction.
@@ -294,53 +430,21 @@ function createAutoPopulateBuilder() {
294
430
  return { from, intendedNonce };
295
431
  }
296
432
  /**
297
- * Extract transaction context from a serialized (signed) transaction.
433
+ * Fetch full transaction data and emit transaction:fetched event.
434
+ * Non-blocking, runs in background. Does not throw.
298
435
  */
299
- async function extractRawTransactionContext(serializedTransaction) {
300
- const parsedTx = parseTransaction(serializedTransaction);
301
- if (parsedTx.nonce === undefined) {
302
- throw new Error('[TrackedWalletClient] Could not extract nonce from serialized transaction.');
303
- }
304
- const from = await recoverTransactionAddress({
305
- serializedTransaction,
306
- });
307
- return {
308
- from,
309
- intendedNonce: parsedTx.nonce,
310
- };
311
- }
312
- /**
313
- * Fetch the transaction after broadcast to verify nonce.
314
- */
315
- async function verifyTransactionNonce(hash, intendedNonce) {
436
+ async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
316
437
  try {
317
438
  const tx = await publicClient.getTransaction({ hash });
318
- const actualNonce = tx.nonce;
319
- if (actualNonce !== intendedNonce) {
320
- console.warn(`[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
321
- `Wallet may have overridden the nonce.`);
322
- }
323
- return actualNonce;
439
+ const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
440
+ emitter.emit('transaction:fetched', knownTx);
324
441
  }
325
- catch (fetchError) {
326
- console.warn(`[TrackedWalletClient] Could not fetch tx ${hash} after broadcast. ` +
327
- `It may not be in the mempool yet.`);
328
- return intendedNonce;
442
+ catch (error) {
443
+ // Log but don't throw - transaction:fetched simply won't fire
444
+ console.warn(`[TrackedWalletClient] Could not fetch tx ${hash}. ` +
445
+ `transaction:fetched event will not be emitted. Error: ${error}`);
329
446
  }
330
447
  }
331
- /**
332
- * Create a tracked transaction record.
333
- */
334
- function createTrackedTransactionRecord(txHash, from, nonce, metadata) {
335
- return {
336
- hash: txHash,
337
- from,
338
- nonce,
339
- chainId: walletClient.chain?.id,
340
- metadata,
341
- broadcastTimestampMs: Date.now(),
342
- };
343
- }
344
448
  /**
345
449
  * Validate that user didn't provide operation, functionName or args in metadata
346
450
  * when populateMetadata is enabled.
@@ -363,30 +467,43 @@ function createAutoPopulateBuilder() {
363
467
  }
364
468
  /**
365
469
  * Common wrapper for transaction methods that broadcast.
470
+ * Emits transaction:broadcasted immediately with intended values,
471
+ * then fetches and emits transaction:fetched with actual values.
366
472
  */
367
473
  async function executeTrackedTransaction(args) {
368
- const { metadata, restArgs, execute, extractHash } = args;
474
+ const { metadata, restArgs, intendedParams, execute, extractHash } = args;
475
+ const broadcastTimestampMs = clock();
369
476
  const { from, intendedNonce } = await extractTransactionContext(args);
370
477
  const result = await execute({
371
478
  ...restArgs,
372
479
  nonce: intendedNonce,
373
480
  });
374
481
  const hash = extractHash(result);
375
- const actualNonce = await verifyTransactionNonce(hash, intendedNonce);
376
- const trackedTx = createTrackedTransactionRecord(hash, from, actualNonce, metadata);
377
- emitter.emit('transaction:broadcasted', trackedTx);
482
+ // Emit transaction:broadcasted immediately with intended values
483
+ const unknownTx = createUnknownTrackedTransaction(hash, from, intendedNonce, walletClient.chain?.id, metadata, broadcastTimestampMs, intendedParams);
484
+ emitter.emit('transaction:broadcasted', unknownTx);
485
+ // Fire-and-forget: fetch full data and emit transaction:fetched
486
+ fetchAndEmitFullData(hash, metadata, broadcastTimestampMs);
378
487
  return result;
379
488
  }
380
489
  /**
381
490
  * Common wrapper for raw transaction broadcasts.
491
+ * For raw transactions, we can parse full data immediately.
492
+ * Emits KnownTrackedTransaction directly to transaction:broadcasted.
382
493
  */
383
494
  async function executeTrackedRawTransaction(args) {
384
495
  const { serializedTransaction, metadata, execute, extractHash } = args;
385
- const { from, intendedNonce } = await extractRawTransactionContext(serializedTransaction);
496
+ const broadcastTimestampMs = clock();
497
+ const from = await recoverTransactionAddress({ serializedTransaction });
498
+ const parsedTx = parseTransaction(serializedTransaction);
499
+ // Execute the broadcast
386
500
  const result = await execute();
387
501
  const hash = extractHash(result);
388
- const trackedTx = createTrackedTransactionRecord(hash, from, intendedNonce, metadata);
389
- emitter.emit('transaction:broadcasted', trackedTx);
502
+ // For raw transactions, we can parse full data immediately
503
+ const knownTx = createKnownTrackedTransactionFromRaw(parsedTx, from, hash, metadata, walletClient.chain?.id, broadcastTimestampMs);
504
+ // Emit as KnownTrackedTransaction since we have all data
505
+ emitter.emit('transaction:broadcasted', knownTx);
506
+ // We do not emit fetched as the tx is already known
390
507
  return result;
391
508
  }
392
509
  return {
@@ -406,22 +523,26 @@ function createAutoPopulateBuilder() {
406
523
  functionName: args.functionName,
407
524
  args: args.args,
408
525
  };
526
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
409
527
  return executeTrackedTransaction({
410
528
  account: normalizeAccount(args.account),
411
529
  nonce,
412
530
  metadata: finalMetadata,
413
531
  restArgs: writeArgs,
532
+ intendedParams,
414
533
  execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
415
534
  extractHash: (hash) => hash,
416
535
  });
417
536
  },
418
537
  async sendTransaction(args) {
419
538
  const { metadata, nonce, ...sendArgs } = args;
539
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
420
540
  return executeTrackedTransaction({
421
541
  account: normalizeAccount(args.account),
422
542
  nonce,
423
543
  metadata: metadata,
424
544
  restArgs: sendArgs,
545
+ intendedParams,
425
546
  execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
426
547
  extractHash: (hash) => hash,
427
548
  });
@@ -449,22 +570,26 @@ function createAutoPopulateBuilder() {
449
570
  functionName: args.functionName,
450
571
  args: args.args,
451
572
  };
573
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
452
574
  return executeTrackedTransaction({
453
575
  account: normalizeAccount(args.account),
454
576
  nonce,
455
577
  metadata: finalMetadata,
456
578
  restArgs: writeArgs,
579
+ intendedParams,
457
580
  execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
458
581
  extractHash: (receipt) => receipt.transactionHash,
459
582
  });
460
583
  },
461
584
  async sendTransactionSync(args) {
462
585
  const { metadata, nonce, ...sendArgs } = args;
586
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
463
587
  return executeTrackedTransaction({
464
588
  account: normalizeAccount(args.account),
465
589
  nonce,
466
590
  metadata: metadata,
467
591
  restArgs: sendArgs,
592
+ intendedParams,
468
593
  execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
469
594
  extractHash: (receipt) => receipt.transactionHash,
470
595
  });
@@ -481,8 +606,8 @@ function createAutoPopulateBuilder() {
481
606
  // ============================================
482
607
  // Event subscription methods
483
608
  // ============================================
484
- onTransactionBroadcasted: (listener) => emitter.on('transaction:broadcasted', listener),
485
- offTransactionBroadcasted: (listener) => emitter.off('transaction:broadcasted', listener),
609
+ on: emitter.on.bind(emitter),
610
+ off: emitter.off.bind(emitter),
486
611
  };
487
612
  },
488
613
  };