@vultisig/cli 2.18.0 → 2.18.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.
- package/CHANGELOG.md +17 -0
- package/dist/index.js +187 -281
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @vultisig/cli
|
|
2
2
|
|
|
3
|
+
## 2.18.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1003](https://github.com/vultisig/vultisig-sdk/pull/1003) [`b27786d`](https://github.com/vultisig/vultisig-sdk/commit/b27786d8ac596ea3d2d4a13da958b266f589b73c) Thanks [@neavra](https://github.com/neavra)! - fix(agent): sign purely from the `tool-output-available` channel and remove the `tx_ready` signing path ([#927](https://github.com/vultisig/vultisig-sdk/issues/927) Phase 2). The client-enriched tool-output candidate (flat builders and `execute_*` prep) is now the sole signing source, matching what the production backend emits — it writes the signable payload on tool-output and emits `data-tx_ready` only as a hollow `{typed_confirm}` marker the CLI never consumed. Removes the Phase-1 dual-read + parity cross-check machinery, the tx_ready capture/selection, and the recovered-tx_ready replay. Fail-closed postures are preserved (a structurally-unsignable candidate is never buffered), and a disconnect that ran a signable tool now warns to re-run rather than signing. Patch: no CLI API change and the same real transactions still sign — this aligns the internal signing source with production.
|
|
8
|
+
|
|
9
|
+
- [#1024](https://github.com/vultisig/vultisig-sdk/pull/1024) [`3bc7904`](https://github.com/vultisig/vultisig-sdk/commit/3bc790403483dd7e90dac2efc33d7bc64c18b921) Thanks [@neavra](https://github.com/neavra)! - Stop `tx-status` from reporting malformed or never-seen transaction hashes as `pending` forever.
|
|
10
|
+
|
|
11
|
+
- The EVM status resolver now distinguishes a genuinely-pending tx (the node knows the hash, receipt still lagging) from one the node has never seen, returning a new terminal `not_found` status for the latter instead of an indefinite `pending`.
|
|
12
|
+
- New `isValidTxHash(chain, hash)` helper validates a hash's shape per chain-kind; the CLI `tx-status` command validates `--tx-hash` before any RPC and fails fast with `INVALID_INPUT` (exit 4) on a malformed hash.
|
|
13
|
+
- CLI `tx-status` polling is now bounded by a total wait budget (`--timeout <seconds>`, default 120) and exits non-zero on give-up — `TX_NOT_FOUND` (exit 5) when the node has no record of the hash, `TX_STATUS_TIMEOUT` (exit 3, retryable) when it is still pending.
|
|
14
|
+
- The poll loop now caps each sleep at the remaining wait budget instead of always sleeping the full poll interval, so a small `--timeout` gives up promptly instead of overshooting by up to one poll interval.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [[`ce38186`](https://github.com/vultisig/vultisig-sdk/commit/ce381864b977b19668702eae6e1ecad63ecbdf2b), [`3bc7904`](https://github.com/vultisig/vultisig-sdk/commit/3bc790403483dd7e90dac2efc33d7bc64c18b921)]:
|
|
17
|
+
- @vultisig/sdk@2.18.6
|
|
18
|
+
- @vultisig/core-chain@2.23.3
|
|
19
|
+
|
|
3
20
|
## 2.18.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -5400,6 +5400,21 @@ var TokenNotFoundError = class extends VsigError {
|
|
|
5400
5400
|
super(message, hint, suggestions, context);
|
|
5401
5401
|
}
|
|
5402
5402
|
};
|
|
5403
|
+
var TxNotFoundError = class extends VsigError {
|
|
5404
|
+
exitCode = 5 /* RESOURCE_NOT_FOUND */;
|
|
5405
|
+
code = "TX_NOT_FOUND";
|
|
5406
|
+
constructor(message, hint, suggestions, context) {
|
|
5407
|
+
super(message, hint, suggestions, context);
|
|
5408
|
+
}
|
|
5409
|
+
};
|
|
5410
|
+
var TxStatusTimeoutError = class extends VsigError {
|
|
5411
|
+
exitCode = 3 /* NETWORK */;
|
|
5412
|
+
code = "TX_STATUS_TIMEOUT";
|
|
5413
|
+
retryable = true;
|
|
5414
|
+
constructor(message, hint, suggestions, context) {
|
|
5415
|
+
super(message, hint, suggestions, context);
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5403
5418
|
var ExternalServiceError = class extends VsigError {
|
|
5404
5419
|
exitCode = 6 /* EXTERNAL_SERVICE */;
|
|
5405
5420
|
code = "EXTERNAL_SERVICE";
|
|
@@ -6538,33 +6553,78 @@ async function executeBroadcast(ctx2, params) {
|
|
|
6538
6553
|
}
|
|
6539
6554
|
|
|
6540
6555
|
// src/commands/tx-status.ts
|
|
6541
|
-
import { Chain as Chain5, Vultisig as Vultisig5 } from "@vultisig/sdk";
|
|
6556
|
+
import { Chain as Chain5, isValidTxHash, Vultisig as Vultisig5 } from "@vultisig/sdk";
|
|
6542
6557
|
var POLL_INTERVAL_MS = 5e3;
|
|
6543
|
-
|
|
6558
|
+
var DEFAULT_TIMEOUT_SEC = 120;
|
|
6559
|
+
var isTerminal = (status) => status === "success" || status === "error";
|
|
6560
|
+
function resolveTimeoutMs(timeoutSec) {
|
|
6561
|
+
if (typeof timeoutSec !== "number" || !Number.isFinite(timeoutSec)) {
|
|
6562
|
+
return DEFAULT_TIMEOUT_SEC * 1e3;
|
|
6563
|
+
}
|
|
6564
|
+
return Math.max(0, timeoutSec) * 1e3;
|
|
6565
|
+
}
|
|
6566
|
+
async function executeTxStatus(ctx2, params, opts = {}) {
|
|
6544
6567
|
const vault = await ctx2.ensureActiveVault();
|
|
6545
6568
|
if (!Object.values(Chain5).includes(params.chain)) {
|
|
6546
|
-
throw new
|
|
6569
|
+
throw new InvalidInputError(`Invalid chain: ${params.chain}`);
|
|
6570
|
+
}
|
|
6571
|
+
if (!isValidTxHash(params.chain, params.txHash)) {
|
|
6572
|
+
throw new InvalidInputError(
|
|
6573
|
+
`Invalid transaction hash for ${params.chain}: "${params.txHash}"`,
|
|
6574
|
+
"Check the hash \u2014 it must match the expected format for the chain.",
|
|
6575
|
+
void 0,
|
|
6576
|
+
{ chain: params.chain, txHash: params.txHash }
|
|
6577
|
+
);
|
|
6547
6578
|
}
|
|
6579
|
+
const pollIntervalMs = opts.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6548
6580
|
const spinner = createSpinner("Checking transaction status...");
|
|
6549
6581
|
try {
|
|
6550
6582
|
let result = await vault.getTxStatus({ chain: params.chain, txHash: params.txHash });
|
|
6551
|
-
if (!params.noWait) {
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6583
|
+
if (!params.noWait && !isTerminal(result.status)) {
|
|
6584
|
+
const deadline = Date.now() + resolveTimeoutMs(params.timeoutSec);
|
|
6585
|
+
let waited = 0;
|
|
6586
|
+
while (!isTerminal(result.status)) {
|
|
6587
|
+
const remainingMs = deadline - Date.now();
|
|
6588
|
+
if (remainingMs <= 0) {
|
|
6589
|
+
spinner.fail(`Gave up waiting after ${Math.round(waited / 1e3)}s (status: ${result.status})`);
|
|
6590
|
+
throw giveUpError(params, result, waited);
|
|
6591
|
+
}
|
|
6592
|
+
const sleepMs = Math.min(pollIntervalMs, remainingMs);
|
|
6593
|
+
waited += sleepMs;
|
|
6594
|
+
spinner.text = `Transaction ${result.status}... (${Math.round(waited / 1e3)}s)`;
|
|
6595
|
+
await sleep(sleepMs);
|
|
6556
6596
|
result = await vault.getTxStatus({ chain: params.chain, txHash: params.txHash });
|
|
6557
|
-
polls++;
|
|
6558
6597
|
}
|
|
6559
6598
|
}
|
|
6560
6599
|
spinner.succeed(`Transaction status: ${result.status}`);
|
|
6561
6600
|
displayResult(params.chain, params.txHash, result);
|
|
6562
6601
|
return result;
|
|
6563
6602
|
} catch (error2) {
|
|
6603
|
+
if (error2 instanceof TxNotFoundError || error2 instanceof TxStatusTimeoutError) {
|
|
6604
|
+
throw error2;
|
|
6605
|
+
}
|
|
6564
6606
|
spinner.fail("Failed to check transaction status");
|
|
6565
6607
|
throw error2;
|
|
6566
6608
|
}
|
|
6567
6609
|
}
|
|
6610
|
+
function giveUpError(params, result, waitedMs) {
|
|
6611
|
+
const seconds = Math.round(waitedMs / 1e3);
|
|
6612
|
+
const context = { chain: params.chain, txHash: params.txHash, status: result.status };
|
|
6613
|
+
if (result.status === "not_found") {
|
|
6614
|
+
return new TxNotFoundError(
|
|
6615
|
+
`Transaction not found on ${params.chain} after ${seconds}s: ${params.txHash}`,
|
|
6616
|
+
"The node has no record of this hash \u2014 it may have been dropped, replaced, or never broadcast.",
|
|
6617
|
+
["Verify the transaction hash", "Re-broadcast if it was never sent"],
|
|
6618
|
+
context
|
|
6619
|
+
);
|
|
6620
|
+
}
|
|
6621
|
+
return new TxStatusTimeoutError(
|
|
6622
|
+
`Transaction still ${result.status} on ${params.chain} after ${seconds}s: ${params.txHash}`,
|
|
6623
|
+
"The transaction may still confirm later.",
|
|
6624
|
+
["Re-run to keep checking", "Increase the wait budget with --timeout <seconds>"],
|
|
6625
|
+
context
|
|
6626
|
+
);
|
|
6627
|
+
}
|
|
6568
6628
|
function displayResult(chain, txHash, result) {
|
|
6569
6629
|
if (isJsonOutput()) {
|
|
6570
6630
|
outputJson({
|
|
@@ -8333,12 +8393,12 @@ var AskInterface = class {
|
|
|
8333
8393
|
}
|
|
8334
8394
|
},
|
|
8335
8395
|
onError: (message, code) => {
|
|
8336
|
-
const
|
|
8396
|
+
const isTerminal2 = isTerminalAgentErrorCode(code);
|
|
8337
8397
|
if (!this.hasAsked) {
|
|
8338
8398
|
this.initError = { message, code };
|
|
8339
|
-
} else if (!this.error ||
|
|
8399
|
+
} else if (!this.error || isTerminal2 && !this.errorIsTerminal) {
|
|
8340
8400
|
this.error = { message, code };
|
|
8341
|
-
this.errorIsTerminal =
|
|
8401
|
+
this.errorIsTerminal = isTerminal2;
|
|
8342
8402
|
}
|
|
8343
8403
|
process.stderr.write(`[error] ${message} [${code}]
|
|
8344
8404
|
`);
|
|
@@ -11769,7 +11829,7 @@ var CLI_SIGNABLE_FLAT_TOOLS = /* @__PURE__ */ new Set([
|
|
|
11769
11829
|
"build_max_subscription_renewal",
|
|
11770
11830
|
"build_pro_subscription_renewal"
|
|
11771
11831
|
]);
|
|
11772
|
-
var
|
|
11832
|
+
var CLI_SIGNABLE_PREP_TOOLS = /* @__PURE__ */ new Set([
|
|
11773
11833
|
"execute_swap",
|
|
11774
11834
|
"execute_send",
|
|
11775
11835
|
"execute_contract_call"
|
|
@@ -11829,6 +11889,22 @@ function asChainIdString(value) {
|
|
|
11829
11889
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
11830
11890
|
return void 0;
|
|
11831
11891
|
}
|
|
11892
|
+
function resolvePrepChain(txArgs) {
|
|
11893
|
+
const byName = asChainString(txArgs.chain) ? resolveChain(asChainString(txArgs.chain)) : null;
|
|
11894
|
+
const byId = asChainIdString(txArgs.chain_id) ? resolveChainId(asChainIdString(txArgs.chain_id)) : null;
|
|
11895
|
+
if (!byName && !byId) return null;
|
|
11896
|
+
if (byName && byId && byName !== byId) return null;
|
|
11897
|
+
return byName ?? byId;
|
|
11898
|
+
}
|
|
11899
|
+
function resolvePrepParentChain(env) {
|
|
11900
|
+
const byChain = asChainString(env.chain) ? resolveChain(asChainString(env.chain)) : null;
|
|
11901
|
+
const byFromChain = asChainString(env.from_chain) ? resolveChain(asChainString(env.from_chain)) : null;
|
|
11902
|
+
const byId = asChainIdString(env.chain_id) ? resolveChainId(asChainIdString(env.chain_id)) : null;
|
|
11903
|
+
const candidates = [byChain, byFromChain, byId].filter((c) => c !== null);
|
|
11904
|
+
if (candidates.length === 0) return null;
|
|
11905
|
+
if (candidates.some((c) => c !== candidates[0])) return null;
|
|
11906
|
+
return candidates[0];
|
|
11907
|
+
}
|
|
11832
11908
|
function buildTxReadyFromToolOutput(toolName, output) {
|
|
11833
11909
|
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName)) return null;
|
|
11834
11910
|
const env = asRecord(output);
|
|
@@ -11863,12 +11939,24 @@ function deriveToolOutputCandidate(toolName, output) {
|
|
|
11863
11939
|
const payload = buildTxReadyFromToolOutput(toolName, output);
|
|
11864
11940
|
return payload ? { payload, source: "flat", toolName } : null;
|
|
11865
11941
|
}
|
|
11866
|
-
if (
|
|
11942
|
+
if (CLI_SIGNABLE_PREP_TOOLS.has(toolName)) {
|
|
11867
11943
|
const env = asRecord(output);
|
|
11868
11944
|
if (!env || env.status === "error" || "error" in env) return null;
|
|
11869
11945
|
const txArgs = asRecord(env.txArgs);
|
|
11870
11946
|
if (!txArgs) return null;
|
|
11871
11947
|
if (typeof txArgs.tx_encoding !== "string" || txArgs.tx_encoding === "") return null;
|
|
11948
|
+
const prepChain = resolvePrepChain(txArgs);
|
|
11949
|
+
if (!prepChain) return null;
|
|
11950
|
+
const hasParentChainMetadata = asChainString(env.chain) !== void 0 || asChainString(env.from_chain) !== void 0 || asChainIdString(env.chain_id) !== void 0;
|
|
11951
|
+
if (hasParentChainMetadata) {
|
|
11952
|
+
const parentChain = resolvePrepParentChain(env);
|
|
11953
|
+
if (!parentChain || parentChain !== prepChain) return null;
|
|
11954
|
+
}
|
|
11955
|
+
const approvalTxArgs = asRecord(env.approvalTxArgs);
|
|
11956
|
+
if (approvalTxArgs) {
|
|
11957
|
+
const approvalChain = resolvePrepChain(approvalTxArgs);
|
|
11958
|
+
if (!approvalChain || approvalChain !== prepChain) return null;
|
|
11959
|
+
}
|
|
11872
11960
|
return { payload: env, source: "prep", toolName };
|
|
11873
11961
|
}
|
|
11874
11962
|
return null;
|
|
@@ -11878,105 +11966,6 @@ function str(value) {
|
|
|
11878
11966
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
11879
11967
|
return void 0;
|
|
11880
11968
|
}
|
|
11881
|
-
function nestedSource(legObj) {
|
|
11882
|
-
const nested = (asRecord(legObj.tx) || asRecord(legObj.swap_tx) || asRecord(legObj.send_tx) || asRecord(legObj.txArgs?.tx)) ?? asRecord(legObj.txArgs) ?? legObj;
|
|
11883
|
-
return nested;
|
|
11884
|
-
}
|
|
11885
|
-
function canonGasLimit(value) {
|
|
11886
|
-
if (typeof value === "number" && Number.isInteger(value) && value > 0) return String(value);
|
|
11887
|
-
if (typeof value === "string") {
|
|
11888
|
-
const t = value.trim();
|
|
11889
|
-
if (/^\d+$/.test(t) || /^0x[0-9a-fA-F]+$/.test(t)) {
|
|
11890
|
-
try {
|
|
11891
|
-
const n = BigInt(t);
|
|
11892
|
-
return n > 0n ? n.toString() : void 0;
|
|
11893
|
-
} catch {
|
|
11894
|
-
return void 0;
|
|
11895
|
-
}
|
|
11896
|
-
}
|
|
11897
|
-
}
|
|
11898
|
-
return void 0;
|
|
11899
|
-
}
|
|
11900
|
-
function canonLeg(legObj) {
|
|
11901
|
-
const src = nestedSource(legObj);
|
|
11902
|
-
const txArgs = asRecord(legObj.txArgs);
|
|
11903
|
-
const toRaw = str(src.to) ?? str(src.to_address);
|
|
11904
|
-
const dataRaw = str(src.data) ?? str(src.calldata);
|
|
11905
|
-
const leg = {
|
|
11906
|
-
to: toRaw ? toRaw.toLowerCase() : void 0,
|
|
11907
|
-
value: normalizeValue(src.value),
|
|
11908
|
-
data: dataRaw ? dataRaw.toLowerCase() : void 0,
|
|
11909
|
-
gasLimit: canonGasLimit(src.gas_limit),
|
|
11910
|
-
chain: str(legObj.chain) ?? str(txArgs?.chain) ?? str(src.chain),
|
|
11911
|
-
chainId: str(legObj.chain_id) ?? str(txArgs?.chain_id) ?? str(src.chain_id),
|
|
11912
|
-
txEncoding: str(src.tx_encoding) ?? str(txArgs?.tx_encoding),
|
|
11913
|
-
amount: str(src.amount) ?? str(txArgs?.amount),
|
|
11914
|
-
memo: str(src.memo) ?? str(txArgs?.memo)
|
|
11915
|
-
};
|
|
11916
|
-
return leg;
|
|
11917
|
-
}
|
|
11918
|
-
var TX_READY_EXCLUSIVE_KEYS = ["typed_confirm", "sequence_id", "sequence_index", "sequence_total"];
|
|
11919
|
-
function collectExclusive(payload) {
|
|
11920
|
-
const found = [];
|
|
11921
|
-
const scan = (obj) => {
|
|
11922
|
-
if (!obj) return;
|
|
11923
|
-
for (const k of TX_READY_EXCLUSIVE_KEYS) if (k in obj && !found.includes(k)) found.push(k);
|
|
11924
|
-
};
|
|
11925
|
-
scan(payload);
|
|
11926
|
-
scan(asRecord(payload.tx));
|
|
11927
|
-
scan(asRecord(payload.txArgs));
|
|
11928
|
-
return found;
|
|
11929
|
-
}
|
|
11930
|
-
function canonicalizeForParity(payload) {
|
|
11931
|
-
const env = asRecord(payload);
|
|
11932
|
-
if (!env) return null;
|
|
11933
|
-
const approval = asRecord(env.approvalTxArgs);
|
|
11934
|
-
const main = asRecord(env.txArgs);
|
|
11935
|
-
const legs = approval && main ? [canonLeg(approval), canonLeg(main)] : [canonLeg(env)];
|
|
11936
|
-
return { legs, exclusive: collectExclusive(env) };
|
|
11937
|
-
}
|
|
11938
|
-
var HARD_FIELDS = [
|
|
11939
|
-
"to",
|
|
11940
|
-
"value",
|
|
11941
|
-
"data",
|
|
11942
|
-
"chain",
|
|
11943
|
-
"chainId",
|
|
11944
|
-
"txEncoding",
|
|
11945
|
-
"amount",
|
|
11946
|
-
"memo",
|
|
11947
|
-
// gas_limit is signing-relevant, NOT advisory: signEvmServerTx copies a
|
|
11948
|
-
// server-supplied gas_limit into the signed ethereumSpecific.gasLimit when it
|
|
11949
|
-
// exceeds the SDK estimate, so a gas_limit divergence means different signed
|
|
11950
|
-
// bytes/fee. Parity must surface it.
|
|
11951
|
-
"gasLimit"
|
|
11952
|
-
];
|
|
11953
|
-
function diffToolOutputParity(enriched, txReady) {
|
|
11954
|
-
const ce = canonicalizeForParity(enriched);
|
|
11955
|
-
const ct = canonicalizeForParity(txReady);
|
|
11956
|
-
const divergences = [];
|
|
11957
|
-
if (!ce || !ct) {
|
|
11958
|
-
return {
|
|
11959
|
-
match: false,
|
|
11960
|
-
divergences: [`uncomparable payload (enriched=${ce ? "ok" : "null"}, tx_ready=${ct ? "ok" : "null"})`],
|
|
11961
|
-
txReadyExclusive: ct?.exclusive ?? []
|
|
11962
|
-
};
|
|
11963
|
-
}
|
|
11964
|
-
if (ce.legs.length !== ct.legs.length) {
|
|
11965
|
-
divergences.push(`leg count: tool-output ${ce.legs.length} vs tx_ready ${ct.legs.length}`);
|
|
11966
|
-
}
|
|
11967
|
-
const n = Math.min(ce.legs.length, ct.legs.length);
|
|
11968
|
-
for (let i = 0; i < n; i++) {
|
|
11969
|
-
for (const f of HARD_FIELDS) {
|
|
11970
|
-
const a = ce.legs[i][f];
|
|
11971
|
-
const b = ct.legs[i][f];
|
|
11972
|
-
if (a !== b && !(a === void 0 && b === void 0)) {
|
|
11973
|
-
divergences.push(`leg[${i}].${f}: tool-output ${a ?? "\u2205"} vs tx_ready ${b ?? "\u2205"}`);
|
|
11974
|
-
}
|
|
11975
|
-
}
|
|
11976
|
-
}
|
|
11977
|
-
const txReadyExclusive = ct.exclusive.filter((k) => !ce.exclusive.includes(k));
|
|
11978
|
-
return { match: divergences.length === 0, divergences, txReadyExclusive };
|
|
11979
|
-
}
|
|
11980
11969
|
function payloadLooksSignable(payload) {
|
|
11981
11970
|
const env = asRecord(payload);
|
|
11982
11971
|
if (!env) return false;
|
|
@@ -12266,7 +12255,6 @@ var AgentClient = class {
|
|
|
12266
12255
|
const result = {
|
|
12267
12256
|
fullText: "",
|
|
12268
12257
|
suggestions: [],
|
|
12269
|
-
transactions: [],
|
|
12270
12258
|
message: null,
|
|
12271
12259
|
finished: false,
|
|
12272
12260
|
disconnected: false,
|
|
@@ -12277,7 +12265,6 @@ var AgentClient = class {
|
|
|
12277
12265
|
serverNow: res.headers.get("X-Server-Now")
|
|
12278
12266
|
};
|
|
12279
12267
|
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
12280
|
-
const parity = { lastSignableToolCallId: null };
|
|
12281
12268
|
const reader = res.body.getReader();
|
|
12282
12269
|
const decoder = new TextDecoder();
|
|
12283
12270
|
let buffer = "";
|
|
@@ -12292,7 +12279,7 @@ var AgentClient = class {
|
|
|
12292
12279
|
currentData += (currentData ? "\n" : "") + stripLeadingSpace(line.slice(5));
|
|
12293
12280
|
} else if (line === "") {
|
|
12294
12281
|
if (currentData) {
|
|
12295
|
-
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId
|
|
12282
|
+
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId);
|
|
12296
12283
|
}
|
|
12297
12284
|
currentEvent = "";
|
|
12298
12285
|
currentData = "";
|
|
@@ -12312,7 +12299,7 @@ var AgentClient = class {
|
|
|
12312
12299
|
if (done) {
|
|
12313
12300
|
if (trailing) processLine(trailing);
|
|
12314
12301
|
if (currentData) {
|
|
12315
|
-
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId
|
|
12302
|
+
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId);
|
|
12316
12303
|
}
|
|
12317
12304
|
break;
|
|
12318
12305
|
}
|
|
@@ -12329,7 +12316,7 @@ var AgentClient = class {
|
|
|
12329
12316
|
}
|
|
12330
12317
|
return result;
|
|
12331
12318
|
}
|
|
12332
|
-
handleSSEEvent(event, data, result, callbacks, toolNameByCallId
|
|
12319
|
+
handleSSEEvent(event, data, result, callbacks, toolNameByCallId) {
|
|
12333
12320
|
try {
|
|
12334
12321
|
const parsed = JSON.parse(data);
|
|
12335
12322
|
const v1Type = getV1Type(parsed);
|
|
@@ -12340,7 +12327,7 @@ var AgentClient = class {
|
|
|
12340
12327
|
this.handleTextDelta(parsed, result, callbacks);
|
|
12341
12328
|
break;
|
|
12342
12329
|
case "tool_progress":
|
|
12343
|
-
this.handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type
|
|
12330
|
+
this.handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type);
|
|
12344
12331
|
break;
|
|
12345
12332
|
case "title": {
|
|
12346
12333
|
const title = v1Data?.title ?? parsed.title;
|
|
@@ -12353,15 +12340,6 @@ var AgentClient = class {
|
|
|
12353
12340
|
callbacks.onSuggestions?.(suggestions);
|
|
12354
12341
|
break;
|
|
12355
12342
|
}
|
|
12356
|
-
case "tx_ready":
|
|
12357
|
-
if (this.verbose) process.stderr.write(`[SSE:tx_ready] raw: ${data.slice(0, 2e3)}
|
|
12358
|
-
`);
|
|
12359
|
-
{
|
|
12360
|
-
const txReady = v1Data ?? parsed;
|
|
12361
|
-
result.transactions.push(txReady);
|
|
12362
|
-
callbacks.onTxReady?.(txReady, parity.lastSignableToolCallId ?? void 0);
|
|
12363
|
-
}
|
|
12364
|
-
break;
|
|
12365
12343
|
case "balance_summary": {
|
|
12366
12344
|
const card = v1Data ?? parsed.data ?? parsed;
|
|
12367
12345
|
callbacks.onBalanceSummary?.(card);
|
|
@@ -12400,7 +12378,7 @@ var AgentClient = class {
|
|
|
12400
12378
|
result.fullText += parsed.delta;
|
|
12401
12379
|
callbacks.onTextDelta?.(parsed.delta);
|
|
12402
12380
|
}
|
|
12403
|
-
handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type
|
|
12381
|
+
handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type) {
|
|
12404
12382
|
if (this.verbose) process.stderr.write(`[SSE:tool_progress] raw: ${data.slice(0, 1e3)}
|
|
12405
12383
|
`);
|
|
12406
12384
|
const status = parsed.status ?? v1StatusFromType(v1Type);
|
|
@@ -12413,32 +12391,27 @@ var AgentClient = class {
|
|
|
12413
12391
|
this.maybeEmitClientSideToolCall(parsed, callbacks, v1Type, callId, toolName);
|
|
12414
12392
|
const ok = deriveToolDoneOk(status, parsed.output);
|
|
12415
12393
|
if (status && toolName) callbacks.onToolProgress?.(toolName, status, label, ok);
|
|
12416
|
-
this.maybeSignToolOutput(status, toolName, parsed.output, callbacks
|
|
12394
|
+
this.maybeSignToolOutput(status, toolName, parsed.output, callbacks);
|
|
12417
12395
|
if (status === "done" && callId) toolNameByCallId.delete(callId);
|
|
12418
12396
|
}
|
|
12419
12397
|
/**
|
|
12420
|
-
* Phase
|
|
12421
|
-
*
|
|
12422
|
-
*
|
|
12423
|
-
* `tx_ready`
|
|
12424
|
-
*
|
|
12425
|
-
*
|
|
12426
|
-
*
|
|
12427
|
-
* `erc20_approve`, `build_custom_*`) → the PARITY reference; `tx_ready`
|
|
12428
|
-
* stays authoritative when signable.
|
|
12429
|
-
* The bridge guards against non-tx results (`no_op` / `insufficient_*` /
|
|
12430
|
-
* errors) so those never reach the signer. Zero backend change.
|
|
12398
|
+
* #927 Phase 2: derive a client-side signable candidate from a signable tool's
|
|
12399
|
+
* raw `tool-output-available` output — the same envelope mobile reads — and
|
|
12400
|
+
* hand it to the session via `onToolOutputTx` as the SOLE signing source
|
|
12401
|
+
* (production emits the payload here; `data-tx_ready` is a hollow marker). The
|
|
12402
|
+
* bridge guards against non-tx results (`no_op` / `insufficient_*` / errors)
|
|
12403
|
+
* and phantom-card prep envelopes so those never reach the signer. Zero backend
|
|
12404
|
+
* change.
|
|
12431
12405
|
*/
|
|
12432
|
-
maybeSignToolOutput(status, toolName, output, callbacks
|
|
12406
|
+
maybeSignToolOutput(status, toolName, output, callbacks) {
|
|
12433
12407
|
if (status !== "done" || !toolName || !callbacks.onToolOutputTx) return;
|
|
12434
|
-
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !
|
|
12408
|
+
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !CLI_SIGNABLE_PREP_TOOLS.has(toolName)) return;
|
|
12435
12409
|
const candidate = deriveToolOutputCandidate(toolName, output);
|
|
12436
12410
|
if (!candidate) return;
|
|
12437
|
-
if (callId) parity.lastSignableToolCallId = callId;
|
|
12438
12411
|
if (this.verbose)
|
|
12439
12412
|
process.stderr.write(`[SSE:tool_output] ${toolName} \u2192 onToolOutputTx (${candidate.source} candidate)
|
|
12440
12413
|
`);
|
|
12441
|
-
callbacks.onToolOutputTx(candidate.payload, toolName, candidate.source
|
|
12414
|
+
callbacks.onToolOutputTx(candidate.payload, toolName, candidate.source);
|
|
12442
12415
|
}
|
|
12443
12416
|
maybeEmitClientSideToolCall(parsed, callbacks, v1Type, callId, toolName) {
|
|
12444
12417
|
if (v1Type !== "tool-input-available" || !callId || !toolName || !this.clientSideToolNames.has(toolName) || !callbacks.onClientSideToolCall) {
|
|
@@ -12455,7 +12428,9 @@ var AgentClient = class {
|
|
|
12455
12428
|
// Maps a V1 `type` field to the legacy event bucket used by handleSSEEvent's
|
|
12456
12429
|
// switch. Frame-level types (start, text-start, text-end, finish-step) and
|
|
12457
12430
|
// non-critical telemetry (data-tokens, data-usage, data-confirmation) route
|
|
12458
|
-
// to 'ignore' which is a no-op.
|
|
12431
|
+
// to 'ignore' which is a no-op. `data-tx_ready` also routes to 'ignore':
|
|
12432
|
+
// #927 Phase 2 signs purely from `tool-output-available`, and production emits
|
|
12433
|
+
// `data-tx_ready` only as a hollow `{typed_confirm}` marker the CLI doesn't use.
|
|
12459
12434
|
mapV1EventType(type) {
|
|
12460
12435
|
switch (type) {
|
|
12461
12436
|
case "text-delta":
|
|
@@ -12469,8 +12444,6 @@ var AgentClient = class {
|
|
|
12469
12444
|
return "title";
|
|
12470
12445
|
case "data-suggestions":
|
|
12471
12446
|
return "suggestions";
|
|
12472
|
-
case "data-tx_ready":
|
|
12473
|
-
return "tx_ready";
|
|
12474
12447
|
case "data-balance_summary":
|
|
12475
12448
|
return "balance_summary";
|
|
12476
12449
|
case "data-turn_outcome":
|
|
@@ -13266,8 +13239,6 @@ var AgentSession = class {
|
|
|
13266
13239
|
`);
|
|
13267
13240
|
}
|
|
13268
13241
|
}
|
|
13269
|
-
let txReadyCandidate = null;
|
|
13270
|
-
let txReadyTwinCallId = null;
|
|
13271
13242
|
let toolOutputCandidate = null;
|
|
13272
13243
|
let balanceCardRendered = false;
|
|
13273
13244
|
const pendingDispatches = [];
|
|
@@ -13293,20 +13264,12 @@ var AgentSession = class {
|
|
|
13293
13264
|
onSuggestions: (suggestions) => {
|
|
13294
13265
|
ui.onSuggestions(suggestions);
|
|
13295
13266
|
},
|
|
13296
|
-
|
|
13297
|
-
if (txReadyCandidate !== null) {
|
|
13298
|
-
this.reportDeferredSignable(ui);
|
|
13299
|
-
return;
|
|
13300
|
-
}
|
|
13301
|
-
txReadyCandidate = tx;
|
|
13302
|
-
txReadyTwinCallId = toolCallId ?? null;
|
|
13303
|
-
},
|
|
13304
|
-
onToolOutputTx: (payload, toolName, source, toolCallId) => {
|
|
13267
|
+
onToolOutputTx: (payload, toolName, source) => {
|
|
13305
13268
|
if (toolOutputCandidate !== null) {
|
|
13306
13269
|
this.reportDeferredSignable(ui);
|
|
13307
13270
|
return;
|
|
13308
13271
|
}
|
|
13309
|
-
toolOutputCandidate = { payload, toolName, source
|
|
13272
|
+
toolOutputCandidate = { payload, toolName, source };
|
|
13310
13273
|
},
|
|
13311
13274
|
onBalanceSummary: (raw) => {
|
|
13312
13275
|
const card = parseBalanceSummaryEnvelope(raw);
|
|
@@ -13340,7 +13303,7 @@ var AgentSession = class {
|
|
|
13340
13303
|
}
|
|
13341
13304
|
if (streamResult.disconnected && !streamResult.message) {
|
|
13342
13305
|
ui.onReconnecting?.();
|
|
13343
|
-
await this.recoverDisconnectedTurn(streamResult, callbacks.
|
|
13306
|
+
await this.recoverDisconnectedTurn(streamResult, callbacks.onBalanceSummary);
|
|
13344
13307
|
}
|
|
13345
13308
|
const responseText = streamResult.message?.content || streamResult.fullText || "";
|
|
13346
13309
|
let displayText = stripLeakedToolCallTags(responseText);
|
|
@@ -13350,7 +13313,7 @@ var AgentSession = class {
|
|
|
13350
13313
|
if (displayText) {
|
|
13351
13314
|
ui.onAssistantMessage(displayText);
|
|
13352
13315
|
}
|
|
13353
|
-
const bufferedSignable = this.selectAndBufferSignable(
|
|
13316
|
+
const bufferedSignable = this.selectAndBufferSignable(toolOutputCandidate);
|
|
13354
13317
|
if (bufferedSignable) {
|
|
13355
13318
|
if (this.config.verbose) process.stderr.write(`[session] buffered a signable server tx, signing client-side
|
|
13356
13319
|
`);
|
|
@@ -13400,102 +13363,42 @@ var AgentSession = class {
|
|
|
13400
13363
|
);
|
|
13401
13364
|
}
|
|
13402
13365
|
/**
|
|
13403
|
-
* Phase
|
|
13404
|
-
*
|
|
13405
|
-
*
|
|
13366
|
+
* #927 Phase 2 sign gate. Buffer the client-enriched tool-output candidate —
|
|
13367
|
+
* the SOLE signing source — into the executor. Returns true when something was
|
|
13368
|
+
* buffered (signed downstream), false otherwise.
|
|
13406
13369
|
*
|
|
13407
|
-
*
|
|
13408
|
-
* 1.
|
|
13409
|
-
*
|
|
13410
|
-
* `
|
|
13411
|
-
*
|
|
13412
|
-
*
|
|
13413
|
-
*
|
|
13414
|
-
*
|
|
13415
|
-
*
|
|
13416
|
-
*
|
|
13417
|
-
* tool-output candidate.
|
|
13418
|
-
*
|
|
13419
|
-
* FAIL-CLOSED on divergence (review — gomesalexandre): a flat tool-output is a
|
|
13420
|
-
* sign source ONLY when (a) there is NO `tx_ready` at all, or (b) the same-turn
|
|
13421
|
-
* `tx_ready` MATCHED parity. Whenever a `tx_ready` is present and the flat did
|
|
13422
|
-
* not prove equal to it, we do NOT sign the client-enriched candidate — we fall
|
|
13423
|
-
* closed to the `tx_ready` path (which, when unsignable, errors at sign time).
|
|
13424
|
-
*
|
|
13425
|
-
* This sign gate is DELIBERATELY INDEPENDENT of the pairing heuristic below:
|
|
13426
|
-
* whether the two same-turn candidates are "the same tool call" affects only
|
|
13427
|
-
* divergence TELEMETRY, never the sign decision. So even if the pairing were
|
|
13428
|
-
* wrong (e.g. the backend batched two tool-output frames before a `tx_ready`
|
|
13429
|
-
* against the documented per-pending order — agent-backend agent.go:6397), a
|
|
13430
|
-
* diverging flat still cannot be signed: it is gated by `parityMatched`, not by
|
|
13431
|
-
* pairing. The fund-safety guarantee therefore rests on parity equality alone,
|
|
13432
|
-
* not on a cross-repo wire-ordering invariant (security + Codex convergence).
|
|
13433
|
-
*
|
|
13434
|
-
* Pairing (review — gomesalexandre) — TELEMETRY ONLY: the two channels are the
|
|
13435
|
-
* SAME tool call iff their tool-call ids match (`txReadyTwinCallId` is the id of
|
|
13436
|
-
* the tool-output frame this `tx_ready` was emitted next to — client.ts). A
|
|
13437
|
-
* DEFINITELY-unpaired pair (both ids present AND different) skips the parity
|
|
13438
|
-
* diff so an unrelated same-turn pair does not emit a false `[DIVERGENCE]`; a
|
|
13439
|
-
* missing id falls back to running the diff so a real divergence is never
|
|
13440
|
-
* suppressed. This gate never enqueues a sign source — it only silences noise.
|
|
13370
|
+
* FAIL-CLOSED, two structural gates:
|
|
13371
|
+
* 1. `deriveToolOutputCandidate` (client.ts) already rejected non-tx envelopes
|
|
13372
|
+
* (`no_op` / `insufficient_*` / errors) and phantom-card prep envelopes
|
|
13373
|
+
* (no `tx_encoding`) — so a candidate that reaches here is well-formed.
|
|
13374
|
+
* 2. `payloadLooksSignable` mirrors the executor's real signer requirements
|
|
13375
|
+
* (`extractNestedTx().to` for EVM; `txArgs.{to,amount}` for non-EVM; both
|
|
13376
|
+
* legs for multi-leg). A structurally-present-but-unsignable candidate is
|
|
13377
|
+
* NOT routed to the signer — the mutation-check target for this gate.
|
|
13378
|
+
* `storeServerTransaction` is the final backstop (returns false if it can't
|
|
13379
|
+
* resolve a tx body / cross-chain multi-leg), so a false is never a wrong sign.
|
|
13441
13380
|
*/
|
|
13442
|
-
selectAndBufferSignable(
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
if (txReadyCandidate && payloadLooksSignable(txReadyCandidate)) {
|
|
13451
|
-
ordered.push({ surface: "tx_ready", payload: txReadyCandidate });
|
|
13452
|
-
}
|
|
13453
|
-
const flatIsSignSource = toolOutputCandidate?.source === "flat" && (txReadyCandidate === null || parityMatched);
|
|
13454
|
-
if (toolOutputCandidate && flatIsSignSource) {
|
|
13455
|
-
ordered.push({ surface: "tool_output", payload: toolOutputCandidate.payload });
|
|
13456
|
-
}
|
|
13457
|
-
if (txReadyCandidate && !payloadLooksSignable(txReadyCandidate)) {
|
|
13458
|
-
ordered.push({ surface: "tx_ready", payload: txReadyCandidate });
|
|
13459
|
-
}
|
|
13460
|
-
for (const { surface, payload } of ordered) {
|
|
13461
|
-
if (this.executor.storeServerTransaction(payload)) {
|
|
13462
|
-
if (this.config.password) this.executor.setPassword(this.config.password);
|
|
13463
|
-
if (this.config.verbose) {
|
|
13464
|
-
const both = txReadyCandidate && toolOutputCandidate ? "both channels" : "single channel";
|
|
13465
|
-
process.stderr.write(`[session] buffered signable from ${surface} (${both})
|
|
13466
|
-
`);
|
|
13467
|
-
}
|
|
13468
|
-
if (surface === "tx_ready" && toolOutputCandidate?.source === "flat" && !parityMatched) {
|
|
13469
|
-
this.reportDeferredSignable(ui);
|
|
13470
|
-
}
|
|
13471
|
-
return true;
|
|
13381
|
+
selectAndBufferSignable(toolOutputCandidate) {
|
|
13382
|
+
if (!toolOutputCandidate) return false;
|
|
13383
|
+
if (!payloadLooksSignable(toolOutputCandidate.payload)) {
|
|
13384
|
+
if (this.config.verbose) {
|
|
13385
|
+
process.stderr.write(
|
|
13386
|
+
`[session] tool-output candidate (${toolOutputCandidate.toolName}, ${toolOutputCandidate.source}) not structurally signable; not buffering
|
|
13387
|
+
`
|
|
13388
|
+
);
|
|
13472
13389
|
}
|
|
13390
|
+
return false;
|
|
13473
13391
|
}
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
/**
|
|
13477
|
-
* Compare the client-enriched tool-output candidate to the backend `tx_ready`
|
|
13478
|
-
* for the same turn and log the result. On a safety-relevant divergence this
|
|
13479
|
-
* logs LOUDLY (always to stderr, not just under --verbose) — that divergence
|
|
13480
|
-
* signal is exactly what Phase 1 exists to surface before Phase 2 makes
|
|
13481
|
-
* tool-output the sole signing source.
|
|
13482
|
-
*/
|
|
13483
|
-
logToolOutputParity(toolOutput, txReady) {
|
|
13484
|
-
const result = diffToolOutputParity(toolOutput.payload, txReady);
|
|
13485
|
-
const exclusive = result.txReadyExclusive.length ? ` | tx_ready-exclusive: ${result.txReadyExclusive.join(",")}` : "";
|
|
13486
|
-
if (result.match) {
|
|
13392
|
+
if (this.executor.storeServerTransaction(toolOutputCandidate.payload)) {
|
|
13393
|
+
if (this.config.password) this.executor.setPassword(this.config.password);
|
|
13487
13394
|
if (this.config.verbose) {
|
|
13488
13395
|
process.stderr.write(
|
|
13489
|
-
`[
|
|
13396
|
+
`[session] buffered signable from tool-output (${toolOutputCandidate.toolName}, ${toolOutputCandidate.source})
|
|
13490
13397
|
`
|
|
13491
13398
|
);
|
|
13492
13399
|
}
|
|
13493
13400
|
return true;
|
|
13494
13401
|
}
|
|
13495
|
-
process.stderr.write(
|
|
13496
|
-
`[parity][DIVERGENCE] ${toolOutput.toolName} (${toolOutput.source}): tool-output != tx_ready \u2014 ${result.divergences.join("; ")}${exclusive}
|
|
13497
|
-
`
|
|
13498
|
-
);
|
|
13499
13402
|
return false;
|
|
13500
13403
|
}
|
|
13501
13404
|
/**
|
|
@@ -13503,15 +13406,14 @@ var AgentSession = class {
|
|
|
13503
13406
|
* assistant message. Polls /messages/since (server-clock anchored via
|
|
13504
13407
|
* X-Server-Now) until the persisted assistant message lands or the bounded
|
|
13505
13408
|
* budget is exhausted. On success it patches `streamResult.message` so the
|
|
13506
|
-
* normal downstream flow surfaces the answer, and replays any persisted
|
|
13507
|
-
*
|
|
13508
|
-
* the
|
|
13409
|
+
* normal downstream flow surfaces the answer, and replays any persisted balance
|
|
13410
|
+
* card. #927 Phase 2: a signable tx is NEVER recovered — the signable payload
|
|
13411
|
+
* rides tool-output-available, which the persisted parts don't reconstruct — so
|
|
13412
|
+
* a recovered turn that ran a signable tool warns to re-run instead of signing.
|
|
13509
13413
|
*/
|
|
13510
|
-
async recoverDisconnectedTurn(streamResult,
|
|
13414
|
+
async recoverDisconnectedTurn(streamResult, onBalanceSummary) {
|
|
13511
13415
|
if (!this.conversationId) return;
|
|
13512
|
-
const
|
|
13513
|
-
const since = serverAnchor ?? new Date(Date.now() - 2e3).toISOString();
|
|
13514
|
-
const replaySignableCards = serverAnchor !== null;
|
|
13416
|
+
const since = serverNowToIso(streamResult.serverNow) ?? new Date(Date.now() - 2e3).toISOString();
|
|
13515
13417
|
let cursor;
|
|
13516
13418
|
let authRecovered = false;
|
|
13517
13419
|
for (let attempt = 0; attempt < this.recoveryMaxPolls; attempt++) {
|
|
@@ -13532,13 +13434,13 @@ var AgentSession = class {
|
|
|
13532
13434
|
continue;
|
|
13533
13435
|
}
|
|
13534
13436
|
if (resp.cursor) cursor = resp.cursor;
|
|
13535
|
-
const assistant = [...resp.messages].reverse().find((m) => m.role === "assistant" && (!!m.content ||
|
|
13437
|
+
const assistant = [...resp.messages].reverse().find((m) => m.role === "assistant" && (!!m.content || hasSignableToolPart(m.parts)));
|
|
13536
13438
|
if (assistant) {
|
|
13537
13439
|
if (this.config.verbose) {
|
|
13538
13440
|
process.stderr.write(`[session] recovered assistant message after ${attempt + 1} poll(s)
|
|
13539
13441
|
`);
|
|
13540
13442
|
}
|
|
13541
|
-
this.applyRecoveredMessage(assistant, streamResult,
|
|
13443
|
+
this.applyRecoveredMessage(assistant, streamResult, onBalanceSummary);
|
|
13542
13444
|
return;
|
|
13543
13445
|
}
|
|
13544
13446
|
await this.recoverySleep();
|
|
@@ -13626,38 +13528,30 @@ var AgentSession = class {
|
|
|
13626
13528
|
}
|
|
13627
13529
|
/**
|
|
13628
13530
|
* Fold a recovered assistant message back into the live stream result: the
|
|
13629
|
-
* authoritative message wins over any partial deltas, and any persisted
|
|
13630
|
-
*
|
|
13631
|
-
*
|
|
13632
|
-
*
|
|
13633
|
-
*
|
|
13634
|
-
* recovery anchor was the local-clock fallback (no X-Server-Now), where a
|
|
13635
|
-
* recovered card cannot be proven to belong to the current turn. See
|
|
13636
|
-
* recoverDisconnectedTurn — a stale tx_ready must never reach the signer.
|
|
13531
|
+
* authoritative message wins over any partial deltas, and any persisted balance
|
|
13532
|
+
* card is replayed. #927 Phase 2: a signable transaction is NEVER recovered —
|
|
13533
|
+
* the signable payload rides tool-output-available, which the persisted parts
|
|
13534
|
+
* do not reconstruct — so a recovered turn that ran a signable tool warns the
|
|
13535
|
+
* user to re-run rather than signing (fail-CLOSED, never a wrong sign).
|
|
13637
13536
|
*/
|
|
13638
|
-
applyRecoveredMessage(msg, streamResult,
|
|
13537
|
+
applyRecoveredMessage(msg, streamResult, onBalanceSummary) {
|
|
13639
13538
|
streamResult.message = msg;
|
|
13640
|
-
let
|
|
13641
|
-
let signableFlatToolPart;
|
|
13539
|
+
let signableToolPart;
|
|
13642
13540
|
for (const part of msg.parts ?? []) {
|
|
13643
13541
|
if (part.type === "data-balance_summary" && part.data) {
|
|
13644
13542
|
onBalanceSummary?.(part.data);
|
|
13645
13543
|
continue;
|
|
13646
13544
|
}
|
|
13647
|
-
if (replaySignableCards && part.type === "data-tx_ready" && part.data && typeof part.data === "object") {
|
|
13648
|
-
const tx = part.data;
|
|
13649
|
-
streamResult.transactions.push(tx);
|
|
13650
|
-
onTxReady?.(tx);
|
|
13651
|
-
replayedTxReady = true;
|
|
13652
|
-
}
|
|
13653
13545
|
if (typeof part.type === "string" && part.type.startsWith("tool-")) {
|
|
13654
13546
|
const toolName = part.type.slice("tool-".length);
|
|
13655
|
-
if (CLI_SIGNABLE_FLAT_TOOLS.has(toolName)
|
|
13547
|
+
if (CLI_SIGNABLE_FLAT_TOOLS.has(toolName) || CLI_SIGNABLE_PREP_TOOLS.has(toolName)) {
|
|
13548
|
+
signableToolPart = toolName;
|
|
13549
|
+
}
|
|
13656
13550
|
}
|
|
13657
13551
|
}
|
|
13658
|
-
if (
|
|
13552
|
+
if (signableToolPart) {
|
|
13659
13553
|
process.stderr.write(
|
|
13660
|
-
`[session][recovery] recovered turn ran signable tool '${
|
|
13554
|
+
`[session][recovery] recovered turn ran signable tool '${signableToolPart}', whose signable output rides tool-output-available. The recovery path does not reconstruct that candidate, so NO transaction was signed this turn \u2014 re-run the request to sign.
|
|
13661
13555
|
`
|
|
13662
13556
|
);
|
|
13663
13557
|
}
|
|
@@ -13825,8 +13719,12 @@ function serverNowToIso(serverNow) {
|
|
|
13825
13719
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
13826
13720
|
return new Date(ms).toISOString();
|
|
13827
13721
|
}
|
|
13828
|
-
function
|
|
13829
|
-
return !!parts?.some((p) =>
|
|
13722
|
+
function hasSignableToolPart(parts) {
|
|
13723
|
+
return !!parts?.some((p) => {
|
|
13724
|
+
if (typeof p.type !== "string" || !p.type.startsWith("tool-")) return false;
|
|
13725
|
+
const toolName = p.type.slice("tool-".length);
|
|
13726
|
+
return CLI_SIGNABLE_FLAT_TOOLS.has(toolName) || CLI_SIGNABLE_PREP_TOOLS.has(toolName);
|
|
13727
|
+
});
|
|
13830
13728
|
}
|
|
13831
13729
|
function isAuthError(err) {
|
|
13832
13730
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
@@ -14552,7 +14450,7 @@ var cachedVersion = null;
|
|
|
14552
14450
|
function getVersion() {
|
|
14553
14451
|
if (cachedVersion) return cachedVersion;
|
|
14554
14452
|
if (true) {
|
|
14555
|
-
cachedVersion = "2.18.
|
|
14453
|
+
cachedVersion = "2.18.6";
|
|
14556
14454
|
return cachedVersion;
|
|
14557
14455
|
}
|
|
14558
14456
|
try {
|
|
@@ -16944,19 +16842,27 @@ program.command("broadcast").description("Broadcast a pre-signed raw transaction
|
|
|
16944
16842
|
});
|
|
16945
16843
|
})
|
|
16946
16844
|
);
|
|
16947
|
-
program.command("tx-status").description("Check the status of a transaction (polls until confirmed)").requiredOption("--chain <chain>", "Target blockchain").requiredOption("--tx-hash <hash>", "Transaction hash to check").option("--no-wait", "Return immediately without waiting for confirmation").addHelpText(
|
|
16845
|
+
program.command("tx-status").description("Check the status of a transaction (polls until confirmed)").requiredOption("--chain <chain>", "Target blockchain").requiredOption("--tx-hash <hash>", "Transaction hash to check").option("--no-wait", "Return immediately without waiting for confirmation").option("--timeout <seconds>", "Max seconds to poll before giving up (default 120)").addHelpText(
|
|
16948
16846
|
"after",
|
|
16949
16847
|
`
|
|
16950
16848
|
Examples:
|
|
16951
16849
|
vultisig tx-status --chain Ethereum --tx-hash 0xabc...
|
|
16850
|
+
vultisig tx-status --chain Ethereum --tx-hash 0xabc... --timeout 300
|
|
16952
16851
|
vultisig tx-status --chain Bitcoin --tx-hash abc... --no-wait --output json`
|
|
16953
16852
|
).action(
|
|
16954
16853
|
withExit(async (options) => {
|
|
16955
16854
|
const context = await init(program.opts().vault);
|
|
16855
|
+
const timeoutSec = options.timeout !== void 0 ? Number(options.timeout) : void 0;
|
|
16856
|
+
if (timeoutSec !== void 0 && (!Number.isFinite(timeoutSec) || timeoutSec < 0)) {
|
|
16857
|
+
throw new InvalidInputError(
|
|
16858
|
+
`Invalid --timeout: "${options.timeout}" (expected a non-negative number of seconds)`
|
|
16859
|
+
);
|
|
16860
|
+
}
|
|
16956
16861
|
await executeTxStatus(context, {
|
|
16957
16862
|
chain: findChainByName(options.chain) || options.chain,
|
|
16958
16863
|
txHash: options.txHash,
|
|
16959
|
-
noWait: !options.wait
|
|
16864
|
+
noWait: !options.wait,
|
|
16865
|
+
timeoutSec
|
|
16960
16866
|
});
|
|
16961
16867
|
})
|
|
16962
16868
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vultisig/cli",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.6",
|
|
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": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"@napi-rs/keyring": "^1.3.0",
|
|
75
75
|
"@noble/hashes": "^2.2.0",
|
|
76
76
|
"@vultisig/client-shared": "^0.2.17",
|
|
77
|
-
"@vultisig/core-chain": "^2.23.
|
|
77
|
+
"@vultisig/core-chain": "^2.23.3",
|
|
78
78
|
"@vultisig/rujira": "^51.0.0",
|
|
79
|
-
"@vultisig/sdk": "^2.18.
|
|
79
|
+
"@vultisig/sdk": "^2.18.6",
|
|
80
80
|
"chalk": "^5.6.2",
|
|
81
81
|
"cli-table3": "^0.6.5",
|
|
82
82
|
"commander": "^15.0.0",
|