@vultisig/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +895 -190
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1432,7 +1432,7 @@ var init_sha3 = __esm({
1432
1432
 
1433
1433
  // src/index.ts
1434
1434
  import "dotenv/config";
1435
- import { promises as fs3 } from "node:fs";
1435
+ import { promises as fs4 } from "node:fs";
1436
1436
  import { parseKeygenQR, Vultisig as Vultisig7 } from "@vultisig/sdk";
1437
1437
  import chalk15 from "chalk";
1438
1438
  import { program } from "commander";
@@ -1517,6 +1517,9 @@ import chalk from "chalk";
1517
1517
  import ora from "ora";
1518
1518
  var silentMode = false;
1519
1519
  var outputFormat = "table";
1520
+ function setSilentMode(silent) {
1521
+ silentMode = silent;
1522
+ }
1520
1523
  function isSilent() {
1521
1524
  return silentMode;
1522
1525
  }
@@ -1852,6 +1855,11 @@ function displayVaultInfo(vault) {
1852
1855
  printResult(chalk2.bold("\nPublic Keys:"));
1853
1856
  printResult(` ECDSA: ${vault.publicKeys.ecdsa.substring(0, 20)}...`);
1854
1857
  printResult(` EdDSA: ${vault.publicKeys.eddsa.substring(0, 20)}...`);
1858
+ if (vault.publicKeyMldsa) {
1859
+ printResult(` ML-DSA-44: ${vault.publicKeyMldsa.substring(0, 20)}...`);
1860
+ } else {
1861
+ printResult(` ML-DSA-44: ${chalk2.gray("Not available")}`);
1862
+ }
1855
1863
  printResult(` Chain Code: ${vault.hexChainCode.substring(0, 20)}...
1856
1864
  `);
1857
1865
  }
@@ -2480,8 +2488,9 @@ Or use this URL: ${qrPayload}
2480
2488
  });
2481
2489
  }
