@vultisig/cli 2.8.0 → 2.8.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @vultisig/cli
2
2
 
3
+ ## 2.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#867](https://github.com/vultisig/vultisig-sdk/pull/867) [`ddd08af`](https://github.com/vultisig/vultisig-sdk/commit/ddd08af883a1b2ee2f72dac4d406782de9090672) Thanks [@neavra](https://github.com/neavra)! - Agent: poll for final on-chain confirmation after broadcasting a signed tx
8
+ (audit F1). A `pending` `tx_status` only means "broadcast accepted" — the tx can
9
+ still revert, expire, or be dropped. After broadcast the session now polls
10
+ `vault.getTxStatus` until the tx reaches a final state and emits `confirmed` /
11
+ `failed`, or `timeout` when the bounded poll budget (~120s) is exhausted. The
12
+ `ask` result records the latest per-tx `status` (deduped by hash), and the pipe
13
+ `tx_status` event gains a `timeout` status. Best-effort and non-fatal: when the
14
+ chain can't be resolved or the vault can't poll status, the existing `pending`
15
+ status stands. The blocking confirmation wait is scoped to the top of the
16
+ message loop (depth 0 — the single-tx ask/pipe case); inside a multi-turn tool
17
+ loop a leg keeps its honest `pending` instead of stacking the poll budget per
18
+ tx. The shared `pending | confirmed | failed | timeout` union is now threaded
19
+ through the ask result, pipe event, and UI callback without unchecked casts.
20
+
3
21
  ## 2.8.0
4
22
 
5
23
  ### Patch Changes
