@etherkit/viem-tx-tracker 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,38 +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
31
41
  }
32
42
  /**
33
- * Create a tracked wallet client that wraps a viem WalletClient.
34
- *
35
- * The tracked client provides the same API as WalletClient but with:
36
- * - Metadata field for transaction tracking (required unless TMetadata includes undefined)
37
- * - Automatic nonce fetching (with 'pending' by default)
38
- * - Post-broadcast transaction verification
39
- * - Event emission for tracking
40
- *
41
- * @typeParam TMetadata - The metadata type. Use `MyMeta | undefined` to make metadata optional.
42
- * @returns A builder with a `.using()` method to provide the wallet and public clients
43
- *
44
- * @example
45
- * ```typescript
46
- * // With required metadata
47
- * const tracked = createTrackedWalletClient<{purpose: string}>()
48
- * .using(walletClient, publicClient);
49
- *
50
- * // With optional metadata
51
- * const tracked = createTrackedWalletClient<{purpose: string} | undefined>()
52
- * .using(walletClient, publicClient);
53
- * ```
43
+ * Create an UnknownTrackedTransaction for immediate emission.
44
+ * Populates all known intended values from the transaction parameters.
54
45
  */
55
- export function createTrackedWalletClient() {
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
+ };
199
+ }
200
+ // Implementation
201
+ export function createTrackedWalletClient(options) {
202
+ const populateMetadata = options?.populateMetadata ?? false;
203
+ const clock = options?.clock ?? Date.now;
204
+ if (populateMetadata) {
205
+ return createAutoPopulateBuilder(clock);
206
+ }
56
207
  return {
57
208
  using(walletClient, publicClient) {
58
- // Create emitter for transaction broadcast events
209
+ // Create emitter for transaction events
59
210
  const emitter = new Emitter();
60
211
  /**
61
212
  * Resolve the nonce to use for a transaction.
@@ -96,104 +247,263 @@ export function createTrackedWalletClient() {
96
247
  return { from, intendedNonce };
97
248
  }
98
249
  /**
99
- * Extract transaction context from a serialized (signed) transaction.
100
- * Parses the transaction and recovers the sender address.
101
- *
102
- * @param serializedTransaction - The RLP-encoded signed transaction
103
- * @returns TransactionContext with from address and nonce
250
+ * Fetch full transaction data and emit transaction:fetched event.
251
+ * Non-blocking, runs in background. Does not throw.
252
+ */
253
+ async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
254
+ try {
255
+ const tx = await publicClient.getTransaction({ hash });
256
+ const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
257
+ emitter.emit('transaction:fetched', knownTx);
258
+ }
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}`);
263
+ }
264
+ }
265
+ /**
266
+ * Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
267
+ * Emits transaction:broadcasted immediately with intended values,
268
+ * then fetches and emits transaction:fetched with actual values.
104
269
  */
105
- async function extractRawTransactionContext(serializedTransaction) {
106
- // Parse the serialized transaction to get the nonce
270
+ async function executeTrackedTransaction(args) {
271
+ const { metadata, restArgs, intendedParams, execute, extractHash } = args;
272
+ const broadcastTimestampMs = clock();
273
+ // Extract common context
274
+ const { from, intendedNonce } = await extractTransactionContext(args);
275
+ // Execute the underlying transaction with nonce injected
276
+ const result = await execute({
277
+ ...restArgs,
278
+ nonce: intendedNonce,
279
+ });
280
+ const hash = extractHash(result);
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);
286
+ return result;
287
+ }
288
+ /**
289
+ * Common wrapper for raw transaction broadcasts (sendRawTransaction).
290
+ * For raw transactions, we can parse full data immediately.
291
+ * Emits KnownTrackedTransaction directly to transaction:broadcasted.
292
+ */
293
+ async function executeTrackedRawTransaction(args) {
294
+ const { serializedTransaction, metadata, execute, extractHash } = args;
295
+ const broadcastTimestampMs = clock();
296
+ const from = await recoverTransactionAddress({ serializedTransaction });
107
297
  const parsedTx = parseTransaction(serializedTransaction);
108
- if (parsedTx.nonce === undefined) {
109
- throw new Error('[TrackedWalletClient] Could not extract nonce from serialized transaction.');
298
+ // Execute the broadcast
299
+ const result = await execute();
300
+ const hash = extractHash(result);
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);
307
+ return result;
308
+ }
309
+ return {
310
+ walletClient: walletClient,
311
+ publicClient,
312
+ // ============================================
313
+ // Async methods (return hash)
314
+ // ============================================
315
+ async writeContract(args) {
316
+ const { metadata, nonce, ...writeArgs } = args;
317
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
318
+ return executeTrackedTransaction({
319
+ account: normalizeAccount(args.account),
320
+ nonce,
321
+ metadata: metadata,
322
+ restArgs: writeArgs,
323
+ intendedParams,
324
+ execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
325
+ extractHash: (hash) => hash,
326
+ });
327
+ },
328
+ async sendTransaction(args) {
329
+ const { metadata, nonce, ...sendArgs } = args;
330
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
331
+ return executeTrackedTransaction({
332
+ account: normalizeAccount(args.account),
333
+ nonce,
334
+ metadata: metadata,
335
+ restArgs: sendArgs,
336
+ intendedParams,
337
+ execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
338
+ extractHash: (hash) => hash,
339
+ });
340
+ },
341
+ async sendRawTransaction(args) {
342
+ const { metadata, serializedTransaction } = args;
343
+ return executeTrackedRawTransaction({
344
+ serializedTransaction,
345
+ metadata: metadata,
346
+ execute: () => walletClient.sendRawTransaction({ serializedTransaction }),
347
+ extractHash: (hash) => hash,
348
+ });
349
+ },
350
+ // ============================================
351
+ // Sync methods (return receipt, wait for confirmation)
352
+ // ============================================
353
+ async writeContractSync(args) {
354
+ const { metadata, nonce, ...writeArgs } = args;
355
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
356
+ return executeTrackedTransaction({
357
+ account: normalizeAccount(args.account),
358
+ nonce,
359
+ metadata: metadata,
360
+ restArgs: writeArgs,
361
+ intendedParams,
362
+ execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
363
+ extractHash: (receipt) => receipt.transactionHash,
364
+ });
365
+ },
366
+ async sendTransactionSync(args) {
367
+ const { metadata, nonce, ...sendArgs } = args;
368
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
369
+ return executeTrackedTransaction({
370
+ account: normalizeAccount(args.account),
371
+ nonce,
372
+ metadata: metadata,
373
+ restArgs: sendArgs,
374
+ intendedParams,
375
+ execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
376
+ extractHash: (receipt) => receipt.transactionHash,
377
+ });
378
+ },
379
+ async sendRawTransactionSync(args) {
380
+ const { metadata, serializedTransaction } = args;
381
+ return executeTrackedRawTransaction({
382
+ serializedTransaction,
383
+ metadata: metadata,
384
+ execute: () => walletClient.sendRawTransactionSync({ serializedTransaction }),
385
+ extractHash: (receipt) => receipt.transactionHash,
386
+ });
387
+ },
388
+ // ============================================
389
+ // Event subscription methods
390
+ // ============================================
391
+ on: emitter.on.bind(emitter),
392
+ off: emitter.off.bind(emitter),
393
+ };
394
+ },
395
+ };
396
+ }
397
+ /**
398
+ * Create an auto-populate builder for TrackedWalletClient.
399
+ * This builder auto-populates operation, functionName and args in writeContract metadata.
400
+ */
401
+ function createAutoPopulateBuilder(clock) {
402
+ return {
403
+ using(walletClient, publicClient) {
404
+ // Create emitter for transaction events
405
+ const emitter = new Emitter();
406
+ /**
407
+ * Resolve the nonce to use for a transaction.
408
+ */
409
+ async function resolveNonce(nonceOption, from) {
410
+ if (typeof nonceOption === 'number') {
411
+ return nonceOption;
110
412
  }
111
- // Recover the sender address from the signature
112
- const from = await recoverTransactionAddress({
113
- serializedTransaction,
413
+ const blockTag = isBlockTag(nonceOption) ? nonceOption : 'pending';
414
+ return await publicClient.getTransactionCount({
415
+ address: from,
416
+ blockTag,
114
417
  });
115
- return {
116
- from,
117
- intendedNonce: parsedTx.nonce,
118
- };
119
418
  }
120
419
  /**
121
- * Fetch the transaction after broadcast to verify nonce.
122
- * Logs a warning if the nonce was overridden or if tx cannot be found.
123
- *
124
- * @param hash - The transaction hash
125
- * @param intendedNonce - The nonce we intended to use
126
- * @returns The actual nonce, or the intended nonce if fetch failed
420
+ * Extract common transaction context (account, nonce) from request args.
421
+ */
422
+ async function extractTransactionContext(args) {
423
+ const account = args.account ?? walletClient.account;
424
+ const from = resolveAccountAddress(account);
425
+ if (!from) {
426
+ throw new Error('[TrackedWalletClient] No account available. ' +
427
+ 'Provide an account in the request or configure the wallet client with an account.');
428
+ }
429
+ const intendedNonce = await resolveNonce(args.nonce, from);
430
+ return { from, intendedNonce };
431
+ }
432
+ /**
433
+ * Fetch full transaction data and emit transaction:fetched event.
434
+ * Non-blocking, runs in background. Does not throw.
127
435
  */
128
- async function verifyTransactionNonce(hash, intendedNonce) {
436
+ async function fetchAndEmitFullData(hash, metadata, broadcastTimestampMs) {
129
437
  try {
130
438
  const tx = await publicClient.getTransaction({ hash });
131
- const actualNonce = tx.nonce;
132
- if (actualNonce !== intendedNonce) {
133
- console.warn(`[TrackedWalletClient] Nonce mismatch: intended ${intendedNonce}, actual ${actualNonce}. ` +
134
- `Wallet may have overridden the nonce.`);
135
- }
136
- return actualNonce;
439
+ const knownTx = createKnownTrackedTransaction(tx, metadata, broadcastTimestampMs);
440
+ emitter.emit('transaction:fetched', knownTx);
137
441
  }
138
- catch (fetchError) {
139
- // Transaction not found in mempool/chain yet
140
- console.warn(`[TrackedWalletClient] Could not fetch tx ${hash} after broadcast. ` +
141
- `It may not be in the mempool yet.`);
142
- 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}`);
143
446
  }