2482
2490
  try {
2491
+ const cosmosChain = params.chain;
2483
2492
  const coin = {
2484
- chain: params.chain,
2493
+ chain: cosmosChain,
2485
2494
  address,
2486
2495
  decimals: 8,
2487
2496
  // THORChain uses 8 decimals
@@ -2501,7 +2510,7 @@ Or use this URL: ${qrPayload}
2501
2510
  gas: chainConfig.gasLimit
2502
2511
  };
2503
2512
  const keysignPayload = await vault.prepareSignAminoTx({
2504
- chain: params.chain,
2513
+ chain: cosmosChain,
2505
2514
  coin,
2506
2515
  msgs: [executeContractMsg],
2507
2516
  fee,
@@ -2511,7 +2520,7 @@ Or use this URL: ${qrPayload}
2511
2520
  const signature = await vault.sign(
2512
2521
  {
2513
2522
  transaction: keysignPayload,
2514
- chain: params.chain,
2523
+ chain: cosmosChain,
2515
2524
  messageHashes
2516
2525
  },
2517
2526
  { signal: params.signal }
@@ -2519,7 +2528,7 @@ Or use this URL: ${qrPayload}
2519
2528
  signSpinner.succeed("Transaction signed");
2520
2529
  const broadcastSpinner = createSpinner("Broadcasting transaction...");
2521
2530
  const txHash = await vault.broadcastTx({
2522
- chain: params.chain,
2531
+ chain: cosmosChain,
2523
2532
  keysignPayload,
2524
2533
  signature
2525
2534
  });
@@ -2603,7 +2612,8 @@ Or use this URL: ${qrPayload}
2603
2612
  const result = {
2604
2613
  signature: sigBase64,
2605
2614
  recovery: signature.recovery,
2606
- format: signature.format
2615
+ format: signature.format,
2616
+ mldsaSignature: signature.mldsaSignature
2607
2617
  };
2608
2618
  if (isJsonOutput()) {
2609
2619
  outputJson(result);
@@ -2613,6 +2623,9 @@ Or use this URL: ${qrPayload}
2613
2623
  printResult(`Recovery: ${result.recovery}`);
2614
2624
  }
2615
2625
  printResult(`Format: ${result.format}`);
2626
+ if (result.mldsaSignature) {
2627
+ printResult(`ML-DSA-44 Signature: ${result.mldsaSignature.substring(0, 40)}...`);
2628
+ }
2616
2629
  }
2617
2630
  return result;
2618
2631
  } finally {
@@ -2733,7 +2746,11 @@ function withAbortSignal(promise, signal) {
2733
2746
  ]);
2734
2747
  }
2735
2748
  async function executeCreateFast(ctx2, options) {
2736
- const { name, password, email, signal, twoStep } = options;
2749
+ const twoStep = options.twoStep || !process.stdin.isTTY;
2750
+ const { name, password, email, signal } = options;
2751
+ if (!options.twoStep && twoStep) {
2752
+ info("Non-interactive terminal detected. Using --two-step mode automatically.");
2753
+ }
2737
2754
  const spinner = createSpinner("Creating vault...");
2738
2755
  const vaultId = await withAbortSignal(
2739
2756
  ctx2.sdk.createFastVault({
@@ -2749,6 +2766,16 @@ async function executeCreateFast(ctx2, options) {
2749
2766
  );
2750
2767
  spinner.succeed(`Vault keys generated: ${name}`);
2751
2768
  if (twoStep) {
2769
+ if (isJsonOutput()) {
2770
+ outputJson({
2771
+ vaultId,
2772
+ status: "pending_verification",
2773
+ message: "Vault created. Verify with email OTP to activate.",
2774
+ verifyCommand: `vultisig verify ${vaultId} --code <OTP>`,
2775
+ resendCommand: `vultisig verify ${vaultId} --resend --email ${email} --password <password>`
2776
+ });
2777
+ return void 0;
2778
+ }
2752
2779
  success("\n+ Vault created and saved to disk (pending verification)");
2753
2780
  info(`
2754
2781
  Vault ID: ${vaultId}`);
@@ -2897,15 +2924,19 @@ Important: Save your vault backup file (.vult) in a secure location.`);
2897
2924
  throw err;
2898
2925
  }
2899
2926
  }
2900
- async function executeImport(ctx2, file) {
2901
- const { password } = await inquirer4.prompt([
2902
- {
2903
- type: "password",
2904
- name: "password",
2905
- message: "Enter vault password (if encrypted):",
2906
- mask: "*"
2907
- }
2908
- ]);
2927
+ async function executeImport(ctx2, file, flagPassword) {
2928
+ let password = flagPassword || process.env.VAULT_PASSWORD || "";
2929
+ if (!password) {
2930
+ const answers = await inquirer4.prompt([
2931
+ {
2932
+ type: "password",
2933
+ name: "password",
2934
+ message: "Enter vault password (if encrypted):",
2935
+ mask: "*"
2936
+ }
2937
+ ]);
2938
+ password = answers.password;
2939
+ }
2909
2940
  const spinner = createSpinner("Importing vault...");
2910
2941
  const vultContent = await fs.readFile(file, "utf-8");
2911
2942
  const vault = await ctx2.sdk.importVault(vultContent, password || void 0);
@@ -2973,11 +3004,25 @@ async function executeVerify(ctx2, vaultId, options = {}) {
2973
3004
  spinner.succeed("Vault verified successfully!");
2974
3005
  setupVaultEvents(vault);
2975
3006
  await ctx2.setActiveVault(vault);
3007
+ if (isJsonOutput()) {
3008
+ outputJson({
3009
+ verified: true,
3010
+ vault: { id: vaultId, name: vault.name, type: "fast" }
3011
+ });
3012
+ return true;
3013
+ }
2976
3014
  success(`
2977
3015
  + Vault "${vault.name}" is now ready to use!`);
2978
3016
  return true;
2979
3017
  } catch (err) {
2980
3018
  spinner.fail("Verification failed");
3019
+ if (isJsonOutput()) {
3020
+ outputJson({
3021
+ verified: false,
3022
+ error: err.message || "Verification failed. Please check the code and try again."
3023
+ });
3024
+ return false;
3025
+ }
2981
3026
  error(`
2982
3027
  \u2717 ${err.message || "Verification failed. Please check the code and try again."}`);
2983
3028
  warn("\nTip: Use --resend to get a new verification code:");
@@ -3058,10 +3103,28 @@ async function executeVaults(ctx2) {
3058
3103
  }
3059
3104
  async function executeSwitch(ctx2, vaultId) {
3060
3105
  const spinner = createSpinner("Loading vault...");
3061
- const vault = await ctx2.sdk.getVaultById(vaultId);
3106
+ let vault = await ctx2.sdk.getVaultById(vaultId);
3107
+ if (!vault) {
3108
+ const allVaults = await ctx2.sdk.listVaults();
3109
+ const byName = allVaults.filter((v) => v.name.toLowerCase() === vaultId.toLowerCase());
3110
+ if (byName.length === 1) {
3111
+ vault = await ctx2.sdk.getVaultById(byName[0].id);
3112
+ } else if (byName.length > 1) {
3113
+ spinner.fail("Ambiguous vault name");
3114
+ throw new Error(`Multiple vaults match name "${vaultId}". Use the full vault ID instead.`);
3115
+ } else {
3116
+ const byPrefix = allVaults.filter((v) => v.id.startsWith(vaultId));
3117
+ if (byPrefix.length === 1) {
3118
+ vault = await ctx2.sdk.getVaultById(byPrefix[0].id);
3119
+ } else if (byPrefix.length > 1) {
3120
+ spinner.fail("Ambiguous vault ID prefix");
3121
+ throw new Error(`Multiple vaults match prefix "${vaultId}". Use a longer prefix or the full ID.`);
3122
+ }
3123
+ }
3124
+ }
3062
3125
  if (!vault) {
3063
3126
  spinner.fail("Vault not found");
3064
- throw new Error(`No vault found with ID: ${vaultId}`);
3127
+ throw new Error(`No vault found matching: ${vaultId}`);
3065
3128
  }
3066
3129
  await ctx2.setActiveVault(vault);
3067
3130
  setupVaultEvents(vault);
@@ -3103,6 +3166,7 @@ async function executeInfo(ctx2) {
3103
3166
  publicKeys: {
3104
3167
  ecdsa: vault.publicKeys.ecdsa,
3105
3168
  eddsa: vault.publicKeys.eddsa,
3169
+ ...vault.publicKeyMldsa ? { mldsa: vault.publicKeyMldsa } : {},
3106
3170
  chainCode: vault.hexChainCode
3107
3171
  }
3108
3172
  }
@@ -4101,6 +4165,89 @@ function displayDiscountTier(tierInfo) {
4101
4165
  import chalk9 from "chalk";
4102
4166
  import Table from "cli-table3";
4103
4167
 
4168
+ // src/agent/ask.ts
4169
+ var AskInterface = class {
4170
+ session;
4171
+ verbose;
4172
+ responseParts = [];
4173
+ toolCalls = [];
4174
+ transactions = [];
4175
+ constructor(session, verbose = false) {
4176
+ this.session = session;
4177
+ this.verbose = verbose;
4178
+ }
4179
+ /**
4180
+ * Get UI callbacks that silently collect results.
4181
+ * Tool progress is logged to stderr in verbose mode.
4182
+ */
4183
+ getCallbacks() {
4184
+ return {
4185
+ onTextDelta: (_delta) => {
4186
+ },
4187
+ onToolCall: (_id, action, params) => {
4188
+ if (this.verbose) {
4189
+ const paramStr = params ? ` ${JSON.stringify(params)}` : "";
4190
+ process.stderr.write(`[tool] ${action}${paramStr} ...
4191
+ `);
4192
+ }
4193
+ },
4194
+ onToolResult: (_id, action, success2, data, error2) => {
4195
+ this.toolCalls.push({ action, success: success2, data, error: error2 });
4196
+ if (this.verbose) {
4197
+ const status = success2 ? "ok" : `error: ${error2}`;
4198
+ process.stderr.write(`[tool] ${action}: ${status}
4199
+ `);
4200
+ }
4201
+ },
4202
+ onAssistantMessage: (content) => {
4203
+ if (content) {
4204
+ this.responseParts.push(content);
4205
+ }
4206
+ },
4207
+ onSuggestions: (_suggestions) => {
4208
+ },
4209
+ onTxStatus: (txHash, chain, _status, explorerUrl) => {
4210
+ this.transactions.push({ hash: txHash, chain, explorerUrl });
4211
+ if (this.verbose) {
4212
+ process.stderr.write(`[tx] ${chain}: ${txHash}
4213
+ `);
4214
+ }
4215
+ },
4216
+ onError: (message) => {
4217
+ process.stderr.write(`[error] ${message}
4218
+ `);
4219
+ },
4220
+ onDone: () => {
4221
+ },
4222
+ requestPassword: async () => {
4223
+ throw new Error(
4224
+ "Password required but not provided. Use --password flag."
4225
+ );
4226
+ },
4227
+ requestConfirmation: async (_message) => {
4228
+ return true;
4229
+ }
4230
+ };
4231
+ }
4232
+ /**
4233
+ * Send a message and wait for the complete response.
4234
+ * All tool calls and actions are executed automatically.
4235
+ */
4236
+ async ask(message) {
4237
+ this.responseParts = [];
4238
+ this.toolCalls = [];
4239
+ this.transactions = [];
4240
+ const callbacks = this.getCallbacks();
4241
+ await this.session.sendMessage(message, callbacks);
4242
+ return {
4243
+ sessionId: this.session.getConversationId() || "",
4244
+ response: this.responseParts[this.responseParts.length - 1] || "",
4245
+ toolCalls: this.toolCalls,
4246
+ transactions: this.transactions
4247
+ };
4248
+ }
4249
+ };
4250
+
4104
4251
  // src/agent/auth.ts
4105
4252
  init_sha3();
4106
4253
  import { randomBytes } from "node:crypto";
@@ -4376,8 +4523,8 @@ var AgentClient = class {
4376
4523
  // ============================================================================
4377
4524
  // Private helpers
4378
4525
  // ============================================================================
4379
- async post(path3, body) {
4380
- const res = await fetch(`${this.baseUrl}${path3}`, {
4526
+ async post(path4, body) {
4527
+ const res = await fetch(`${this.baseUrl}${path4}`, {
4381
4528
  method: "POST",
4382
4529
  headers: {
4383
4530
  "Content-Type": "application/json",
@@ -4391,8 +4538,8 @@ var AgentClient = class {
4391
4538
  }
4392
4539
  return await res.json();
4393
4540
  }
4394
- async delete(path3, body) {
4395
- const res = await fetch(`${this.baseUrl}${path3}`, {
4541
+ async delete(path4, body) {
4542
+ const res = await fetch(`${this.baseUrl}${path4}`, {
4396
4543
  method: "DELETE",
4397
4544
  headers: {
4398
4545
  "Content-Type": "application/json",
@@ -4412,7 +4559,8 @@ import { Chain as Chain8 } from "@vultisig/sdk";
4412
4559
  async function buildMessageContext(vault) {
4413
4560
  const context = {
4414
4561
  vault_address: vault.publicKeys.ecdsa,
4415
- vault_name: vault.name
4562
+ vault_name: vault.name,
4563
+ mldsa_public_key: vault.publicKeyMldsa
4416
4564
  };
4417
4565
  try {
4418
4566
  const chains = vault.chains;
@@ -4472,6 +4620,30 @@ async function buildMessageContext(vault) {
4472
4620
  }
4473
4621
  return context;
4474
4622
  }
4623
+ async function buildMinimalContext(vault) {
4624
+ const context = {
4625
+ vault_address: vault.publicKeys.ecdsa,
4626
+ vault_name: vault.name
4627
+ };
4628
+ try {
4629
+ const chains = vault.chains;
4630
+ const addressEntries = await Promise.allSettled(
4631
+ chains.map(async (chain) => ({
4632
+ chain: chain.toString(),
4633
+ address: await vault.address(chain)
4634
+ }))
4635
+ );
4636
+ const addresses = {};
4637
+ for (const result of addressEntries) {
4638
+ if (result.status === "fulfilled") {
4639
+ addresses[result.value.chain] = result.value.address;
4640
+ }
4641
+ }
4642
+ context.addresses = addresses;
4643
+ } catch {
4644
+ }
4645
+ return context;
4646
+ }
4475
4647
  function getNativeTokenTicker(chain) {
4476
4648
  const tickers = {
4477
4649
  [Chain8.Ethereum]: "ETH",
@@ -4532,6 +4704,161 @@ function getNativeTokenDecimals(chain) {
4532
4704
  // src/agent/executor.ts
4533
4705
  import { Chain as Chain9, Vultisig as Vultisig6 } from "@vultisig/sdk";
4534
4706
 
4707
+ // src/core/VaultStateStore.ts
4708
+ import * as fs2 from "node:fs";
4709
+ import * as os from "node:os";
4710
+ import * as path2 from "node:path";
4711
+ var LOCK_STALE_MS = 6e4;
4712
+ var LOCK_RETRY_INIT_MS = 100;
4713
+ var LOCK_MAX_WAIT_MS = 3e4;
4714
+ var STATE_TTL_MS = 10 * 6e4;
4715
+ var VaultStateStore = class {
4716
+ baseDir;
4717
+ constructor(vaultId) {
4718
+ const safeId = vaultId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 40);
4719
+ if (!safeId) {
4720
+ throw new Error("Invalid vaultId: must contain alphanumeric characters");
4721
+ }
4722
+ this.baseDir = path2.join(os.homedir(), ".vultisig", "vault-state", safeId);
4723
+ fs2.mkdirSync(this.baseDir, { recursive: true });
4724
+ }
4725
+ // -------------------------------------------------------------------------
4726
+ // Chain-level locking
4727
+ // -------------------------------------------------------------------------
4728
+ /**
4729
+ * Acquire an exclusive file lock for the given chain.
4730
+ * Blocks (with exponential backoff) until the lock is available or timeout.
4731
+ *
4732
+ * @returns A release function — caller MUST call it when done.
4733
+ */
4734
+ async acquireChainLock(chain) {
4735
+ const lockPath = path2.join(this.baseDir, `${chain}.lock`);
4736
+ const startTime = Date.now();
4737
+ let delay = LOCK_RETRY_INIT_MS;
4738
+ while (true) {
4739
+ try {
4740
+ const fd = fs2.openSync(lockPath, "wx");
4741
+ const lockToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4742
+ const info2 = { pid: process.pid, timestamp: Date.now(), token: lockToken };
4743
+ fs2.writeSync(fd, JSON.stringify(info2));
4744
+ fs2.closeSync(fd);
4745
+ return async () => {
4746
+ try {
4747
+ const content = fs2.readFileSync(lockPath, "utf8");
4748
+ const current = JSON.parse(content);
4749
+ if (current.token === lockToken) {
4750
+ fs2.unlinkSync(lockPath);
4751
+ }
4752
+ } catch {
4753
+ }
4754
+ };
4755
+ } catch (err) {
4756
+ if (err.code !== "EEXIST") throw err;
4757
+ if (this.tryCleanStaleLock(lockPath)) {
4758
+ continue;
4759
+ }
4760
+ if (Date.now() - startTime > LOCK_MAX_WAIT_MS) {
4761
+ throw new Error(
4762
+ `Timeout after ${LOCK_MAX_WAIT_MS}ms waiting for ${chain} chain lock. Another process may be stuck. Lock file: ${lockPath}`
4763
+ );
4764
+ }
4765
+ await sleep2(delay);
4766
+ delay = Math.min(delay * 1.5, 2e3);
4767
+ }
4768
+ }
4769
+ }
4770
+ // -------------------------------------------------------------------------
4771
+ // EVM nonce management
4772
+ // -------------------------------------------------------------------------
4773
+ /**
4774
+ * Get the next nonce to use for an EVM chain.
4775
+ *
4776
+ * Takes the on-chain nonce (from `getTransactionCount`) and returns
4777
+ * `max(onChainNonce, localLastUsed + 1)`. This ensures that:
4778
+ * - Locally queued txs get incrementing nonces
4779
+ * - External txs (MetaMask, other wallets) are respected
4780
+ *
4781
+ * MUST be called while holding the chain lock.
4782
+ */
4783
+ getNextEvmNonce(chain, onChainNonce) {
4784
+ const state = this.readEvmState(chain);
4785
+ if (!state) return onChainNonce;
4786
+ if (Date.now() - state.updatedAt > STATE_TTL_MS) return onChainNonce;
4787
+ const localNext = BigInt(state.lastUsedNonce) + 1n;
4788
+ return localNext > onChainNonce ? localNext : onChainNonce;
4789
+ }
4790
+ /**
4791
+ * Clear persisted nonce state for a chain (e.g. when pending txs were evicted).
4792
+ */
4793
+ clearEvmState(chain) {
4794
+ const filePath = path2.join(this.baseDir, `${chain}.state.json`);
4795
+ try {
4796
+ fs2.unlinkSync(filePath);
4797
+ } catch {
4798
+ }
4799
+ }
4800
+ /**
4801
+ * Record that we broadcast a tx using the given nonce.
4802
+ * For approve+swap flows, pass the HIGHEST nonce used.
4803
+ *
4804
+ * MUST be called while holding the chain lock (before releasing).
4805
+ */
4806
+ recordEvmNonce(chain, nonce) {
4807
+ const state = {
4808
+ lastUsedNonce: nonce.toString(),
4809
+ updatedAt: Date.now()
4810
+ };
4811
+ this.writeEvmState(chain, state);
4812
+ }
4813
+ // -------------------------------------------------------------------------
4814
+ // Internal helpers
4815
+ // -------------------------------------------------------------------------
4816
+ readEvmState(chain) {
4817
+ const filePath = path2.join(this.baseDir, `${chain}.state.json`);
4818
+ try {
4819
+ const content = fs2.readFileSync(filePath, "utf8");
4820
+ return JSON.parse(content);
4821
+ } catch {
4822
+ return null;
4823
+ }
4824
+ }
4825
+ writeEvmState(chain, state) {
4826
+ const filePath = path2.join(this.baseDir, `${chain}.state.json`);
4827
+ const tmpPath = filePath + `.tmp.${process.pid}`;
4828
+ fs2.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
4829
+ fs2.renameSync(tmpPath, filePath);
4830
+ }
4831
+ /**
4832
+ * Check if a lock file is stale and remove it if so.
4833
+ * @returns true if a stale lock was removed.
4834
+ */
4835
+ tryCleanStaleLock(lockPath) {
4836
+ try {
4837
+ const content = fs2.readFileSync(lockPath, "utf8");
4838
+ const info2 = JSON.parse(content);
4839
+ if (Date.now() - info2.timestamp > LOCK_STALE_MS) {
4840
+ fs2.unlinkSync(lockPath);
4841
+ return true;
4842
+ }
4843
+ } catch {
4844
+ try {
4845
+ const stat = fs2.statSync(lockPath);
4846
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
4847
+ fs2.unlinkSync(lockPath);
4848
+ return true;
4849
+ }
4850
+ } catch {
4851
+ return true;
4852
+ }
4853
+ return false;
4854
+ }
4855
+ return false;
4856
+ }
4857
+ };
4858
+ function sleep2(ms) {
4859
+ return new Promise((resolve) => setTimeout(resolve, ms));
4860
+ }
4861
+
4535
4862
  // src/agent/types.ts
4536
4863
  var AUTO_EXECUTE_ACTIONS = /* @__PURE__ */ new Set([
4537
4864
  "add_chain",
@@ -4559,14 +4886,50 @@ var AUTO_EXECUTE_ACTIONS = /* @__PURE__ */ new Set([
4559
4886
  var PASSWORD_REQUIRED_ACTIONS = /* @__PURE__ */ new Set(["sign_tx", "sign_typed_data", "build_custom_tx"]);
4560
4887
 
4561
4888
  // src/agent/executor.ts
4889
+ var EVM_CHAINS = /* @__PURE__ */ new Set([
4890
+ "Ethereum",
4891
+ "BSC",
4892
+ "Polygon",
4893
+ "Avalanche",
4894
+ "Arbitrum",
4895
+ "Optimism",
4896
+ "Base",
4897
+ "Blast",
4898
+ "Zksync",
4899
+ "Mantle",
4900
+ "CronosChain",
4901
+ "Hyperliquid",
4902
+ "Sei"
4903
+ ]);
4904
+ var EVM_GAS_RPC = {
4905
+ Ethereum: "https://eth.llamarpc.com",
4906
+ BSC: "https://bsc-dataseed.binance.org",
4907
+ Polygon: "https://polygon-rpc.com",
4908
+ Avalanche: "https://api.avax.network/ext/bc/C/rpc",
4909
+ Arbitrum: "https://arb1.arbitrum.io/rpc",
4910
+ Optimism: "https://mainnet.optimism.io",
4911
+ Base: "https://mainnet.base.org",
4912
+ Blast: "https://rpc.blast.io",
4913
+ Zksync: "https://mainnet.era.zksync.io",
4914
+ Mantle: "https://rpc.mantle.xyz",
4915
+ CronosChain: "https://cronos-evm-rpc.publicnode.com",
4916
+ Hyperliquid: "https://rpc.hyperliquid.xyz/evm",
4917
+ Sei: "https://evm-rpc.sei-apis.com"
4918
+ };
4562
4919
  var AgentExecutor = class {
4563
4920
  vault;
4564
4921
  pendingPayloads = /* @__PURE__ */ new Map();
4565
4922
  password = null;
4566
4923
  verbose;
4567
- constructor(vault, verbose = false) {
4924
+ stateStore = null;
4925
+ /** Held chain lock release functions, keyed by chain name */
4926
+ chainLockReleases = /* @__PURE__ */ new Map();
4927
+ constructor(vault, verbose = false, vaultId) {
4568
4928
  this.vault = vault;
4569
4929
  this.verbose = verbose;
4930
+ if (vaultId) {
4931
+ this.stateStore = new VaultStateStore(vaultId);
4932
+ }
4570
4933
  }
4571
4934
  setPassword(password) {
4572
4935
  this.password = password;
@@ -4791,40 +5154,47 @@ var AgentExecutor = class {
4791
5154
  const amountStr = params.amount;
4792
5155
  if (!toAddress) throw new Error("Destination address is required");
4793
5156
  if (!amountStr) throw new Error("Amount is required");
4794
- const address = await this.vault.address(chain);
4795
- const balance = await this.vault.balance(chain, params.token_id);
4796
- const coin = {
4797
- chain,
4798
- address,
4799
- decimals: balance.decimals,
4800
- ticker: symbol || balance.symbol,
4801
- id: params.token_id
4802
- };
4803
- const amount = parseAmount(amountStr, balance.decimals);
4804
- const memo = params.memo;
4805
- const payload = await this.vault.prepareSendTx({ coin, receiver: toAddress, amount, memo });
4806
- this.pendingPayloads.clear();
4807
- const payloadId = `tx_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4808
- this.pendingPayloads.set(payloadId, { payload, coin, chain, timestamp: Date.now() });
4809
- this.pendingPayloads.set("latest", { payload, coin, chain, timestamp: Date.now() });
4810
- const messageHashes = await this.vault.extractMessageHashes(payload);
4811
- return {
4812
- keysign_payload: payloadId,
4813
- from_chain: chain.toString(),
4814
- from_symbol: coin.ticker,
4815
- amount: amountStr,
4816
- sender: address,
4817
- destination: toAddress,
4818
- memo: memo || void 0,
4819
- message_hashes: messageHashes,
4820
- tx_details: {
4821
- chain: chain.toString(),
4822
- from: address,
4823
- to: toAddress,
5157
+ await this.acquireEvmLockIfNeeded(chain);
5158
+ try {
5159
+ const address = await this.vault.address(chain);
5160
+ const balance = await this.vault.balance(chain, params.token_id);
5161
+ const coin = {
5162
+ chain,
5163
+ address,
5164
+ decimals: balance.decimals,
5165
+ ticker: symbol || balance.symbol,
5166
+ id: params.token_id
5167
+ };
5168
+ const amount = parseAmount(amountStr, balance.decimals);
5169
+ const memo = params.memo;
5170
+ const payload = await this.vault.prepareSendTx({ coin, receiver: toAddress, amount, memo });
5171
+ await this.patchEvmNonce(chain, payload);
5172
+ const messageHashes = await this.vault.extractMessageHashes(payload);
5173
+ this.pendingPayloads.clear();
5174
+ const payloadId = `tx_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5175
+ this.pendingPayloads.set(payloadId, { payload, coin, chain, timestamp: Date.now() });
5176
+ this.pendingPayloads.set("latest", { payload, coin, chain, timestamp: Date.now() });
5177
+ return {
5178
+ keysign_payload: payloadId,
5179
+ from_chain: chain.toString(),
5180
+ from_symbol: coin.ticker,
4824
5181
  amount: amountStr,
4825
- symbol: coin.ticker
4826
- }
4827
- };
5182
+ sender: address,
5183
+ destination: toAddress,
5184
+ memo: memo || void 0,
5185
+ message_hashes: messageHashes,
5186
+ tx_details: {
5187
+ chain: chain.toString(),
5188
+ from: address,
5189
+ to: toAddress,
5190
+ amount: amountStr,
5191
+ symbol: coin.ticker
5192
+ }
5193
+ };
5194
+ } catch (err) {
5195
+ await this.releaseEvmLock(chain);
5196
+ throw err;
5197
+ }
4828
5198
  }
4829
5199
  async buildSwapTx(params) {
4830
5200
  if (this.verbose) process.stderr.write(`[build_swap_tx] called with params: ${JSON.stringify(params).slice(0, 500)}
@@ -4834,43 +5204,50 @@ var AgentExecutor = class {
4834
5204
  const fromChain = resolveChain(fromChainName);
4835
5205
  const toChain = toChainName ? resolveChain(toChainName) : null;
4836
5206
  if (!fromChain) throw new Error(`Unknown from_chain: ${fromChainName}`);
4837
- const amountStr = params.amount;
4838
- const fromSymbol = params.from_symbol || params.from_token || "";
4839
- const toSymbol = params.to_symbol || params.to_token || "";
4840
- const fromToken = params.from_contract || params.from_token_id;
4841
- const toToken = params.to_contract || params.to_token_id;
4842
- const fromCoin = { chain: fromChain, token: fromToken || void 0 };
4843
- const toCoin = { chain: toChain || fromChain, token: toToken || void 0 };
4844
- const quote = await this.vault.getSwapQuote({
4845
- fromCoin,
4846
- toCoin,
4847
- amount: parseFloat(amountStr)
4848
- });
4849
- const swapResult = await this.vault.prepareSwapTx({
4850
- fromCoin,
4851
- toCoin,
4852
- amount: parseFloat(amountStr),
4853
- swapQuote: quote,
4854
- autoApprove: true
4855
- });
4856
- const chain = fromChain;
4857
- const payload = swapResult.keysignPayload;
4858
- this.pendingPayloads.clear();
4859
- const payloadId = `swap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4860
- this.pendingPayloads.set(payloadId, { payload, coin: { chain, address: "", decimals: 18, ticker: fromSymbol }, chain, timestamp: Date.now() });
4861
- this.pendingPayloads.set("latest", { payload, coin: { chain, address: "", decimals: 18, ticker: fromSymbol }, chain, timestamp: Date.now() });
4862
- const messageHashes = await this.vault.extractMessageHashes(payload);
4863
- return {
4864
- keysign_payload: payloadId,
4865
- from_chain: fromChain.toString(),
4866
- to_chain: (toChain || fromChain).toString(),
4867
- from_symbol: fromSymbol,
4868
- to_symbol: toSymbol,
4869
- amount: amountStr,
4870
- estimated_output: quote.estimatedOutput?.toString(),
4871
- provider: quote.provider,
4872
- message_hashes: messageHashes
4873
- };
5207
+ await this.acquireEvmLockIfNeeded(fromChain);
5208
+ try {
5209
+ const amountStr = params.amount;
5210
+ const fromSymbol = params.from_symbol || params.from_token || "";
5211
+ const toSymbol = params.to_symbol || params.to_token || "";
5212
+ const fromToken = params.from_contract || params.from_token_id;
5213
+ const toToken = params.to_contract || params.to_token_id;
5214
+ const fromCoin = { chain: fromChain, token: fromToken || void 0 };
5215
+ const toCoin = { chain: toChain || fromChain, token: toToken || void 0 };
5216
+ const quote = await this.vault.getSwapQuote({
5217
+ fromCoin,
5218
+ toCoin,
5219
+ amount: parseFloat(amountStr)
5220
+ });
5221
+ const swapResult = await this.vault.prepareSwapTx({
5222
+ fromCoin,
5223
+ toCoin,
5224
+ amount: parseFloat(amountStr),
5225
+ swapQuote: quote,
5226
+ autoApprove: true
5227
+ });
5228
+ const chain = fromChain;
5229
+ const payload = swapResult.keysignPayload;
5230
+ await this.patchEvmNonce(chain, payload);
5231
+ const messageHashes = await this.vault.extractMessageHashes(payload);
5232
+ this.pendingPayloads.clear();
5233
+ const payloadId = `swap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5234
+ this.pendingPayloads.set(payloadId, { payload, coin: { chain, address: "", decimals: 18, ticker: fromSymbol }, chain, timestamp: Date.now() });
5235
+ this.pendingPayloads.set("latest", { payload, coin: { chain, address: "", decimals: 18, ticker: fromSymbol }, chain, timestamp: Date.now() });
5236
+ return {
5237
+ keysign_payload: payloadId,
5238
+ from_chain: fromChain.toString(),
5239
+ to_chain: (toChain || fromChain).toString(),
5240
+ from_symbol: fromSymbol,
5241
+ to_symbol: toSymbol,
5242
+ amount: amountStr,
5243
+ estimated_output: quote.estimatedOutput?.toString(),
5244
+ provider: quote.provider,
5245
+ message_hashes: messageHashes
5246
+ };
5247
+ } catch (err) {
5248
+ await this.releaseEvmLock(fromChain);
5249
+ throw err;
5250
+ }
4874
5251
  }
4875
5252
  async buildTx(params) {
4876
5253
  if (params.function_name && params.contract_address) {
@@ -4951,6 +5328,18 @@ var AgentExecutor = class {
4951
5328
  }
4952
5329
  const { payload, chain } = stored;
4953
5330
  if (payload.__serverTx) {
5331
+ if (chain === "Solana" && (payload.swap_tx || payload.provider)) {
5332
+ try {
5333
+ return await this.buildAndSignSolanaSwapLocally(payload);
5334
+ } catch (e) {
5335
+ if (e._phase === "prepare") {
5336
+ if (this.verbose) process.stderr.write(`[sign_tx] Solana local build failed (${e.message}), falling back to signServerTx
5337
+ `);
5338
+ } else {
5339
+ throw e;
5340
+ }
5341
+ }
5342
+ }
4954
5343
  return this.signServerTx(payload, chain, params);
4955
5344
  }
4956
5345
  return this.signSdkTx(payload, chain, payloadId);
@@ -4959,33 +5348,45 @@ var AgentExecutor = class {
4959
5348
  * Sign and broadcast an SDK-built transaction (keysign payload from local build methods).
4960
5349
  */
4961
5350
  async signSdkTx(payload, chain, _payloadId) {
4962
- if (this.vault.isEncrypted && !this.vault.isUnlocked?.()) {
4963
- if (this.password) {
4964
- await this.vault.unlock?.(this.password);
5351
+ try {
5352
+ if (this.vault.isEncrypted && !this.vault.isUnlocked?.()) {
5353
+ if (this.password) {
5354
+ await this.vault.unlock?.(this.password);
5355
+ }
4965
5356
  }
5357
+ await this.patchEvmGas(chain, payload);
5358
+ const messageHashes = await this.vault.extractMessageHashes(payload);
5359
+ const signature = await this.vault.sign(
5360
+ {
5361
+ transaction: payload,
5362
+ chain: payload.coin?.chain || chain,
5363
+ messageHashes
5364
+ },
5365
+ {}
5366
+ );
5367
+ const txHash = await this.vault.broadcastTx({
5368
+ chain,
5369
+ keysignPayload: payload,
5370
+ signature
5371
+ });
5372
+ try {
5373
+ this.recordEvmNonceFromPayload(chain, payload, messageHashes.length);
5374
+ } catch (nonceErr) {
5375
+ console.warn(`[nonce] failed to persist nonce for ${chain}:`, nonceErr);
5376
+ }
5377
+ await this.releaseEvmLock(chain);
5378
+ this.pendingPayloads.clear();
5379
+ const explorerUrl = Vultisig6.getTxExplorerUrl(chain, txHash);
5380
+ return {
5381
+ tx_hash: txHash,
5382
+ chain: chain.toString(),
5383
+ status: "pending",
5384
+ explorer_url: explorerUrl
5385
+ };
5386
+ } catch (err) {
5387
+ await this.releaseEvmLock(chain);
5388
+ throw err;
4966
5389
  }
4967
- const messageHashes = await this.vault.extractMessageHashes(payload);
4968
- const signature = await this.vault.sign(
4969
- {
4970
- transaction: payload,
4971
- chain: payload.coin?.chain || chain,
4972
- messageHashes
4973
- },
4974
- {}
4975
- );
4976
- const txHash = await this.vault.broadcastTx({
4977
- chain,
4978
- keysignPayload: payload,
4979
- signature
4980
- });
4981
- this.pendingPayloads.clear();
4982
- const explorerUrl = Vultisig6.getTxExplorerUrl(chain, txHash);
4983
- return {
4984
- tx_hash: txHash,
4985
- chain: chain.toString(),
4986
- status: "pending",
4987
- explorer_url: explorerUrl
4988
- };
4989
5390
  }
4990
5391
  /**
4991
5392
  * Sign and broadcast a server-built transaction (raw EVM tx from tx_ready SSE).
@@ -5004,38 +5405,121 @@ var AgentExecutor = class {
5004
5405
  } else if (chainId) {
5005
5406
  chain = resolveChainId(chainId) || defaultChain;
5006
5407
  }
5007
- const address = await this.vault.address(chain);
5008
- const balance = await this.vault.balance(chain);
5009
- const coin = {
5010
- chain,
5011
- address,
5012
- decimals: balance.decimals || 18,
5013
- ticker: balance.symbol || chain.toString()
5014
- };
5015
- const amount = BigInt(swapTx.value || "0");
5016
- const hasCalldata = !!(swapTx.data && swapTx.data !== "0x");
5017
- if (this.verbose) process.stderr.write(`[sign_server_tx] chain=${chain}, to=${swapTx.to}, value=${swapTx.value}, amount=${amount}, hasCalldata=${hasCalldata}
5408
+ await this.acquireEvmLockIfNeeded(chain);
5409
+ try {
5410
+ const address = await this.vault.address(chain);
5411
+ const balance = await this.vault.balance(chain);
5412
+ const coin = {
5413
+ chain,
5414
+ address,
5415
+ decimals: balance.decimals || 18,
5416
+ ticker: balance.symbol || chain.toString()
5417
+ };
5418
+ const amount = BigInt(swapTx.value || "0");
5419
+ const hasCalldata = !!(swapTx.data && swapTx.data !== "0x");
5420
+ if (this.verbose) process.stderr.write(`[sign_server_tx] chain=${chain}, to=${swapTx.to}, value=${swapTx.value}, amount=${amount}, hasCalldata=${hasCalldata}
5421
+ `);
5422
+ if (this.vault.isEncrypted && !this.vault.isUnlocked?.()) {
5423
+ if (this.password) {
5424
+ await this.vault.unlock?.(this.password);
5425
+ }
5426
+ }
5427
+ const buildAmount = amount === 0n && hasCalldata ? 1n : amount;
5428
+ const keysignPayload = await this.vault.prepareSendTx({
5429
+ coin,
5430
+ receiver: swapTx.to,
5431
+ amount: buildAmount,
5432
+ memo: swapTx.data
5433
+ });
5434
+ if (amount === 0n && hasCalldata) {
5435
+ ;
5436
+ keysignPayload.toAmount = "0";
5437
+ }
5438
+ await this.patchEvmNonce(chain, keysignPayload);
5439
+ await this.patchEvmGas(chain, keysignPayload);
5440
+ const messageHashes = await this.vault.extractMessageHashes(keysignPayload);
5441
+ const signature = await this.vault.sign(
5442
+ {
5443
+ transaction: keysignPayload,
5444
+ chain,
5445
+ messageHashes
5446
+ },
5447
+ {}
5448
+ );
5449
+ const txHash = await this.vault.broadcastTx({
5450
+ chain,
5451
+ keysignPayload,
5452
+ signature
5453
+ });
5454
+ try {
5455
+ this.recordEvmNonceFromPayload(chain, keysignPayload, messageHashes.length);
5456
+ } catch (nonceErr) {
5457
+ console.warn(`[nonce] failed to persist nonce for ${chain}:`, nonceErr);
5458
+ }
5459
+ await this.releaseEvmLock(chain);
5460
+ this.pendingPayloads.clear();
5461
+ const explorerUrl = Vultisig6.getTxExplorerUrl(chain, txHash);
5462
+ return {
5463
+ tx_hash: txHash,
5464
+ chain: chain.toString(),
5465
+ status: "pending",
5466
+ explorer_url: explorerUrl
5467
+ };
5468
+ } catch (err) {
5469
+ await this.releaseEvmLock(chain);
5470
+ throw err;
5471
+ }
5472
+ }
5473
+ /**
5474
+ * Build, sign, and broadcast a Solana swap locally using the SDK's swap flow.
5475
+ * Uses swap params from the tx_ready event to call vault.getSwapQuote → prepareSwapTx.
5476
+ */
5477
+ async buildAndSignSolanaSwapLocally(serverTxData) {
5478
+ const fromChainName = serverTxData.from_chain || serverTxData.chain || "Solana";
5479
+ const toChainName = serverTxData.to_chain;
5480
+ const fromChain = resolveChain(fromChainName);
5481
+ if (!fromChain) throw Object.assign(new Error(`Unknown from_chain: ${fromChainName}`), { _phase: "prepare" });
5482
+ const toChain = toChainName ? resolveChain(toChainName) : fromChain;
5483
+ if (!toChain) throw Object.assign(new Error(`Unknown to_chain: ${toChainName}`), { _phase: "prepare" });
5484
+ const amountStr = serverTxData.amount;
5485
+ if (!amountStr) throw Object.assign(new Error("Missing amount in tx_ready data for local Solana swap build"), { _phase: "prepare" });
5486
+ const fromToken = serverTxData.from_address;
5487
+ const toToken = serverTxData.to_address;
5488
+ const fromDecimals = serverTxData.from_decimals;
5489
+ if (fromDecimals == null) throw Object.assign(new Error("Missing from_decimals in tx_ready data for local Solana swap build"), { _phase: "prepare" });
5490
+ const fromCoin = { chain: fromChain, token: fromToken || void 0 };
5491
+ const toCoin = { chain: toChain, token: toToken || void 0 };
5492
+ const humanAmount = Number(amountStr) / Math.pow(10, fromDecimals);
5493
+ if (this.verbose) process.stderr.write(`[solana_local_swap] from=${fromChainName} to=${toChainName || fromChainName} amount=${amountStr}
5018
5494
  `);
5019
5495
  if (this.vault.isEncrypted && !this.vault.isUnlocked?.()) {
5020
5496
  if (this.password) {
5021
5497
  await this.vault.unlock?.(this.password);
5022
5498
  }
5023
5499
  }
5024
- const buildAmount = amount === 0n && hasCalldata ? 1n : amount;
5025
- const keysignPayload = await this.vault.prepareSendTx({
5026
- coin,
5027
- receiver: swapTx.to,
5028
- amount: buildAmount,
5029
- memo: swapTx.data
5030
- });
5031
- if (amount === 0n && hasCalldata) {
5032
- ;
5033
- keysignPayload.toAmount = "0";
5500
+ let quote, swapResult;
5501
+ try {
5502
+ quote = await this.vault.getSwapQuote({
5503
+ fromCoin,
5504
+ toCoin,
5505
+ amount: humanAmount
5506
+ });
5507
+ swapResult = await this.vault.prepareSwapTx({
5508
+ fromCoin,
5509
+ toCoin,
5510
+ amount: humanAmount,
5511
+ swapQuote: quote,
5512
+ autoApprove: true
5513
+ });
5514
+ } catch (e) {
5515
+ throw Object.assign(e, { _phase: "prepare" });
5034
5516
  }
5035
- const messageHashes = await this.vault.extractMessageHashes(keysignPayload);
5517
+ const payload = swapResult.keysignPayload;
5518
+ const chain = fromChain;
5519
+ const messageHashes = await this.vault.extractMessageHashes(payload);
5036
5520
  const signature = await this.vault.sign(
5037
5521
  {
5038
- transaction: keysignPayload,
5522
+ transaction: payload,
5039
5523
  chain,
5040
5524
  messageHashes
5041
5525
  },
@@ -5043,7 +5527,7 @@ var AgentExecutor = class {
5043
5527
  );
5044
5528
  const txHash = await this.vault.broadcastTx({
5045
5529
  chain,
5046
- keysignPayload,
5530
+ keysignPayload: payload,
5047
5531
  signature
5048
5532
  });
5049
5533
  this.pendingPayloads.clear();
@@ -5056,6 +5540,147 @@ var AgentExecutor = class {
5056
5540
  };
5057
5541
  }
5058
5542
  // ============================================================================
5543
+ // EVM Nonce Management
5544
+ // ============================================================================
5545
+ /**
5546
+ * Acquire chain-level file lock if the chain is EVM.
5547
+ * Releases any previously held lock first (e.g. from an abandoned build).
5548
+ */
5549
+ async acquireEvmLockIfNeeded(chain) {
5550
+ if (!this.stateStore || !EVM_CHAINS.has(chain)) return;
5551
+ await this.releaseEvmLock(chain);
5552
+ const release = await this.stateStore.acquireChainLock(chain);
5553
+ this.chainLockReleases.set(chain, release);
5554
+ if (this.verbose) process.stderr.write(`[nonce] Acquired lock for ${chain}
5555
+ `);
5556
+ }
5557
+ /**
5558
+ * Release the held chain lock (no-op if not held).
5559
+ */
5560
+ async releaseEvmLock(chain) {
5561
+ const release = this.chainLockReleases.get(chain);
5562
+ if (release) {
5563
+ await release();
5564
+ this.chainLockReleases.delete(chain);
5565
+ if (this.verbose) process.stderr.write(`[nonce] Released lock for ${chain}
5566
+ `);
5567
+ }
5568
+ }
5569
+ /**
5570
+ * Patch the EVM nonce in a keysign payload if our local state is ahead of on-chain.
5571
+ * The payload's blockchainSpecific.ethereumSpecific.nonce was set from RPC during
5572
+ * prepareSendTx(). If we have locally-tracked pending txs, we override with a higher value.
5573
+ *
5574
+ * Also detects evicted txs: if local state claims a higher nonce but there are
5575
+ * no pending txs in the mempool (pending == latest), the intermediate txs were
5576
+ * dropped and local state is stale.
5577
+ */
5578
+ async patchEvmNonce(chain, payload) {
5579
+ if (!this.stateStore || !EVM_CHAINS.has(chain)) return;
5580
+ const bs = payload.blockchainSpecific;
5581
+ if (!bs || bs.case !== "ethereumSpecific") return;
5582
+ const rpcNonce = bs.value.nonce;
5583
+ const nextNonce = this.stateStore.getNextEvmNonce(chain, rpcNonce);
5584
+ if (nextNonce !== rpcNonce) {
5585
+ const pendingNonce = await this.fetchEvmPendingNonce(chain);
5586
+ if (pendingNonce !== null && pendingNonce === rpcNonce) {
5587
+ if (this.verbose) process.stderr.write(`[nonce] Stale local state for ${chain}: local=${nextNonce}, on-chain=${rpcNonce}, no pending txs \u2014 using on-chain nonce
5588
+ `);
5589
+ this.stateStore.clearEvmState(chain);
5590
+ return;
5591
+ }
5592
+ const nonceGap = nextNonce - rpcNonce;
5593
+ if (pendingNonce === null && nonceGap > 3n) {
5594
+ if (this.verbose) process.stderr.write(`[nonce] Large nonce gap for ${chain} (${nonceGap}) and couldn't verify pending txs \u2014 using on-chain nonce ${rpcNonce}
5595
+ `);
5596
+ this.stateStore.clearEvmState(chain);
5597
+ return;
5598
+ }
5599
+ bs.value.nonce = nextNonce;
5600
+ if (this.verbose) process.stderr.write(`[nonce] Patched ${chain} nonce: ${rpcNonce} \u2192 ${nextNonce}
5601
+ `);
5602
+ }
5603
+ }
5604
+ /**
5605
+ * Ensure the keysign payload's maxFeePerGas covers current network base fee.
5606
+ * Re-fetches latest base fee from RPC and bumps maxFeePerGas if it's too low.
5607
+ * Compensates for gas price drift between build time and sign time.
5608
+ */
5609
+ async patchEvmGas(chain, payload) {
5610
+ if (!EVM_CHAINS.has(chain)) return;
5611
+ const bs = payload.blockchainSpecific;
5612
+ if (!bs || bs.case !== "ethereumSpecific") return;
5613
+ const rpcUrl = EVM_GAS_RPC[chain];
5614
+ if (!rpcUrl) return;
5615
+ try {
5616
+ const res = await fetch(rpcUrl, {
5617
+ method: "POST",
5618
+ headers: { "Content-Type": "application/json" },
5619
+ body: JSON.stringify({
5620
+ jsonrpc: "2.0",
5621
+ method: "eth_getBlockByNumber",
5622
+ params: ["latest", false],
5623
+ id: 1
5624
+ }),
5625
+ signal: AbortSignal.timeout(5e3)
5626
+ });
5627
+ const data = await res.json();
5628
+ const baseFee = BigInt(data.result?.baseFeePerGas || "0");
5629
+ if (baseFee === 0n) return;
5630
+ const currentPriorityFee = BigInt(bs.value.priorityFee || "0");
5631
+ const currentMaxFee = BigInt(bs.value.maxFeePerGasWei || "0");
5632
+ const minMaxFee = baseFee * 25n / 10n + currentPriorityFee;
5633
+ if (currentMaxFee < minMaxFee) {
5634
+ bs.value.maxFeePerGasWei = minMaxFee.toString();
5635
+ if (this.verbose) process.stderr.write(`[gas] Bumped ${chain} maxFeePerGas: ${currentMaxFee} \u2192 ${minMaxFee} (baseFee=${baseFee})
5636
+ `);
5637
+ }
5638
+ } catch {
5639
+ if (this.verbose) process.stderr.write(`[gas] Failed to refresh base fee for ${chain}, keeping original
5640
+ `);
5641
+ }
5642
+ }
5643
+ /**
5644
+ * Fetch the pending nonce from RPC (eth_getTransactionCount with "pending" tag).
5645
+ * Returns null if the RPC call fails (non-fatal).
5646
+ */
5647
+ async fetchEvmPendingNonce(chain) {
5648
+ const rpcUrl = EVM_GAS_RPC[chain];
5649
+ if (!rpcUrl) return null;
5650
+ try {
5651
+ const address = await this.vault.address(chain);
5652
+ const res = await fetch(rpcUrl, {
5653
+ method: "POST",
5654
+ headers: { "Content-Type": "application/json" },
5655
+ body: JSON.stringify({
5656
+ jsonrpc: "2.0",
5657
+ method: "eth_getTransactionCount",
5658
+ params: [address, "pending"],
5659
+ id: 1
5660
+ }),
5661
+ signal: AbortSignal.timeout(5e3)
5662
+ });
5663
+ const data = await res.json();
5664
+ return BigInt(data.result || "0");
5665
+ } catch {
5666
+ return null;
5667
+ }
5668
+ }
5669
+ /**
5670
+ * Record the nonce(s) used after a successful broadcast.
5671
+ * For approve+swap flows with N message hashes, the highest nonce used is base + N - 1.
5672
+ */
5673
+ recordEvmNonceFromPayload(chain, payload, numTxs) {
5674
+ if (!this.stateStore || !EVM_CHAINS.has(chain)) return;
5675
+ const bs = payload.blockchainSpecific;
5676
+ if (!bs || bs.case !== "ethereumSpecific") return;
5677
+ const baseNonce = bs.value.nonce;
5678
+ const highestNonce = baseNonce + BigInt(Math.max(0, numTxs - 1));
5679
+ this.stateStore.recordEvmNonce(chain, highestNonce);
5680
+ if (this.verbose) process.stderr.write(`[nonce] Recorded ${chain} nonce: ${highestNonce}
5681
+ `);
5682
+ }
5683
+ // ============================================================================
5059
5684
  // EIP-712 Typed Data Signing
5060
5685
  // ============================================================================
5061
5686
  /**
@@ -5636,9 +6261,9 @@ var PipeInterface = class {
5636
6261
  };
5637
6262
 
5638
6263
  // src/agent/session.ts
5639
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5640
- import { homedir } from "node:os";
5641
- import { join } from "node:path";
6264
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
6265
+ import { homedir as homedir2 } from "node:os";
6266
+ import { join as join2 } from "node:path";
5642
6267
  var AgentSession = class {
5643
6268
  client;
5644
6269
  vault;
@@ -5654,7 +6279,7 @@ var AgentSession = class {
5654
6279
  this.config = config;
5655
6280
  this.client = new AgentClient(config.backendUrl);
5656
6281
  this.client.verbose = !!config.verbose;
5657
- this.executor = new AgentExecutor(vault, !!config.verbose);
6282
+ this.executor = new AgentExecutor(vault, !!config.verbose, vault.publicKeys.ecdsa);
5658
6283
  this.publicKey = vault.publicKeys.ecdsa;
5659
6284
  if (config.password) {
5660
6285
  this.executor.setPassword(config.password);
@@ -5709,7 +6334,7 @@ var AgentSession = class {
5709
6334
  const conv = await this.client.createConversation(this.publicKey);
5710
6335
  this.conversationId = conv.id;
5711
6336
  }
5712
- this.cachedContext = await buildMessageContext(this.vault);
6337
+ this.cachedContext = this.config.viaAgent || this.config.askMode ? await buildMinimalContext(this.vault) : await buildMessageContext(this.vault);
5713
6338
  }
5714
6339
  getConversationId() {
5715
6340
  return this.conversationId;
@@ -5736,7 +6361,7 @@ var AgentSession = class {
5736
6361
  }
5737
6362
  this.abortController = new AbortController();
5738
6363
  try {
5739
- this.cachedContext = await buildMessageContext(this.vault);
6364
+ this.cachedContext = this.config.viaAgent || this.config.askMode ? await buildMinimalContext(this.vault) : await buildMessageContext(this.vault);
5740
6365
  } catch {
5741
6366
  }
5742
6367
  try {
@@ -5765,6 +6390,9 @@ var AgentSession = class {
5765
6390
  public_key: this.publicKey,
5766
6391
  context: this.cachedContext
5767
6392
  };
6393
+ if (this.config.viaAgent || this.config.askMode) {
6394
+ request.via_agent = true;
6395
+ }
5768
6396
  if (content) {
5769
6397
  request.content = content;
5770
6398
  }
@@ -5959,23 +6587,23 @@ function parseInlineToolCalls(text) {
5959
6587
  return actions;
5960
6588
  }
5961
6589
  function getTokenCachePath() {
5962
- const dir = process.env.VULTISIG_CONFIG_DIR ?? join(homedir(), ".vultisig");
5963
- return join(dir, "agent-tokens.json");
6590
+ const dir = process.env.VULTISIG_CONFIG_DIR ?? join2(homedir2(), ".vultisig");
6591
+ return join2(dir, "agent-tokens.json");
5964
6592
  }
5965
6593
  function readTokenStore() {
5966
6594
  try {
5967
- const path3 = getTokenCachePath();
5968
- if (!existsSync(path3)) return {};
5969
- return JSON.parse(readFileSync(path3, "utf-8"));
6595
+ const path4 = getTokenCachePath();
6596
+ if (!existsSync(path4)) return {};
6597
+ return JSON.parse(readFileSync2(path4, "utf-8"));
5970
6598
  } catch {
5971
6599
  return {};
5972
6600
  }
5973
6601
  }
5974
6602
  function writeTokenStore(store) {
5975
- const path3 = getTokenCachePath();
5976
- const dir = join(path3, "..");
5977
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
5978
- writeFileSync(path3, JSON.stringify(store, null, 2), { mode: 384 });
6603
+ const path4 = getTokenCachePath();
6604
+ const dir = join2(path4, "..");
6605
+ if (!existsSync(dir)) mkdirSync2(dir, { recursive: true });
6606
+ writeFileSync2(path4, JSON.stringify(store, null, 2), { mode: 384 });
5979
6607
  }
5980
6608
  function loadCachedToken(publicKey) {
5981
6609
  const store = readTokenStore();
@@ -6353,6 +6981,68 @@ async function executeAgent(ctx2, options) {
6353
6981
  }
6354
6982
  }
6355
6983
  }
6984
+ async function executeAgentAsk(ctx2, message, options) {
6985
+ setSilentMode(true);
6986
+ const originalConsoleLog = console.log;
6987
+ console.log = (...args) => {
6988
+ process.stderr.write(args.map(String).join(" ") + "\n");
6989
+ };
6990
+ try {
6991
+ const vault = await ctx2.ensureActiveVault();
6992
+ const config = {
6993
+ backendUrl: options.backendUrl || process.env.VULTISIG_AGENT_URL || "http://localhost:9998",
6994
+ vaultName: vault.name,
6995
+ password: options.password,
6996
+ sessionId: options.session,
6997
+ verbose: options.verbose,
6998
+ askMode: true
6999
+ };
7000
+ const session = new AgentSession(vault, config);
7001
+ const ask = new AskInterface(session, !!config.verbose);
7002
+ const callbacks = ask.getCallbacks();
7003
+ await session.initialize(callbacks);
7004
+ const result = await ask.ask(message);
7005
+ if (options.json) {
7006
+ process.stdout.write(
7007
+ JSON.stringify({
7008
+ session_id: result.sessionId,
7009
+ response: result.response,
7010
+ tool_calls: result.toolCalls,
7011
+ transactions: result.transactions
7012
+ }) + "\n"
7013
+ );
7014
+ } else {
7015
+ process.stdout.write(`session:${result.sessionId}
7016
+ `);
7017
+ if (result.response) {
7018
+ process.stdout.write(`
7019
+ ${result.response}
7020
+ `);
7021
+ }
7022
+ for (const tx of result.transactions) {
7023
+ process.stdout.write(`
7024
+ tx:${tx.chain}:${tx.hash}
7025
+ `);
7026
+ if (tx.explorerUrl) {
7027
+ process.stdout.write(`explorer:${tx.explorerUrl}
7028
+ `);
7029
+ }
7030
+ }
7031
+ }
7032
+ } catch (err) {
7033
+ if (options.json) {
7034
+ process.stdout.write(JSON.stringify({ error: err.message }) + "\n");
7035
+ } else {
7036
+ process.stderr.write(`Error: ${err.message}
7037
+ `);
7038
+ }
7039
+ process.exit(1);
7040
+ } finally {
7041
+ console.log = originalConsoleLog;
7042
+ setSilentMode(false);
7043
+ }
7044
+ process.exit(0);
7045
+ }
6356
7046
  async function executeAgentSessionsList(ctx2, options) {
6357
7047
  const vault = await ctx2.ensureActiveVault();
6358
7048
  const backendUrl = options.backendUrl || process.env.VULTISIG_AGENT_URL || "http://localhost:9998";
@@ -6429,8 +7119,8 @@ function formatDate(iso) {
6429
7119
 
6430
7120
  // src/interactive/completer.ts
6431
7121
  import { Chain as Chain10 } from "@vultisig/sdk";
6432
- import fs2 from "fs";
6433
- import path2 from "path";
7122
+ import fs3 from "fs";
7123
+ import path3 from "path";
6434
7124
  var COMMANDS = [
6435
7125
  // Vault management
6436
7126
  "vaults",
@@ -6525,28 +7215,28 @@ function createCompleter(ctx2) {
6525
7215
  }
6526
7216
  function completeFilePath(partial, filterVult) {
6527
7217
  try {
6528
- const endsWithSeparator = partial.endsWith("/") || partial.endsWith(path2.sep);
7218
+ const endsWithSeparator = partial.endsWith("/") || partial.endsWith(path3.sep);
6529
7219
  let dir;
6530
7220
  let basename;
6531
7221
  if (endsWithSeparator) {
6532
7222
  dir = partial;
6533
7223
  basename = "";
6534
7224
  } else {
6535
- dir = path2.dirname(partial);
6536
- basename = path2.basename(partial);
6537
- if (fs2.existsSync(partial) && fs2.statSync(partial).isDirectory()) {
7225
+ dir = path3.dirname(partial);
7226
+ basename = path3.basename(partial);
7227
+ if (fs3.existsSync(partial) && fs3.statSync(partial).isDirectory()) {
6538
7228
  dir = partial;
6539
7229
  basename = "";
6540
7230
  }
6541
7231
  }
6542
- const resolvedDir = path2.resolve(dir);
6543
- if (!fs2.existsSync(resolvedDir) || !fs2.statSync(resolvedDir).isDirectory()) {
7232
+ const resolvedDir = path3.resolve(dir);
7233
+ if (!fs3.existsSync(resolvedDir) || !fs3.statSync(resolvedDir).isDirectory()) {
6544
7234
  return [[], partial];
6545
7235
  }
6546
- const files = fs2.readdirSync(resolvedDir);
7236
+ const files = fs3.readdirSync(resolvedDir);
6547
7237
  const matches = files.filter((file) => file.startsWith(basename)).map((file) => {
6548
- const fullPath = path2.join(dir, file);
6549
- const stats = fs2.statSync(path2.join(resolvedDir, file));
7238
+ const fullPath = path3.join(dir, file);
7239
+ const stats = fs3.statSync(path3.join(resolvedDir, file));
6550
7240
  if (stats.isDirectory()) {
6551
7241
  return fullPath + "/";
6552
7242
  }
@@ -7825,19 +8515,19 @@ import chalk13 from "chalk";
7825
8515
 
7826
8516
  // src/lib/version.ts
7827
8517
  import chalk14 from "chalk";
7828
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
7829
- import { homedir as homedir2 } from "os";
7830
- import { join as join2 } from "path";
8518
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
8519
+ import { homedir as homedir3 } from "os";
8520
+ import { join as join3 } from "path";
7831
8521
  var cachedVersion = null;
7832
8522
  function getVersion() {
7833
8523
  if (cachedVersion) return cachedVersion;
7834
8524
  if (true) {
7835
- cachedVersion = "0.8.0";
8525
+ cachedVersion = "0.9.0";
7836
8526
  return cachedVersion;
7837
8527
  }
7838
8528
  try {
7839
8529
  const packagePath = new URL("../../package.json", import.meta.url);
7840
- const pkg = JSON.parse(readFileSync2(packagePath, "utf-8"));
8530
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf-8"));
7841
8531
  cachedVersion = pkg.version;
7842
8532
  return cachedVersion;
7843
8533
  } catch {
@@ -7845,13 +8535,13 @@ function getVersion() {
7845
8535
  return cachedVersion;
7846
8536
  }
7847
8537
  }
7848
- var CACHE_DIR = join2(homedir2(), ".vultisig", "cache");
7849
- var VERSION_CACHE_FILE = join2(CACHE_DIR, "version-check.json");
8538
+ var CACHE_DIR = join3(homedir3(), ".vultisig", "cache");
8539
+ var VERSION_CACHE_FILE = join3(CACHE_DIR, "version-check.json");
7850
8540
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7851
8541
  function readVersionCache() {
7852
8542
  try {
7853
8543
  if (!existsSync2(VERSION_CACHE_FILE)) return null;
7854
- const data = readFileSync2(VERSION_CACHE_FILE, "utf-8");
8544
+ const data = readFileSync3(VERSION_CACHE_FILE, "utf-8");
7855
8545
  return JSON.parse(data);
7856
8546
  } catch {
7857
8547
  return null;
@@ -7860,9 +8550,9 @@ function readVersionCache() {
7860
8550
  function writeVersionCache(cache) {
7861
8551
  try {
7862
8552
  if (!existsSync2(CACHE_DIR)) {
7863
- mkdirSync2(CACHE_DIR, { recursive: true });
8553
+ mkdirSync3(CACHE_DIR, { recursive: true });
7864
8554
  }
7865
- writeFileSync2(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2));
8555
+ writeFileSync3(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2));
7866
8556
  } catch {
7867
8557
  }
7868
8558
  }
@@ -7967,9 +8657,9 @@ function getUpdateCommand() {
7967
8657
  }
7968
8658
 
7969
8659
  // src/lib/completion.ts
7970
- import { homedir as homedir3 } from "os";
7971
- import { join as join3 } from "path";
7972
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
8660
+ import { homedir as homedir4 } from "os";
8661
+ import { join as join4 } from "path";
8662
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
7973
8663
  var tabtab = null;
7974
8664
  async function getTabtab() {
7975
8665
  if (!tabtab) {
@@ -8034,7 +8724,7 @@ var CHAINS = [
8034
8724
  ];
8035
8725
  function getVaultNames() {
8036
8726
  try {
8037
- const vaultDir = join3(homedir3(), ".vultisig", "vaults");
8727
+ const vaultDir = join4(homedir4(), ".vultisig", "vaults");
8038
8728
  if (!existsSync3(vaultDir)) return [];
8039
8729
  const { readdirSync } = __require("fs");
8040
8730
  const files = readdirSync(vaultDir);
@@ -8042,7 +8732,7 @@ function getVaultNames() {
8042
8732
  for (const file of files) {
8043
8733
  if (file.startsWith("vault:") && file.endsWith(".json")) {
8044
8734
  try {
8045
- const content = readFileSync3(join3(vaultDir, file), "utf-8");
8735
+ const content = readFileSync4(join4(vaultDir, file), "utf-8");
8046
8736
  const vault = JSON.parse(content);
8047
8737
  if (vault.name) names.push(vault.name);
8048
8738
  if (vault.id) names.push(vault.id);
@@ -8365,10 +9055,10 @@ createCmd.command("secure").description("Create a secure vault (multi-device MPC
8365
9055
  });
8366
9056
  })
8367
9057
  );
8368
- program.command("import <file>").description("Import vault from .vult file").action(
8369
- withExit(async (file) => {
9058
+ program.command("import <file>").description("Import vault from .vult file").option("--password <password>", "Password to decrypt the vault file").action(
9059
+ withExit(async (file, options) => {
8370
9060
  const context = await init(program.opts().vault);
8371
- await executeImport(context, file);
9061
+ await executeImport(context, file, options.password);
8372
9062
  })
8373
9063
  );
8374
9064
  var createFromSeedphraseCmd = program.command("create-from-seedphrase").description("Create vault from BIP39 seedphrase");
@@ -8485,7 +9175,7 @@ joinCmd.command("secure").description("Join a SecureVault creation session").opt
8485
9175
  const context = await init(program.opts().vault);
8486
9176
  let qrPayload = options.qr;
8487
9177
  if (!qrPayload && options.qrFile) {
8488
- qrPayload = (await fs3.readFile(options.qrFile, "utf-8")).trim();
9178
+ qrPayload = (await fs4.readFile(options.qrFile, "utf-8")).trim();
8489
9179
  }
8490
9180
  if (!qrPayload) {
8491
9181
  qrPayload = await promptQrPayload();
@@ -8635,10 +9325,10 @@ program.command("discount").description("Show your VULT discount tier for swap f
8635
9325
  })
8636
9326
  );
8637
9327
  program.command("export [path]").description("Export vault to file").option("--password <password>", "Password to unlock the vault (for encrypted vaults)").option("--exportPassword <password>", "Password to encrypt the exported file (defaults to --password)").action(
8638
- withExit(async (path3, options) => {
9328
+ withExit(async (path4, options) => {
8639
9329
  const context = await init(program.opts().vault, options.password);
8640
9330
  await executeExport(context, {
8641
- outputPath: path3,
9331
+ outputPath: path4,
8642
9332
  password: options.password,
8643
9333
  exportPassword: options.exportPassword
8644
9334
  });
@@ -8856,6 +9546,21 @@ var agentCmd = program.command("agent").description("AI-powered chat interface f
8856
9546
  sessionId: options.sessionId
8857
9547
  });
8858
9548
  });
9549
+ agentCmd.command("ask <message>").description("Send a single message and get the response (for AI agent integration)").option("--session <id>", "Continue an existing conversation").option("--backend-url <url>", "Agent backend URL (default: http://localhost:9998)").option("--password <password>", "Vault password for signing operations").option("--verbose", "Show tool calls and debug info on stderr").option("--json", "Output structured JSON instead of text").action(
9550
+ async (message, options) => {
9551
+ const parentOpts = agentCmd.opts();
9552
+ const context = await init(
9553
+ program.opts().vault,
9554
+ options.password || parentOpts.password
9555
+ );
9556
+ await executeAgentAsk(context, message, {
9557
+ ...options,
9558
+ backendUrl: options.backendUrl || parentOpts.backendUrl,
9559
+ password: options.password || parentOpts.password,
9560
+ verbose: options.verbose || parentOpts.verbose
9561
+ });
9562
+ }
9563
+ );
8859
9564
  var sessionsCmd = agentCmd.command("sessions").description("Manage agent chat sessions");
8860
9565
  sessionsCmd.command("list").description("List chat sessions for the current vault").option("--backend-url <url>", "Agent backend URL (default: http://localhost:9998)").option("--password <password>", "Vault password for authentication").action(
8861
9566
  withExit(async (options) => {