package/dist/index.js CHANGED
@@ -8224,9 +8224,15 @@ var AskInterface = class {
8224
8224
  onSuggestions: (_suggestions) => {
8225
8225
  },
8226
8226
  onTxStatus: (txHash, chain, status, explorerUrl) => {
8227
- this.transactions.push({ hash: txHash, chain, ...status ? { status } : {}, explorerUrl });
8227
+ const existing = this.transactions.find((t) => t.hash === txHash);
8228
+ if (existing) {
8229
+ existing.status = status;
8230
+ if (explorerUrl) existing.explorerUrl = explorerUrl;
8231
+ } else {
8232
+ this.transactions.push({ hash: txHash, chain, explorerUrl, status });
8233
+ }
8228
8234
  if (this.verbose) {
8229
- process.stderr.write(`[tx] ${chain}: ${txHash}
8235
+ process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
8230
8236
  `);
8231
8237
  }
8232
8238
  },
@@ -12080,6 +12086,8 @@ var CLIENT_SIDE_TOOL_DISPATCH = {
12080
12086
  var MAX_MESSAGE_LOOP_DEPTH = 16;
12081
12087
  var RECOVERY_POLL_INTERVAL_MS = 2e3;
12082
12088
  var RECOVERY_MAX_POLLS = 90;
12089
+ var TX_CONFIRM_POLL_INTERVAL_MS = 3e3;
12090
+ var TX_CONFIRM_MAX_POLLS = 40;
12083
12091
  var AgentSession = class {
12084
12092
  client;
12085
12093
  vault;
@@ -12097,6 +12105,10 @@ var AgentSession = class {
12097
12105
  // poll loop without real 2s waits.
12098
12106
  recoveryPollIntervalMs = RECOVERY_POLL_INTERVAL_MS;
12099
12107
  recoveryMaxPolls = RECOVERY_MAX_POLLS;
12108
+ // Post-broadcast confirmation poll cadence — instance fields so tests can
12109
+ // drive the loop without real waits.
12110
+ txConfirmPollIntervalMs = TX_CONFIRM_POLL_INTERVAL_MS;
12111
+ txConfirmMaxPolls = TX_CONFIRM_MAX_POLLS;
12100
12112
  constructor(vault, config) {
12101
12113
  this.vault = vault;
12102
12114
  this.config = config;
@@ -12375,7 +12387,9 @@ var AgentSession = class {
12375
12387
  const txHash = recent.data.tx_hash;
12376
12388
  const chain = recent.data.chain;
12377
12389
  const explorerUrl = recent.data.explorer_url;
12378
- if (txHash) ui.onTxStatus(txHash, chain || "", "pending", explorerUrl);
12390
+ if (txHash) {
12391
+ await this.emitAndConfirmTx(txHash, chain, explorerUrl, depth, ui);
12392
+ }
12379
12393
  }
12380
12394
  await this.processMessageLoop(null, ui, depth + 1);
12381
12395
  return;
@@ -12434,6 +12448,75 @@ var AgentSession = class {
12434
12448
  recoverySleep() {
12435
12449
  return new Promise((resolve) => setTimeout(resolve, this.recoveryPollIntervalMs));
12436
12450
  }
12451
+ /**
12452
+ * Post-broadcast confirmation polling (audit F1). A bare `pending` status only
12453
+ * means "broadcast accepted"; the tx can still revert, expire, or be dropped,
12454
+ * so a headless caller that stops at `pending` may mark a later-reverted
12455
+ * operation complete. Poll vault.getTxStatus until the tx reaches a final
12456
+ * state and emit the matching lifecycle status (`confirmed`/`failed`), or
12457
+ * `timeout` when the bounded poll budget is exhausted (the tx may still
12458
+ * confirm later — callers can re-check with `vultisig tx-status`).
12459
+ *
12460
+ * Transient RPC/network errors are treated as "not final yet" and retried
12461
+ * until the budget is spent. Best-effort and non-fatal: if the chain can't be
12462
+ * resolved or the vault doesn't expose getTxStatus, the caller's already-
12463
+ * emitted `pending` status stands and this returns quietly.
12464
+ *
12465
+ * Scoped to headless callers (ask/pipe) that need machine-readable finality.
12466
+ * The interactive TUI already shows `pending` + an explorer link immediately
12467
+ * and has the dedicated `vultisig tx-status` command, so blocking its prompt
12468
+ * for the full poll budget would be a UX regression the audit didn't scope.
12469
+ * The poll also bails on cancel (Ctrl-C aborts the controller) so a long wait
12470
+ * is interruptible.
12471
+ *
12472
+ * The caller only invokes this at message-loop depth 0 (see the call site):
12473
+ * inside a multi-turn tool loop the broadcast result already drives the next
12474
+ * turn, so blocking here would stack the poll budget per leg without feeding
12475
+ * the server any extra signal. Those deeper legs keep their honest `pending`.
12476
+ */
12477
+ async emitAndConfirmTx(txHash, chain, explorerUrl, depth, ui) {
12478
+ ui.onTxStatus(txHash, chain || "", "pending", explorerUrl);
12479
+ if (depth === 0) {
12480
+ await this.confirmBroadcastedTx(txHash, chain, explorerUrl, ui);
12481
+ }
12482
+ }
12483
+ async confirmBroadcastedTx(txHash, chainName, explorerUrl, ui) {
12484
+ if (!this.config.askMode && !this.config.viaAgent) return;
12485
+ const chain = resolveChain(chainName ?? "");
12486
+ if (!chain || typeof this.vault?.getTxStatus !== "function") return;
12487
+ for (let attempt = 0; attempt < this.txConfirmMaxPolls; attempt++) {
12488
+ if (this.abortController?.signal?.aborted) return;
12489
+ try {
12490
+ const result = await this.vault.getTxStatus({ chain, txHash });
12491
+ if (result.status === "success") {
12492
+ ui.onTxStatus(txHash, chainName ?? "", "confirmed", explorerUrl);
12493
+ return;
12494
+ }
12495
+ if (result.status === "error") {
12496
+ ui.onTxStatus(txHash, chainName ?? "", "failed", explorerUrl);
12497
+ return;
12498
+ }
12499
+ } catch (err) {
12500
+ if (this.config.verbose) {
12501
+ process.stderr.write(`[session] tx confirm poll ${attempt + 1} failed: ${err?.message ?? err}
12502
+ `);
12503
+ }
12504
+ }
12505
+ if (attempt < this.txConfirmMaxPolls - 1) await this.txConfirmSleep();
12506
+ }
12507
+ if (this.abortController?.signal?.aborted) return;
12508
+ if (this.config.verbose) {
12509
+ process.stderr.write(
12510
+ `[session] tx ${txHash} not confirmed within ${this.txConfirmMaxPolls} polls; emitting timeout
12511
+ `
12512
+ );
12513
+ }
12514
+ ui.onTxStatus(txHash, chainName ?? "", "timeout", explorerUrl);
12515
+ }
12516
+ /** Sleep between confirmation polls. Separate method so tests can stub it out. */
12517
+ txConfirmSleep() {
12518
+ return new Promise((resolve) => setTimeout(resolve, this.txConfirmPollIntervalMs));
12519
+ }
12437
12520
  /**
12438
12521
  * Fold a recovered assistant message back into the live stream result: the
12439
12522
  * authoritative message wins over any partial deltas, and any persisted
@@ -13283,7 +13366,7 @@ var cachedVersion = null;
13283
13366
  function getVersion() {
13284
13367
  if (cachedVersion) return cachedVersion;
13285
13368
  if (true) {
13286
- cachedVersion = "2.8.0";
13369
+ cachedVersion = "2.8.1";
13287
13370
  return cachedVersion;
13288
13371
  }
13289
13372
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vultisig/cli",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "The self-custody MPC wallet CLI for AI coding agents (Claude Code, Cursor, OpenCode). Natural-language agent mode, 36+ chains, DKLS23 threshold signatures. Seedless.",
5
5
  "type": "module",
6
6
  "bin": {