144
447
  }
145
448
  /**
146
- * Create a tracked transaction record.
449
+ * Validate that user didn't provide operation, functionName or args in metadata
450
+ * when populateMetadata is enabled.
147
451
  */
148
- function createTrackedTransactionRecord(txHash, from, nonce, metadata) {
149
- return {
150
- hash: txHash,
151
- from,
152
- nonce,
153
- chainId: walletClient.chain?.id,
154
- metadata,
155
- broadcastTimestampMs: Date.now(),
156
- };
452
+ function validateNoAutoPopulatedFieldsInMetadata(userMetadata) {
453
+ if (userMetadata && typeof userMetadata === 'object') {
454
+ if ('type' in userMetadata) {
455
+ throw new Error('[TrackedWalletClient] Cannot specify type in metadata when populateMetadata is enabled. ' +
456
+ 'The type is automatically populated from the contract call.');
457
+ }
458
+ if ('functionName' in userMetadata) {
459
+ throw new Error('[TrackedWalletClient] Cannot specify functionName in metadata when populateMetadata is enabled. ' +
460
+ 'The functionName is automatically populated from the contract call.');
461
+ }
462
+ if ('args' in userMetadata) {
463
+ throw new Error('[TrackedWalletClient] Cannot specify args in metadata when populateMetadata is enabled. ' +
464
+ 'The args are automatically populated from the contract call.');
465
+ }
466
+ }
157
467
  }
158
468
  /**
159
- * Common wrapper for transaction methods that broadcast (sendTransaction, writeContract).
160
- * Handles nonce resolution, underlying call, post-broadcast verification, and tracking record creation.
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.
161
472
  */
162
473
  async function executeTrackedTransaction(args) {
163
- const { metadata, restArgs, execute, extractHash } = args;
164
- // Extract common context
474
+ const { metadata, restArgs, intendedParams, execute, extractHash } = args;
475
+ const broadcastTimestampMs = clock();
165
476
  const { from, intendedNonce } = await extractTransactionContext(args);
166
- // Execute the underlying transaction with nonce injected
167
477
  const result = await execute({
168
478
  ...restArgs,
169
479
  nonce: intendedNonce,
170
480
  });
171
481
  const hash = extractHash(result);
172
- // Verify transaction and get actual nonce
173
- const actualNonce = await verifyTransactionNonce(hash, intendedNonce);
174
- // Create tracked transaction record
175
- const trackedTx = createTrackedTransactionRecord(hash, from, actualNonce, metadata);
176
- // Emit transaction broadcasted event
177
- 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);
178
487
  return result;
179
488
  }
180
489
  /**
181
- * Common wrapper for raw transaction broadcasts (sendRawTransaction).
182
- * Decodes the transaction to extract from/nonce, broadcasts, and creates tracking record.
490
+ * Common wrapper for raw transaction broadcasts.
491
+ * For raw transactions, we can parse full data immediately.
492
+ * Emits KnownTrackedTransaction directly to transaction:broadcasted.
183
493
  */
184
494
  async function executeTrackedRawTransaction(args) {
185
495
  const { serializedTransaction, metadata, execute, extractHash } = args;
186
- // Extract context from the serialized transaction
187
- const { from, intendedNonce } = await extractRawTransactionContext(serializedTransaction);
496
+ const broadcastTimestampMs = clock();
497
+ const from = await recoverTransactionAddress({ serializedTransaction });
498
+ const parsedTx = parseTransaction(serializedTransaction);
188
499
  // Execute the broadcast
189
500
  const result = await execute();
190
501
  const hash = extractHash(result);
191
- // For raw transactions, the nonce is already embedded, so no verification needed
192
- // (wallet cannot override nonce in an already-signed transaction)
193
- // Create tracked transaction record
194
- const trackedTx = createTrackedTransactionRecord(hash, from, intendedNonce, metadata);
195
- // Emit transaction broadcasted event
196
- 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
197
507
  return result;
198
508
  }
199
509
  return {
@@ -203,23 +513,36 @@ export function createTrackedWalletClient() {
203
513
  // Async methods (return hash)
204
514
  // ============================================
205
515
  async writeContract(args) {
206
- const { metadata, nonce, ...writeArgs } = args;
516
+ const { metadata: userMetadata, nonce, ...writeArgs } = args;
517
+ // Validate that user didn't provide operation, functionName or args
518
+ validateNoAutoPopulatedFieldsInMetadata(userMetadata);
519
+ // Auto-populate type, functionName and args
520
+ const finalMetadata = {
521
+ ...(userMetadata ?? {}),
522
+ type: 'functionCall',
523
+ functionName: args.functionName,
524
+ args: args.args,
525
+ };
526
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
207
527
  return executeTrackedTransaction({
208
528
  account: normalizeAccount(args.account),
209
529
  nonce,
210
- metadata: metadata,
530
+ metadata: finalMetadata,
211
531
  restArgs: writeArgs,
532
+ intendedParams,
212
533
  execute: (argsWithNonce) => walletClient.writeContract(argsWithNonce),
213
534
  extractHash: (hash) => hash,
214
535
  });
215
536
  },
216
537
  async sendTransaction(args) {
217
538
  const { metadata, nonce, ...sendArgs } = args;
539
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
218
540
  return executeTrackedTransaction({
219
541
  account: normalizeAccount(args.account),
220
542
  nonce,
221
543
  metadata: metadata,
222
544
  restArgs: sendArgs,
545
+ intendedParams,
223
546
  execute: (argsWithNonce) => walletClient.sendTransaction(argsWithNonce),
224
547
  extractHash: (hash) => hash,
225
548
  });
@@ -237,23 +560,36 @@ export function createTrackedWalletClient() {
237
560
  // Sync methods (return receipt, wait for confirmation)
238
561
  // ============================================
239
562
  async writeContractSync(args) {
240
- const { metadata, nonce, ...writeArgs } = args;
563
+ const { metadata: userMetadata, nonce, ...writeArgs } = args;
564
+ // Validate that user didn't provide operation, functionName or args
565
+ validateNoAutoPopulatedFieldsInMetadata(userMetadata);
566
+ // Auto-populate type, functionName and args
567
+ const finalMetadata = {
568
+ ...(userMetadata ?? {}),
569
+ type: 'functionCall',
570
+ functionName: args.functionName,
571
+ args: args.args,
572
+ };
573
+ const intendedParams = extractIntendedParamsFromWriteContract(args);
241
574
  return executeTrackedTransaction({
242
575
  account: normalizeAccount(args.account),
243
576
  nonce,
244
- metadata: metadata,
577
+ metadata: finalMetadata,
245
578
  restArgs: writeArgs,
579
+ intendedParams,
246
580
  execute: (argsWithNonce) => walletClient.writeContractSync(argsWithNonce),
247
581
  extractHash: (receipt) => receipt.transactionHash,
248
582
  });
249
583
  },
250
584
  async sendTransactionSync(args) {
251
585
  const { metadata, nonce, ...sendArgs } = args;
586
+ const intendedParams = extractIntendedParamsFromSendTransaction(args);
252
587
  return executeTrackedTransaction({
253
588
  account: normalizeAccount(args.account),
254
589
  nonce,
255
590
  metadata: metadata,
256
591
  restArgs: sendArgs,
592
+ intendedParams,
257
593
  execute: (argsWithNonce) => walletClient.sendTransactionSync(argsWithNonce),
258
594
  extractHash: (receipt) => receipt.transactionHash,
259
595
  });
@@ -270,8 +606,8 @@ export function createTrackedWalletClient() {
270
606
  // ============================================
271
607
  // Event subscription methods
272
608
  // ============================================
273
- onTransactionBroadcasted: (listener) => emitter.on('transaction:broadcasted', listener),
274
- offTransactionBroadcasted: (listener) => emitter.off('transaction:broadcasted', listener),
609
+ on: emitter.on.bind(emitter),
610
+ off: emitter.off.bind(emitter),
275
611
  };
276
612
  },
277
613
  };