@vultisig/cli 2.22.0 → 3.0.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 +72 -0
  2. package/dist/index.js +1488 -1346
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1687,7 +1687,7 @@ function createHasher(hashCons) {
1687
1687
  hashC.create = () => hashCons();
1688
1688
  return hashC;
1689
1689
  }
1690
- function randomBytes3(bytesLength = 32) {
1690
+ function randomBytes2(bytesLength = 32) {
1691
1691
  if (crypto2 && typeof crypto2.getRandomValues === "function") {
1692
1692
  return crypto2.getRandomValues(new Uint8Array(bytesLength));
1693
1693
  }
@@ -4467,7 +4467,7 @@ function getHash(hash) {
4467
4467
  return {
4468
4468
  hash,
4469
4469
  hmac: (key, ...msgs) => hmac(hash, key, concatBytes(...msgs)),
4470
- randomBytes: randomBytes3
4470
+ randomBytes: randomBytes2
4471
4471
  };
4472
4472
  }
4473
4473
  function createCurve(curveDef, defHash) {
@@ -4700,7 +4700,7 @@ function challenge(...args) {
4700
4700
  function schnorrGetPublicKey(privateKey) {
4701
4701
  return schnorrGetExtPubKey(privateKey).bytes;
4702
4702
  }
4703
- function schnorrSign(message, privateKey, auxRand = randomBytes3(32)) {
4703
+ function schnorrSign(message, privateKey, auxRand = randomBytes2(32)) {
4704
4704
  const m = ensureBytes("message", message);
4705
4705
  const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey);
4706
4706
  const a = ensureBytes("auxRand", auxRand, 32);
@@ -5855,12 +5855,20 @@ function displayPortfolio(portfolio, currency, _raw = false) {
5855
5855
  printResult(chalk2.cyan("| ") + chalk2.bold.green(totalDisplay) + chalk2.cyan(" |"));
5856
5856
  printResult(chalk2.cyan("+----------------------------------------+\n"));
5857
5857
  printResult(chalk2.bold("Chain Breakdown:\n"));
5858
- const table = portfolio.chainBalances.map(({ chain, balance, value }) => ({
5859
- Chain: chain,
5860
- Amount: balance.formattedAmount,
5861
- Symbol: balance.symbol,
5862
- Value: value ? `${value.amount} ${value.currency.toUpperCase()}` : "N/A"
5863
- }));
5858
+ const table = portfolio.chainBalances.flatMap(({ chain, balance, value, tokens }) => [
5859
+ {
5860
+ Chain: chain,
5861
+ Amount: balance.formattedAmount,
5862
+ Symbol: balance.symbol,
5863
+ Value: value ? `${value.amount} ${value.currency.toUpperCase()}` : "N/A"
5864
+ },
5865
+ ...(tokens ?? []).map((token) => ({
5866
+ Chain: chain,
5867
+ Amount: token.balance?.formattedAmount ?? "-",
5868
+ Symbol: token.balance?.symbol ?? token.tokenId,
5869
+ Value: `${token.value.amount} ${token.value.currency.toUpperCase()}`
5870
+ }))
5871
+ ]);
5864
5872
  printTable(table);
5865
5873
  }
5866
5874
  function displayAddresses(addresses) {
@@ -6085,6 +6093,16 @@ async function executeBalance(ctx2, options = {}) {
6085
6093
  const spinner = createSpinner("Loading balances...");
6086
6094
  const raw = options.raw ?? false;
6087
6095
  if (options.chain) {
6096
+ if (options.includeTokens) {
6097
+ const balances = await vault.balances([options.chain], true);
6098
+ spinner.succeed("Balances loaded");
6099
+ if (isJsonOutput()) {
6100
+ outputJson({ chain: options.chain, balances });
6101
+ return;
6102
+ }
6103
+ displayBalancesTable(balances, raw);
6104
+ return;
6105
+ }
6088
6106
  const balance = await vault.balance(options.chain);
6089
6107
  spinner.succeed("Balance loaded");
6090
6108
  if (isJsonOutput()) {
@@ -6115,7 +6133,6 @@ async function executePortfolio(ctx2, options = {}) {
6115
6133
  }
6116
6134
  const currencyName = fiatCurrencyNameRecord2[currency];
6117
6135
  const spinner = createSpinner(`Loading portfolio in ${currencyName}...`);
6118
- const totalValue = await vault.getTotalValue(currency);
6119
6136
  const chains = vault.chains;
6120
6137
  const results = await Promise.all(
6121
6138
  chains.map(async (chain) => {
@@ -6125,12 +6142,34 @@ async function executePortfolio(ctx2, options = {}) {
6125
6142
  } catch (err) {
6126
6143
  return { failure: { chain, stage: "balance", error: conciseError(err) } };
6127
6144
  }
6145
+ let values;
6128
6146
  try {
6129
- const value = await vault.getValue(chain, void 0, currency);
6130
- return { entry: { chain, balance, value } };
6147
+ values = await vault.getValues(chain, currency);
6131
6148
  } catch (err) {
6132
6149
  return { entry: { chain, balance }, failure: { chain, stage: "value", error: conciseError(err) } };
6133
6150
  }
6151
+ const { native, ...tokenValues } = values;
6152
+ if (!native) {
6153
+ try {
6154
+ const retried = await vault.getValue(chain, void 0, currency);
6155
+ return { entry: { chain, balance, value: retried, tokens: await withAmounts(tokenValues) } };
6156
+ } catch (err) {
6157
+ return {
6158
+ entry: { chain, balance, tokens: await withAmounts(tokenValues) },
6159
+ failure: { chain, stage: "value", error: conciseError(err) }
6160
+ };
6161
+ }
6162
+ }
6163
+ return { entry: { chain, balance, value: native, tokens: await withAmounts(tokenValues) } };
6164
+ async function withAmounts(byTokenId) {
6165
+ return Promise.all(
6166
+ Object.entries(byTokenId).map(async ([tokenId, value]) => ({
6167
+ tokenId,
6168
+ value,
6169
+ balance: await vault.balance(chain, tokenId).catch(() => void 0)
6170
+ }))
6171
+ );
6172
+ }
6134
6173
  })
6135
6174
  );
6136
6175
  const chainBalances = [];
@@ -6147,6 +6186,11 @@ async function executePortfolio(ctx2, options = {}) {
6147
6186
  ["Check your internet connection", "Retry in a few moments"]
6148
6187
  );
6149
6188
  }
6189
+ const total = chainBalances.reduce(
6190
+ (sum, entry) => sum + parseFloat(entry.value?.amount ?? "0") + (entry.tokens ?? []).reduce((tokenSum, token) => tokenSum + parseFloat(token.value.amount), 0),
6191
+ 0
6192
+ );
6193
+ const totalValue = { amount: total.toFixed(2), currency, lastUpdated: Date.now() };
6150
6194
  const portfolio = { totalValue, chainBalances };
6151
6195
  spinner.succeed("Portfolio loaded");
6152
6196
  if (isJsonOutput()) {
@@ -6324,8 +6368,8 @@ import {
6324
6368
  writeFileSync,
6325
6369
  writeSync
6326
6370
  } from "node:fs";
6327
- import { homedir } from "node:os";
6328
6371
  import { dirname, join } from "node:path";
6372
+ import { getVultisigConfigDir } from "@vultisig/client-shared";
6329
6373
 
6330
6374
  // src/agent/agentErrors.ts
6331
6375
  import { VaultError as VaultError2, VaultErrorCode as VaultErrorCode2, VaultImportError as VaultImportError2, VaultImportErrorCode as VaultImportErrorCode2 } from "@vultisig/sdk";
@@ -6560,8 +6604,7 @@ function nowMs() {
6560
6604
  function journalPath() {
6561
6605
  const explicit = process.env.VULTISIG_BROADCAST_JOURNAL_PATH;
6562
6606
  if (explicit && explicit.trim()) return explicit;
6563
- const dir = process.env.VULTISIG_CONFIG_DIR && process.env.VULTISIG_CONFIG_DIR.trim() ? process.env.VULTISIG_CONFIG_DIR : join(homedir(), ".vultisig");
6564
- return join(dir, "broadcasts.jsonl");
6607
+ return join(getVultisigConfigDir(), "broadcasts.jsonl");
6565
6608
  }
6566
6609
  function normalize(v, canonicalizeEmptyCalldata = false) {
6567
6610
  const normalized = (v ?? "").trim().toLowerCase();
@@ -7289,7 +7332,7 @@ async function discoverTokens(ctx2, chain) {
7289
7332
  });
7290
7333
  }
7291
7334
  const allTokens = vault.getTokens(chain);
7292
- spinner.succeed(`Discovered ${newTokens.length} new token(s) on ${chain}`);
7335
+ spinner.succeed(`Now tracking ${newTokens.length} new token(s) on ${chain}`);
7293
7336
  if (isJsonOutput()) {
7294
7337
  outputJson({
7295
7338
  chain,
@@ -7306,6 +7349,9 @@ async function discoverTokens(ctx2, chain) {
7306
7349
  printResult(` ${d.ticker} (${d.contractAddress})`);
7307
7350
  }
7308
7351
  info(chalk4.gray(`
7352
+ Saved to this vault \u2014 tracked tokens count toward portfolio and balance --tokens.`));
7353
+ info(chalk4.gray(`Use --remove <tokenId> to stop tracking one.`));
7354
+ info(chalk4.gray(`
7309
7355
  ${allTokens.length} total token(s) tracked on ${chain}`));
7310
7356
  }
7311
7357
  async function listTokens(ctx2, chain) {
@@ -7378,6 +7424,51 @@ async function executeSend(ctx2, params) {
7378
7424
  }
7379
7425
  return sendTransaction(vault, params);
7380
7426
  }
7427
+ async function previewDryRun(vault, params, dryResult, to, destinationTag) {
7428
+ const balance = await vault.balance(params.chain, params.tokenId);
7429
+ const hasInsufficientBalance = parseFloat(dryResult.total) > parseFloat(balance.formattedAmount);
7430
+ const isTokenSend = balance.tokenId !== void 0;
7431
+ const feeBalance = isTokenSend ? await vault.balance(params.chain).catch(() => void 0) : void 0;
7432
+ const warnings = [];
7433
+ if (hasInsufficientBalance) {
7434
+ warnings.push(`Insufficient balance: you have ${balance.formattedAmount} ${balance.symbol}`);
7435
+ }
7436
+ if (isTokenSend && feeBalance === void 0) {
7437
+ warnings.push(`Could not check your ${dryResult.feeSymbol} balance for the network fee`);
7438
+ } else if (feeBalance && parseFloat(dryResult.fee) > parseFloat(feeBalance.formattedAmount)) {
7439
+ warnings.push(
7440
+ `Insufficient ${dryResult.feeSymbol} for the network fee: you have ${feeBalance.formattedAmount} ${dryResult.feeSymbol}, the fee is ${dryResult.fee}`
7441
+ );
7442
+ }
7443
+ const result = {
7444
+ dryRun: true,
7445
+ chain: params.chain,
7446
+ to,
7447
+ amount: params.amount,
7448
+ symbol: balance.symbol,
7449
+ fee: dryResult.fee,
7450
+ feeSymbol: dryResult.feeSymbol,
7451
+ total: dryResult.total,
7452
+ balance: balance.formattedAmount,
7453
+ destinationTag,
7454
+ ...warnings.length > 0 ? { warning: warnings.join(". ") } : {}
7455
+ };
7456
+ if (isJsonOutput()) {
7457
+ outputJson(result);
7458
+ return result;
7459
+ }
7460
+ info(`
7461
+ Dry-run preview:`);
7462
+ info(` Chain: ${result.chain}`);
7463
+ info(` To: ${result.to}`);
7464
+ info(` Amount: ${result.amount} ${result.symbol}`);
7465
+ if (result.destinationTag !== void 0) info(` Destination tag: ${result.destinationTag}`);
7466
+ info(` Fee: ${result.fee} ${result.feeSymbol}`);
7467
+ info(` Total: ${result.total} ${result.symbol}`);
7468
+ info(` Balance: ${result.balance} ${result.symbol}`);
7469
+ if (result.warning) warn(` Warning: ${result.warning}`);
7470
+ return result;
7471
+ }
7381
7472
  async function sendTransaction(vault, params) {
7382
7473
  if (!params.dryRun && !params.yes && isNonInteractive()) {
7383
7474
  throw new ConfirmationRequiredError(
@@ -7406,37 +7497,7 @@ async function sendTransaction(vault, params) {
7406
7497
  prepareSpinner.succeed("Transaction prepared");
7407
7498
  if (!dryResult.dryRun) throw new Error("unreachable");
7408
7499
  if (params.dryRun) {
7409
- const balance2 = await vault.balance(params.chain, params.tokenId);
7410
- const hasInsufficientBalance = parseFloat(dryResult.total) > parseFloat(balance2.formattedAmount);
7411
- const result = {
7412
- dryRun: true,
7413
- chain: params.chain,
7414
- to,
7415
- amount: params.amount,
7416
- symbol: balance2.symbol,
7417
- fee: dryResult.fee,
7418
- total: dryResult.total,
7419
- balance: balance2.formattedAmount,
7420
- destinationTag
7421
- };
7422
- if (hasInsufficientBalance) {
7423
- result.warning = `Insufficient balance: you have ${balance2.formattedAmount} ${balance2.symbol}`;
7424
- }
7425
- if (isJsonOutput()) {
7426
- outputJson(result);
7427
- } else {
7428
- info(`
7429
- Dry-run preview:`);
7430
- info(` Chain: ${result.chain}`);
7431
- info(` To: ${result.to}`);
7432
- info(` Amount: ${result.amount} ${result.symbol}`);
7433
- if (result.destinationTag !== void 0) info(` Destination tag: ${result.destinationTag}`);
7434
- info(` Fee: ${result.fee} ${result.symbol}`);
7435
- info(` Total: ${result.total} ${result.symbol}`);
7436
- info(` Balance: ${result.balance} ${result.symbol}`);
7437
- if (result.warning) warn(` Warning: ${result.warning}`);
7438
- }
7439
- return result;
7500
+ return previewDryRun(vault, params, dryResult, to, destinationTag);
7440
7501
  }
7441
7502
  let gas;
7442
7503
  try {
@@ -8890,1337 +8951,1485 @@ Please specify more characters of the vault ID.`
8890
8951
  throw new Error(`Vault not found: "${idOrName}"`);
8891
8952
  }
8892
8953
 
8893
- // src/commands/swap.ts
8894
- async function executeSwapChains(ctx2) {
8895
- const vault = await ctx2.ensureActiveVault();
8896
- const spinner = createSpinner("Loading supported swap chains...");
8897
- const chains = await vault.getSupportedSwapChains();
8898
- spinner.succeed("Swap chains loaded");
8899
- if (isJsonOutput()) {
8900
- outputJson({ swapChains: [...chains] });
8901
- return chains;
8902
- }
8903
- displaySwapChains(chains);
8904
- return chains;
8905
- }
8906
- async function executeSwapQuote(ctx2, options) {
8907
- const vault = await ctx2.ensureActiveVault();
8908
- const isMax = options.amount === "max";
8909
- if (!isMax && (isNaN(options.amount) || options.amount <= 0)) {
8910
- throw new Error("Invalid amount");
8911
- }
8912
- const spinner = createSpinner("Getting swap quote...");
8913
- const result = await vault.swap({
8914
- fromChain: options.fromChain,
8915
- fromSymbol: options.fromToken || "",
8916
- toChain: options.toChain,
8917
- toSymbol: options.toToken || "",
8918
- amount: isMax ? "max" : String(options.amount),
8919
- dryRun: true
8920
- });
8921
- if (!result.dryRun) throw new Error("unreachable");
8922
- spinner.succeed("Quote received");
8923
- const quote = result.quote;
8924
- const fromAmountDisplay = isMax ? `${formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals)} (max)` : String(options.amount);
8925
- if (isJsonOutput()) {
8926
- outputJson({
8927
- fromChain: options.fromChain,
8928
- toChain: options.toChain,
8929
- amount: isMax ? "max" : options.amount,
8930
- isMax,
8931
- quote
8932
- });
8933
- return quote;
8934
- }
8935
- const feeBalance = await vault.balance(options.fromChain);
8936
- const discountTier = await vault.getDiscountTier();
8937
- displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
8938
- fromDecimals: quote.fromCoin.decimals,
8939
- toDecimals: quote.toCoin.decimals,
8940
- feeDecimals: feeBalance.decimals,
8941
- feeSymbol: feeBalance.symbol,
8942
- discountTier
8943
- });
8944
- info('\nTo execute this swap, use the "swap" command');
8945
- return quote;
8946
- }
8947
- function validateSwapAmount(amount) {
8948
- if (amount === "max") return;
8949
- if (isNaN(amount) || amount <= 0) throw new Error("Invalid amount");
8950
- }
8951
- function getSwapAmountString(amount) {
8952
- return amount === "max" ? "max" : String(amount);
8953
- }
8954
- function toSwapRequest(options, amount, dryRun) {
8955
- return {
8956
- fromChain: options.fromChain,
8957
- fromSymbol: options.fromToken || "",
8958
- toChain: options.toChain,
8959
- toSymbol: options.toToken || "",
8960
- amount,
8961
- ...options.slippage !== void 0 && { slippageTolerance: options.slippage },
8962
- ...dryRun && { dryRun: true }
8963
- };
8964
- }
8965
- function toDryRunResult(options, quote, fromAmountRaw) {
8966
- const result = {
8967
- dryRun: true,
8968
- fromChain: String(options.fromChain),
8969
- fromToken: quote.fromCoin.ticker,
8970
- toChain: String(options.toChain),
8971
- toToken: quote.toCoin.ticker,
8972
- inputAmount: fromAmountRaw,
8973
- ...options.amount === "max" && { isMax: true },
8974
- estimatedOutput: formatBigintAmount(quote.estimatedOutput, quote.toCoin.decimals),
8975
- provider: quote.provider
8976
- };
8977
- if (quote.estimatedOutputFiat != null) result.estimatedOutputFiat = parseFloat(quote.estimatedOutputFiat.toFixed(2));
8978
- if (quote.requiresApproval) result.requiresApproval = true;
8979
- if (quote.warnings?.length) result.warnings = [...quote.warnings];
8980
- return result;
8954
+ // ../../node_modules/viem/_esm/accounts/utils/publicKeyToAddress.js
8955
+ init_getAddress();
8956
+ init_keccak256();
8957
+ function publicKeyToAddress(publicKey) {
8958
+ const address = keccak256(`0x${publicKey.substring(4)}`).substring(26);
8959
+ return checksumAddress(`0x${address}`);
8981
8960
  }
8982
- function displayDryRunResult(result) {
8983
- info(`
8984
- Dry-run preview:`);
8985
- info(` From: ${result.inputAmount} ${result.fromToken} (${result.fromChain})`);
8986
- info(` To: ${result.estimatedOutput} ${result.toToken} (${result.toChain})`);
8987
- info(` Provider: ${result.provider}`);
8988
- if (result.estimatedOutputFiat != null) info(` Est. value (USD): $${result.estimatedOutputFiat}`);
8989
- if (result.requiresApproval) info(` Requires approval: yes`);
8990
- if (result.warnings?.length) result.warnings.forEach((w) => warn(` Warning: ${w}`));
8961
+
8962
+ // ../../node_modules/viem/_esm/utils/signature/recoverPublicKey.js
8963
+ init_isHex();
8964
+ init_size();
8965
+ init_fromHex();
8966
+ init_toHex();
8967
+ async function recoverPublicKey({ hash, signature }) {
8968
+ const hashHex = isHex(hash) ? hash : toHex(hash);
8969
+ const { secp256k1: secp256k12 } = await Promise.resolve().then(() => (init_secp256k1(), secp256k1_exports));
8970
+ const signature_ = (() => {
8971
+ if (typeof signature === "object" && "r" in signature && "s" in signature) {
8972
+ const { r, s, v, yParity } = signature;
8973
+ const yParityOrV2 = Number(yParity ?? v);
8974
+ const recoveryBit2 = toRecoveryBit(yParityOrV2);
8975
+ return new secp256k12.Signature(hexToBigInt(r), hexToBigInt(s)).addRecoveryBit(recoveryBit2);
8976
+ }
8977
+ const signatureHex = isHex(signature) ? signature : toHex(signature);
8978
+ if (size(signatureHex) !== 65)
8979
+ throw new Error("invalid signature length");
8980
+ const yParityOrV = hexToNumber(`0x${signatureHex.slice(130)}`);
8981
+ const recoveryBit = toRecoveryBit(yParityOrV);
8982
+ return secp256k12.Signature.fromCompact(signatureHex.substring(2, 130)).addRecoveryBit(recoveryBit);
8983
+ })();
8984
+ const publicKey = signature_.recoverPublicKey(hashHex.substring(2)).toHex(false);
8985
+ return `0x${publicKey}`;
8991
8986
  }
8992
- function refuseSwapWhenNonInteractive() {
8993
- throw new ConfirmationRequiredError(
8994
- "Swap requires confirmation.",
8995
- "Pass --yes to confirm, or --dry-run to preview without signing."
8996
- );
8987
+ function toRecoveryBit(yParityOrV) {
8988
+ if (yParityOrV === 0 || yParityOrV === 1)
8989
+ return yParityOrV;
8990
+ if (yParityOrV === 27)
8991
+ return 0;
8992
+ if (yParityOrV === 28)
8993
+ return 1;
8994
+ throw new Error("Invalid yParityOrV value");
8997
8995
  }
8998
- async function confirmSwapIfNeeded(options) {
8999
- if (options.yes) return;
9000
- if (isNonInteractive()) {
9001
- refuseSwapWhenNonInteractive();
9002
- }
9003
- const confirmed = await confirmSwap();
9004
- if (!confirmed) {
9005
- throw new ConfirmationRequiredError("Swap declined at the confirmation prompt");
9006
- }
8996
+
8997
+ // ../../node_modules/viem/_esm/utils/signature/recoverAddress.js
8998
+ async function recoverAddress({ hash, signature }) {
8999
+ return publicKeyToAddress(await recoverPublicKey({ hash, signature }));
9007
9000
  }
9008
- async function executeSwap(ctx2, options) {
9009
- const vault = await ctx2.ensureActiveVault();
9010
- validateSwapAmount(options.amount);
9011
- const amountStr = getSwapAmountString(options.amount);
9012
- if (!options.dryRun && !options.yes && isNonInteractive()) {
9013
- refuseSwapWhenNonInteractive();
9001
+
9002
+ // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
9003
+ init_encodeAbiParameters();
9004
+ init_concat();
9005
+ init_toHex();
9006
+ init_keccak256();
9007
+
9008
+ // ../../node_modules/viem/_esm/utils/typedData.js
9009
+ init_abi();
9010
+ init_address();
9011
+
9012
+ // ../../node_modules/viem/_esm/errors/typedData.js
9013
+ init_stringify();
9014
+ init_base();
9015
+ var InvalidDomainError = class extends BaseError {
9016
+ constructor({ domain }) {
9017
+ super(`Invalid domain "${stringify(domain)}".`, {
9018
+ metaMessages: ["Must be a valid EIP-712 domain."]
9019
+ });
9014
9020
  }
9015
- const quoteSpinner = createSpinner("Getting swap quote...");
9016
- const dryResult = await vault.swap(toSwapRequest(options, amountStr, true));
9017
- if (!dryResult.dryRun) throw new Error("unreachable");
9018
- quoteSpinner.succeed("Quote received");
9019
- const quote = dryResult.quote;
9020
- const fromAmountRaw = options.amount === "max" ? formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals) : String(options.amount);
9021
- const fromAmountDisplay = options.amount === "max" ? `${fromAmountRaw} (max)` : fromAmountRaw;
9022
- if (options.dryRun) {
9023
- const result = toDryRunResult(options, quote, fromAmountRaw);
9024
- if (isJsonOutput()) {
9025
- outputJson(result);
9026
- } else {
9027
- displayDryRunResult(result);
9028
- }
9029
- return result;
9021
+ };
9022
+ var InvalidPrimaryTypeError = class extends BaseError {
9023
+ constructor({ primaryType, types }) {
9024
+ super(`Invalid primary type \`${primaryType}\` must be one of \`${JSON.stringify(Object.keys(types))}\`.`, {
9025
+ docsPath: "/api/glossary/Errors#typeddatainvalidprimarytypeerror",
9026
+ metaMessages: ["Check that the primary type is a key in `types`."]
9027
+ });
9030
9028
  }
9031
- const feeBalance = await vault.balance(options.fromChain);
9032
- const discountTier = await vault.getDiscountTier();
9033
- if (!isJsonOutput()) {
9034
- displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9035
- fromDecimals: quote.fromCoin.decimals,
9036
- toDecimals: quote.toCoin.decimals,
9037
- feeDecimals: feeBalance.decimals,
9038
- feeSymbol: feeBalance.symbol,
9039
- discountTier
9029
+ };
9030
+ var InvalidStructTypeError = class extends BaseError {
9031
+ constructor({ type }) {
9032
+ super(`Struct type "${type}" is invalid.`, {
9033
+ metaMessages: ["Struct type must not be a Solidity type."],
9034
+ name: "InvalidStructTypeError"
9040
9035
  });
9041
9036
  }
9042
- await confirmSwapIfNeeded(options);
9043
- await ensureVaultUnlocked(vault, options.password);
9044
- const intent = buildSwapBroadcastIntent(vault, {
9045
- fromChain: options.fromChain,
9046
- toChain: options.toChain,
9047
- fromToken: options.fromToken,
9048
- toToken: options.toToken,
9049
- amount: fromAmountRaw,
9050
- isMax: options.amount === "max"
9051
- });
9052
- let signSpinner;
9053
- try {
9054
- const broadcast = await guardedBroadcast(intent, options.force ?? false, async () => {
9055
- signSpinner = createSpinner("Signing swap transaction...");
9056
- vault.on("signingProgress", ({ step }) => {
9057
- if (signSpinner) signSpinner.text = `${step.message} (${step.progress}%)`;
9058
- });
9059
- const result = await vault.swap(toSwapRequest(options, amountStr));
9060
- if (result.dryRun) throw new Error("unreachable");
9061
- return result;
9062
- });
9063
- signSpinner?.succeed(`Swap broadcast: ${broadcast.txHash}`);
9064
- if (isJsonOutput()) {
9065
- outputJson({
9066
- txHash: broadcast.txHash,
9067
- fromChain: options.fromChain,
9068
- toChain: options.toChain,
9069
- quote
9070
- });
9071
- } else {
9072
- displaySwapResult(options.fromChain, options.toChain, broadcast.txHash, quote, quote.toCoin.decimals);
9037
+ };
9038
+
9039
+ // ../../node_modules/viem/_esm/utils/typedData.js
9040
+ init_isAddress();
9041
+ init_size();
9042
+ init_toHex();
9043
+ init_regex();
9044
+ function validateTypedData(parameters) {
9045
+ const { domain, message, primaryType, types } = parameters;
9046
+ const validateData = (struct, data) => {
9047
+ for (const param of struct) {
9048
+ const { name, type } = param;
9049
+ const value = data[name];
9050
+ const integerMatch = type.match(integerRegex);
9051
+ if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
9052
+ const [_type, base, size_] = integerMatch;
9053
+ numberToHex(value, {
9054
+ signed: base === "int",
9055
+ size: Number.parseInt(size_, 10) / 8
9056
+ });
9057
+ }
9058
+ if (type === "address" && typeof value === "string" && !isAddress(value))
9059
+ throw new InvalidAddressError2({ address: value });
9060
+ const bytesMatch = type.match(bytesRegex);
9061
+ if (bytesMatch) {
9062
+ const [_type, size_] = bytesMatch;
9063
+ if (size_ && size(value) !== Number.parseInt(size_, 10))
9064
+ throw new BytesSizeMismatchError({
9065
+ expectedSize: Number.parseInt(size_, 10),
9066
+ givenSize: size(value)
9067
+ });
9068
+ }
9069
+ const struct2 = types[type];
9070
+ if (struct2) {
9071
+ validateReference(type);
9072
+ validateData(struct2, value);
9073
+ }
9073
9074
  }
9074
- return { txHash: broadcast.txHash, quote };
9075
- } catch (err) {
9076
- signSpinner?.stop();
9077
- throw err;
9078
- } finally {
9079
- vault.removeAllListeners("signingProgress");
9075
+ };
9076
+ if (types.EIP712Domain && domain) {
9077
+ if (typeof domain !== "object")
9078
+ throw new InvalidDomainError({ domain });
9079
+ validateData(types.EIP712Domain, domain);
9080
+ }
9081
+ if (primaryType !== "EIP712Domain") {
9082
+ if (types[primaryType])
9083
+ validateData(types[primaryType], message);
9084
+ else
9085
+ throw new InvalidPrimaryTypeError({ primaryType, types });
9080
9086
  }
9081
9087
  }
9088
+ function getTypesForEIP712Domain({ domain }) {
9089
+ return [
9090
+ typeof domain?.name === "string" && { name: "name", type: "string" },
9091
+ domain?.version && { name: "version", type: "string" },
9092
+ (typeof domain?.chainId === "number" || typeof domain?.chainId === "bigint") && {
9093
+ name: "chainId",
9094
+ type: "uint256"
9095
+ },
9096
+ domain?.verifyingContract && {
9097
+ name: "verifyingContract",
9098
+ type: "address"
9099
+ },
9100
+ domain?.salt && { name: "salt", type: "bytes32" }
9101
+ ].filter(Boolean);
9102
+ }
9103
+ function validateReference(type) {
9104
+ if (type === "address" || type === "bool" || type === "string" || type.startsWith("bytes") || type.startsWith("uint") || type.startsWith("int"))
9105
+ throw new InvalidStructTypeError({ type });
9106
+ }
9082
9107
 
9083
- // src/commands/settings.ts
9084
- import { Chain as Chain9, fiatCurrencies as fiatCurrencies2, fiatCurrencyNameRecord as fiatCurrencyNameRecord3 } from "@vultisig/sdk";
9085
- import chalk6 from "chalk";
9086
- async function executeCurrency(ctx2, newCurrency) {
9087
- const vault = await ctx2.ensureActiveVault();
9088
- if (!newCurrency) {
9089
- const currentCurrency = vault.currency;
9090
- const currencyName2 = fiatCurrencyNameRecord3[currentCurrency];
9091
- printResult(chalk6.cyan("\nCurrent Currency Preference:"));
9092
- printResult(` ${chalk6.green(currentCurrency.toUpperCase())} - ${currencyName2}`);
9093
- info(chalk6.gray(`
9094
- Supported currencies: ${fiatCurrencies2.join(", ")}`));
9095
- info(chalk6.gray('Use "vultisig currency <code>" to change'));
9096
- return currentCurrency;
9097
- }
9098
- const currency = newCurrency.toLowerCase();
9099
- if (!fiatCurrencies2.includes(currency)) {
9100
- error(`x Invalid currency: ${newCurrency}`);
9101
- warn(`Supported currencies: ${fiatCurrencies2.join(", ")}`);
9102
- throw new Error("Invalid currency");
9108
+ // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
9109
+ function hashTypedData(parameters) {
9110
+ const { domain = {}, message, primaryType } = parameters;
9111
+ const types = {
9112
+ EIP712Domain: getTypesForEIP712Domain({ domain }),
9113
+ ...parameters.types
9114
+ };
9115
+ validateTypedData({
9116
+ domain,
9117
+ message,
9118
+ primaryType,
9119
+ types
9120
+ });
9121
+ const parts = ["0x1901"];
9122
+ if (domain)
9123
+ parts.push(hashDomain({
9124
+ domain,
9125
+ types
9126
+ }));
9127
+ if (primaryType !== "EIP712Domain")
9128
+ parts.push(hashStruct({
9129
+ data: message,
9130
+ primaryType,
9131
+ types
9132
+ }));
9133
+ return keccak256(concat(parts));
9134
+ }
9135
+ function hashDomain({ domain, types }) {
9136
+ return hashStruct({
9137
+ data: domain,
9138
+ primaryType: "EIP712Domain",
9139
+ types
9140
+ });
9141
+ }
9142
+ function hashStruct({ data, primaryType, types }) {
9143
+ const encoded = encodeData({
9144
+ data,
9145
+ primaryType,
9146
+ types
9147
+ });
9148
+ return keccak256(encoded);
9149
+ }
9150
+ function encodeData({ data, primaryType, types }) {
9151
+ const encodedTypes = [{ type: "bytes32" }];
9152
+ const encodedValues = [hashType({ primaryType, types })];
9153
+ for (const field of types[primaryType]) {
9154
+ const [type, value] = encodeField({
9155
+ types,
9156
+ name: field.name,
9157
+ type: field.type,
9158
+ value: data[field.name]
9159
+ });
9160
+ encodedTypes.push(type);
9161
+ encodedValues.push(value);
9103
9162
  }
9104
- const spinner = createSpinner("Updating currency preference...");
9105
- await vault.setCurrency(currency);
9106
- spinner.succeed("Currency updated");
9107
- const currencyName = fiatCurrencyNameRecord3[currency];
9108
- if (isJsonOutput()) {
9109
- outputJson({ currency, name: currencyName, updated: true });
9110
- return currency;
9163
+ return encodeAbiParameters(encodedTypes, encodedValues);
9164
+ }
9165
+ function hashType({ primaryType, types }) {
9166
+ const encodedHashType = toHex(encodeType({ primaryType, types }));
9167
+ return keccak256(encodedHashType);
9168
+ }
9169
+ function encodeType({ primaryType, types }) {
9170
+ let result = "";
9171
+ const unsortedDeps = findTypeDependencies({ primaryType, types });
9172
+ unsortedDeps.delete(primaryType);
9173
+ const deps = [primaryType, ...Array.from(unsortedDeps).sort()];
9174
+ for (const type of deps) {
9175
+ result += `${type}(${types[type].map(({ name, type: t }) => `${t} ${name}`).join(",")})`;
9111
9176
  }
9112
- success(`
9113
- + Currency preference set to ${currency.toUpperCase()} (${currencyName})`);
9114
- return currency;
9177
+ return result;
9115
9178
  }
9116
- async function executeServer(ctx2) {
9117
- const spinner = createSpinner("Checking server status...");
9118
- try {
9119
- const status = await ctx2.sdk.getServerStatus();
9120
- spinner.succeed("Server status retrieved");
9121
- if (isJsonOutput()) {
9122
- outputJson({ server: status });
9123
- return status;
9124
- }
9125
- printResult(chalk6.cyan("\nServer Status:\n"));
9126
- printResult(chalk6.bold("Fast Vault Server:"));
9127
- printResult(` Online: ${status.fastVault.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9128
- if (status.fastVault.latency) {
9129
- printResult(` Latency: ${status.fastVault.latency}ms`);
9130
- }
9131
- printResult(chalk6.bold("\nMessage Relay:"));
9132
- printResult(` Online: ${status.messageRelay.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9133
- if (status.messageRelay.latency) {
9134
- printResult(` Latency: ${status.messageRelay.latency}ms`);
9135
- }
9136
- return status;
9137
- } catch (err) {
9138
- spinner.fail("Failed to check server status");
9139
- error(`
9140
- x ${err.message}`);
9141
- throw err;
9179
+ function findTypeDependencies({ primaryType: primaryType_, types }, results = /* @__PURE__ */ new Set()) {
9180
+ const match = primaryType_.match(/^\w*/u);
9181
+ const primaryType = match?.[0];
9182
+ if (results.has(primaryType) || types[primaryType] === void 0) {
9183
+ return results;
9142
9184
  }
9185
+ results.add(primaryType);
9186
+ for (const field of types[primaryType]) {
9187
+ findTypeDependencies({ primaryType: field.type, types }, results);
9188
+ }
9189
+ return results;
9143
9190
  }
9144
- async function executeAddressBook(ctx2, options = {}) {
9145
- if (options.add) {
9146
- let chain = options.chain;
9147
- let address = options.address;
9148
- let name = options.name;
9149
- const prompts = [];
9150
- if (!chain) {
9151
- prompts.push({
9152
- type: "select",
9153
- name: "chain",
9154
- message: "Select chain:",
9155
- choices: Object.values(Chain9)
9156
- });
9157
- }
9158
- if (!address) {
9159
- prompts.push({
9160
- type: "input",
9161
- name: "address",
9162
- message: "Enter address:",
9163
- validate: (input) => input.trim() !== "" || "Address is required"
9164
- });
9165
- }
9166
- if (!name) {
9167
- prompts.push({
9168
- type: "input",
9169
- name: "name",
9170
- message: "Enter name/label:",
9171
- validate: (input) => input.trim() !== "" || "Name is required"
9172
- });
9173
- }
9174
- if (prompts.length > 0) {
9175
- const answers = await prompt(prompts);
9176
- chain = chain || answers.chain;
9177
- address = address || answers.address?.trim();
9178
- name = name || answers.name?.trim();
9179
- }
9180
- const spinner2 = createSpinner("Adding address to address book...");
9181
- const entry = {
9182
- chain,
9183
- address,
9184
- name,
9185
- source: "saved",
9186
- dateAdded: Date.now()
9187
- };
9188
- await ctx2.sdk.addAddressBookEntry([entry]);
9189
- spinner2.succeed("Address added");
9190
- if (isJsonOutput()) {
9191
- outputJson({ added: entry });
9192
- return [];
9193
- }
9194
- success(`
9195
- + Added ${name} (${chain}: ${address})`);
9196
- return [];
9197
- }
9198
- if (options.remove) {
9199
- const spinner2 = createSpinner("Removing address from address book...");
9200
- await ctx2.sdk.removeAddressBookEntry([{ address: options.remove, chain: options.chain }]);
9201
- spinner2.succeed("Address removed");
9202
- if (isJsonOutput()) {
9203
- outputJson({ removed: { address: options.remove, chain: options.chain } });
9204
- return [];
9205
- }
9206
- success(`
9207
- + Removed ${options.remove}`);
9208
- return [];
9209
- }
9210
- const spinner = createSpinner("Loading address book...");
9211
- const addressBook = await ctx2.sdk.getAddressBook(options.chain);
9212
- spinner.succeed("Address book loaded");
9213
- const allEntries = [...addressBook.saved, ...addressBook.vaults];
9214
- if (isJsonOutput()) {
9215
- outputJson({ addressBook: allEntries, chain: options.chain });
9216
- return allEntries;
9191
+ function encodeField({ types, name, type, value }) {
9192
+ if (types[type] !== void 0) {
9193
+ return [
9194
+ { type: "bytes32" },
9195
+ keccak256(encodeData({ data: value, primaryType: type, types }))
9196
+ ];
9217
9197
  }
9218
- if (allEntries.length === 0) {
9219
- warn(`
9220
- No addresses in address book${options.chain ? ` for ${options.chain}` : ""}`);
9221
- info(chalk6.gray("\nUse --add to add an address to the address book"));
9222
- } else {
9223
- printResult(chalk6.cyan(`
9224
- Address Book${options.chain ? ` (${options.chain})` : ""}:
9225
- `));
9226
- const table = allEntries.map((entry) => ({
9227
- Name: entry.name,
9228
- Chain: entry.chain,
9229
- Address: entry.address,
9230
- Source: entry.source
9198
+ if (type === "bytes")
9199
+ return [{ type: "bytes32" }, keccak256(value)];
9200
+ if (type === "string")
9201
+ return [{ type: "bytes32" }, keccak256(toHex(value))];
9202
+ if (type.lastIndexOf("]") === type.length - 1) {
9203
+ const parsedType = type.slice(0, type.lastIndexOf("["));
9204
+ const typeValuePairs = value.map((item) => encodeField({
9205
+ name,
9206
+ type: parsedType,
9207
+ types,
9208
+ value: item
9231
9209
  }));
9232
- printTable(table);
9233
- info(chalk6.gray("\nUse --add to add or --remove <address> to remove an address"));
9210
+ return [
9211
+ { type: "bytes32" },
9212
+ keccak256(encodeAbiParameters(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v)))
9213
+ ];
9234
9214
  }
9235
- return allEntries;
9215
+ return [{ type }, value];
9236
9216
  }
9237
9217
 
9238
- // src/commands/rujira.ts
9239
- import {
9240
- getRoutesSummary,
9241
- listEasyRoutes,
9242
- RujiraClient,
9243
- VultisigRujiraProvider
9244
- } from "@vultisig/rujira";
9245
- async function createRujiraClient(ctx2, options = {}) {
9246
- const vault = await ctx2.ensureActiveVault();
9247
- const provider = new VultisigRujiraProvider(vault);
9248
- const client = new RujiraClient({
9249
- signer: provider,
9250
- rpcEndpoint: options.rpcEndpoint,
9251
- config: {
9252
- // Allow overriding rest endpoint via config (used for thornode calls)
9253
- ...options.restEndpoint ? { restEndpoint: options.restEndpoint } : {}
9218
+ // ../../node_modules/viem/_esm/errors/unit.js
9219
+ init_base();
9220
+ var InvalidDecimalNumberError = class extends BaseError {
9221
+ constructor({ value }) {
9222
+ super(`Number \`${value}\` is not a valid decimal number.`, {
9223
+ name: "InvalidDecimalNumberError"
9224
+ });
9225
+ }
9226
+ };
9227
+
9228
+ // ../../node_modules/viem/_esm/utils/unit/parseUnits.js
9229
+ function parseUnits(value, decimals) {
9230
+ if (!/^(-?)([0-9]*)\.?([0-9]*)$/.test(value))
9231
+ throw new InvalidDecimalNumberError({ value });
9232
+ let [integer, fraction = "0"] = value.split(".");
9233
+ const negative = integer.startsWith("-");
9234
+ if (negative)
9235
+ integer = integer.slice(1);
9236
+ fraction = fraction.replace(/(0+)$/, "");
9237
+ if (decimals === 0) {
9238
+ if (Math.round(Number(`.${fraction}`)) === 1)
9239
+ integer = `${BigInt(integer) + 1n}`;
9240
+ fraction = "";
9241
+ } else if (fraction.length > decimals) {
9242
+ const [left, unit, right] = [
9243
+ fraction.slice(0, decimals - 1),
9244
+ fraction.slice(decimals - 1, decimals),
9245
+ fraction.slice(decimals)
9246
+ ];
9247
+ const rounded = Math.round(Number(`${unit}.${right}`));
9248
+ if (rounded > 9)
9249
+ fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, "0");
9250
+ else
9251
+ fraction = `${left}${rounded}`;
9252
+ if (fraction.length > decimals) {
9253
+ fraction = fraction.slice(1);
9254
+ integer = `${BigInt(integer) + 1n}`;
9254
9255
  }
9255
- });
9256
- const spinner = createSpinner("Connecting to Rujira/THORChain...");
9257
- await client.connect();
9258
- spinner.succeed("Connected");
9259
- return client;
9256
+ fraction = fraction.slice(0, decimals);
9257
+ } else {
9258
+ fraction = fraction.padEnd(decimals, "0");
9259
+ }
9260
+ return BigInt(`${negative ? "-" : ""}${integer}${fraction}`);
9260
9261
  }
9261
- async function executeRujiraBalance(ctx2, options = {}) {
9262
- const vault = await ctx2.ensureActiveVault();
9263
- const thorAddress = await vault.address("THORChain");
9264
- const client = await createRujiraClient(ctx2, options);
9265
- const spinner = createSpinner("Loading THORChain balances...");
9266
- const balances = await client.deposit.getBalances(thorAddress);
9267
- spinner.succeed("Balances loaded");
9268
- const filtered = options.securedOnly ? balances.filter((b) => b.denom.includes("-") || b.denom.includes("/")) : balances;
9269
- if (isJsonOutput()) {
9270
- outputJson({ thorAddress, balances: filtered });
9271
- return;
9262
+
9263
+ // ../../node_modules/viem/_esm/index.js
9264
+ init_formatUnits();
9265
+
9266
+ // ../../packages/core/chain/dist/amount/toChainAmount.js
9267
+ var ChainAmountParseError = class extends Error {
9268
+ name = "ChainAmountParseError";
9269
+ constructor(message) {
9270
+ super(message);
9272
9271
  }
9273
- info(`THORChain address: ${thorAddress}`);
9274
- if (!filtered.length) {
9275
- printResult("No balances found");
9276
- return;
9272
+ };
9273
+ var SCIENTIFIC_DECIMAL = /^([+-]?)(?:(\d+)\.?(\d*)|\.(\d+))[eE]([+-]?\d+)$/i;
9274
+ var MAX_SCALE_ABS = 10000n;
9275
+ var padFractionDigits = (frac, totalLen) => {
9276
+ const need = totalLen - BigInt(frac.length);
9277
+ if (need <= 0n) {
9278
+ return frac;
9277
9279
  }
9278
- printTable(
9279
- filtered.map((b) => ({
9280
- asset: b.asset,
9281
- denom: b.denom,
9282
- amount: b.formatted,
9283
- raw: b.amount
9284
- }))
9285
- );
9286
- }
9287
- async function executeRujiraRoutes() {
9288
- const routes = listEasyRoutes();
9289
- const summary = getRoutesSummary();
9280
+ if (need <= BigInt(Number.MAX_SAFE_INTEGER)) {
9281
+ return `${"0".repeat(Number(need))}${frac}`;
9282
+ }
9283
+ let out = frac;
9284
+ while (BigInt(out.length) < totalLen) {
9285
+ out = `0${out}`;
9286
+ }
9287
+ return out;
9288
+ };
9289
+ var expandScientificNotationToDecimalString = (s) => {
9290
+ const m = SCIENTIFIC_DECIMAL.exec(s.trim());
9291
+ if (!m) {
9292
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9293
+ }
9294
+ const signNeg = m[1] === "-";
9295
+ let digitStr;
9296
+ let fracLen;
9297
+ if (m[4] !== void 0) {
9298
+ digitStr = m[4];
9299
+ fracLen = digitStr.length;
9300
+ } else {
9301
+ digitStr = `${m[2] ?? ""}${m[3] ?? ""}`;
9302
+ fracLen = (m[3] ?? "").length;
9303
+ }
9304
+ if (!/^\d+$/.test(digitStr)) {
9305
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9306
+ }
9307
+ const expStr = m[5] ?? "";
9308
+ if (expStr === "" || expStr === "+" || expStr === "-") {
9309
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9310
+ }
9311
+ const allDigits = BigInt(digitStr);
9312
+ const exp = BigInt(expStr);
9313
+ const scale = exp - BigInt(fracLen);
9314
+ const scaleAbs = scale < 0n ? -scale : scale;
9315
+ if (scaleAbs > MAX_SCALE_ABS) {
9316
+ throw new ChainAmountParseError(`Amount exponent out of supported range: "${s}"`);
9317
+ }
9318
+ let absResult;
9319
+ if (scale >= 0n) {
9320
+ const mult = 10n ** scale;
9321
+ absResult = (allDigits * mult).toString();
9322
+ } else {
9323
+ const k = -scale;
9324
+ const divisor = 10n ** k;
9325
+ const intPart = allDigits / divisor;
9326
+ const rem = allDigits % divisor;
9327
+ const frac = padFractionDigits(rem.toString(), k);
9328
+ absResult = intPart === 0n ? `0.${frac}` : `${intPart.toString()}.${frac}`;
9329
+ }
9330
+ if (signNeg && allDigits !== 0n) {
9331
+ return `-${absResult}`;
9332
+ }
9333
+ return absResult;
9334
+ };
9335
+ var formatNumberAmount = (amount) => {
9336
+ const str2 = amount.toString();
9337
+ return /[eE]/.test(str2) ? expandScientificNotationToDecimalString(str2) : str2;
9338
+ };
9339
+ var truncateToDecimals = (s, decimals) => {
9340
+ const dotIdx = s.indexOf(".");
9341
+ if (dotIdx === -1)
9342
+ return s;
9343
+ if (decimals === 0)
9344
+ return s.slice(0, dotIdx);
9345
+ const fracPart = s.slice(dotIdx + 1);
9346
+ if (fracPart.length <= decimals)
9347
+ return s;
9348
+ return `${s.slice(0, dotIdx + 1)}${fracPart.slice(0, decimals)}`;
9349
+ };
9350
+ var toChainAmount = (amount, decimals) => {
9351
+ if (typeof amount === "string") {
9352
+ const trimmed = amount.trim();
9353
+ if (!trimmed) {
9354
+ throw new ChainAmountParseError("Amount cannot be empty");
9355
+ }
9356
+ if (/[eE]/.test(trimmed)) {
9357
+ const expanded = expandScientificNotationToDecimalString(trimmed);
9358
+ return parseUnits(truncateToDecimals(expanded, decimals), decimals);
9359
+ }
9360
+ return parseUnits(truncateToDecimals(trimmed, decimals), decimals);
9361
+ }
9362
+ return parseUnits(truncateToDecimals(formatNumberAmount(amount), decimals), decimals);
9363
+ };
9364
+
9365
+ // src/commands/swap.ts
9366
+ async function executeSwapChains(ctx2) {
9367
+ const vault = await ctx2.ensureActiveVault();
9368
+ const spinner = createSpinner("Loading supported swap chains...");
9369
+ const chains = await vault.getSupportedSwapChains();
9370
+ spinner.succeed("Swap chains loaded");
9290
9371
  if (isJsonOutput()) {
9291
- outputJson({ routes, summary });
9292
- return;
9372
+ outputJson({ swapChains: [...chains] });
9373
+ return chains;
9293
9374
  }
9294
- printResult(summary);
9295
- printResult("");
9296
- printTable(
9297
- routes.map((r) => ({
9298
- name: r.name,
9299
- from: r.from,
9300
- to: r.to,
9301
- liquidity: r.liquidity,
9302
- description: r.description
9303
- }))
9304
- );
9375
+ displaySwapChains(chains);
9376
+ return chains;
9305
9377
  }
9306
- async function executeRujiraDeposit(ctx2, options = {}) {
9378
+ async function executeSwapQuote(ctx2, options) {
9379
+ const isMax = options.amount === "max";
9380
+ const amount = normalizeSwapAmount(options.amount);
9307
9381
  const vault = await ctx2.ensureActiveVault();
9308
- const thorAddress = await vault.address("THORChain");
9309
- const client = await createRujiraClient(ctx2, options);
9310
- if (!options.asset) {
9311
- const spinner2 = createSpinner("Loading THORChain inbound addresses...");
9312
- const inbound = await client.deposit.getInboundAddresses();
9313
- spinner2.succeed("Inbound addresses loaded");
9314
- if (isJsonOutput()) {
9315
- outputJson({ thorAddress, inboundAddresses: inbound });
9316
- return;
9317
- }
9318
- info(`THORChain address: ${thorAddress}`);
9319
- printResult("Provide an L1 asset to get a chain-specific inbound address + memo.");
9320
- printResult("Example: vultisig rujira deposit --asset BTC.BTC --amount 100000");
9321
- printResult("");
9322
- printTable(
9323
- inbound.map((a) => ({
9324
- chain: a.chain,
9325
- address: a.address,
9326
- halted: a.halted,
9327
- globalTradingPaused: a.global_trading_paused,
9328
- chainTradingPaused: a.chain_trading_paused
9329
- }))
9330
- );
9331
- return;
9332
- }
9333
- const amount = options.amount ?? "1";
9334
- const spinner = createSpinner("Preparing deposit instructions...");
9335
- const prepared = await client.deposit.prepare({
9336
- fromAsset: options.asset,
9382
+ const spinner = createSpinner("Getting swap quote...");
9383
+ const result = await vault.swap({
9384
+ fromChain: options.fromChain,
9385
+ fromSymbol: options.fromToken || "",
9386
+ toChain: options.toChain,
9387
+ toSymbol: options.toToken || "",
9337
9388
  amount,
9338
- thorAddress,
9339
- affiliate: options.affiliate,
9340
- affiliateBps: options.affiliateBps
9389
+ dryRun: true
9341
9390
  });
9342
- spinner.succeed("Deposit prepared");
9391
+ if (!result.dryRun) throw new Error("unreachable");
9392
+ spinner.succeed("Quote received");
9393
+ const quote = result.quote;
9394
+ const semanticAmount = isMax ? amount : normalizeSwapAmount(amount, quote.fromCoin.decimals);
9395
+ const fromAmountDisplay = isMax ? `${formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals)} (max)` : semanticAmount;
9343
9396
  if (isJsonOutput()) {
9344
- outputJson({ thorAddress, deposit: prepared });
9345
- return;
9397
+ outputJson({
9398
+ fromChain: options.fromChain,
9399
+ toChain: options.toChain,
9400
+ amount: semanticAmount,
9401
+ isMax,
9402
+ quote
9403
+ });
9404
+ return quote;
9346
9405
  }
9347
- info(`THORChain address: ${thorAddress}`);
9348
- printResult("Deposit instructions (send from L1):");
9349
- printResult(` Chain: ${prepared.chain}`);
9350
- printResult(` Asset: ${prepared.asset}`);
9351
- printResult(` Inbound address:${prepared.inboundAddress}`);
9352
- printResult(` Memo: ${prepared.memo}`);
9353
- printResult(` Min amount: ${prepared.minimumAmount}`);
9354
- if (prepared.warning) {
9355
- warn(prepared.warning);
9406
+ const feeBalance = await vault.balance(options.fromChain);
9407
+ const discountTier = await vault.getDiscountTier();
9408
+ displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9409
+ fromDecimals: quote.fromCoin.decimals,
9410
+ toDecimals: quote.toCoin.decimals,
9411
+ feeDecimals: feeBalance.decimals,
9412
+ feeSymbol: feeBalance.symbol,
9413
+ discountTier
9414
+ });
9415
+ info('\nTo execute this swap, use the "swap" command');
9416
+ return quote;
9417
+ }
9418
+ var SWAP_AMOUNT_CANONICAL_DECIMALS = 1e4;
9419
+ function normalizeSwapAmount(amount, decimals = SWAP_AMOUNT_CANONICAL_DECIMALS) {
9420
+ if (amount === "max") return amount;
9421
+ try {
9422
+ const chainAmount = toChainAmount(amount, decimals);
9423
+ if (chainAmount <= 0n) throw new Error("Invalid amount");
9424
+ return formatUnits(chainAmount, decimals);
9425
+ } catch {
9426
+ throw new Error("Invalid amount");
9356
9427
  }
9357
9428
  }
9358
- async function executeRujiraSwap(ctx2, options) {
9429
+ function toSwapRequest(options, amount, dryRun) {
9430
+ return {
9431
+ fromChain: options.fromChain,
9432
+ fromSymbol: options.fromToken || "",
9433
+ toChain: options.toChain,
9434
+ toSymbol: options.toToken || "",
9435
+ amount,
9436
+ ...options.slippage !== void 0 && { slippageTolerance: options.slippage },
9437
+ ...dryRun && { dryRun: true }
9438
+ };
9439
+ }
9440
+ function toDryRunResult(options, quote, fromAmountRaw) {
9441
+ const result = {
9442
+ dryRun: true,
9443
+ fromChain: String(options.fromChain),
9444
+ fromToken: quote.fromCoin.ticker,
9445
+ toChain: String(options.toChain),
9446
+ toToken: quote.toCoin.ticker,
9447
+ inputAmount: fromAmountRaw,
9448
+ ...options.amount === "max" && { isMax: true },
9449
+ estimatedOutput: formatBigintAmount(quote.estimatedOutput, quote.toCoin.decimals),
9450
+ provider: quote.provider
9451
+ };
9452
+ if (quote.estimatedOutputFiat != null) result.estimatedOutputFiat = parseFloat(quote.estimatedOutputFiat.toFixed(2));
9453
+ if (quote.requiresApproval) result.requiresApproval = true;
9454
+ if (quote.warnings?.length) result.warnings = [...quote.warnings];
9455
+ return result;
9456
+ }
9457
+ function displayDryRunResult(result) {
9458
+ info(`
9459
+ Dry-run preview:`);
9460
+ info(` From: ${result.inputAmount} ${result.fromToken} (${result.fromChain})`);
9461
+ info(` To: ${result.estimatedOutput} ${result.toToken} (${result.toChain})`);
9462
+ info(` Provider: ${result.provider}`);
9463
+ if (result.estimatedOutputFiat != null) info(` Est. value (USD): $${result.estimatedOutputFiat}`);
9464
+ if (result.requiresApproval) info(` Requires approval: yes`);
9465
+ if (result.warnings?.length) result.warnings.forEach((w) => warn(` Warning: ${w}`));
9466
+ }
9467
+ function refuseSwapWhenNonInteractive() {
9468
+ throw new ConfirmationRequiredError(
9469
+ "Swap requires confirmation.",
9470
+ "Pass --yes to confirm, or --dry-run to preview without signing."
9471
+ );
9472
+ }
9473
+ async function confirmSwapIfNeeded(options) {
9474
+ if (options.yes) return;
9475
+ if (isNonInteractive()) {
9476
+ refuseSwapWhenNonInteractive();
9477
+ }
9478
+ const confirmed = await confirmSwap();
9479
+ if (!confirmed) {
9480
+ throw new ConfirmationRequiredError("Swap declined at the confirmation prompt");
9481
+ }
9482
+ }
9483
+ async function executeSwap(ctx2, options) {
9484
+ const amountStr = normalizeSwapAmount(options.amount);
9359
9485
  const vault = await ctx2.ensureActiveVault();
9360
9486
  if (!options.dryRun && !options.yes && isNonInteractive()) {
9361
- throw new ConfirmationRequiredError(
9362
- "Swap requires confirmation.",
9363
- "Pass --yes to confirm, or --dry-run to preview."
9364
- );
9487
+ refuseSwapWhenNonInteractive();
9365
9488
  }
9366
- const client = await createRujiraClient(ctx2, options);
9367
- const destination = options.destination ?? await vault.address("THORChain");
9368
- const quoteSpinner = createSpinner("Getting FIN swap quote...");
9369
- const quote = await client.swap.getQuote({
9370
- fromAsset: options.fromAsset,
9371
- toAsset: options.toAsset,
9372
- amount: options.amount,
9373
- destination,
9374
- slippageBps: options.slippageBps
9375
- });
9489
+ const quoteSpinner = createSpinner("Getting swap quote...");
9490
+ const dryResult = await vault.swap(toSwapRequest(options, amountStr, true));
9491
+ if (!dryResult.dryRun) throw new Error("unreachable");
9376
9492
  quoteSpinner.succeed("Quote received");
9493
+ const quote = dryResult.quote;
9494
+ const semanticAmount = options.amount === "max" ? amountStr : normalizeSwapAmount(amountStr, quote.fromCoin.decimals);
9495
+ const fromAmountRaw = options.amount === "max" ? formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals) : semanticAmount;
9496
+ const fromAmountDisplay = options.amount === "max" ? `${fromAmountRaw} (max)` : fromAmountRaw;
9377
9497
  if (options.dryRun) {
9498
+ const result = toDryRunResult(options, quote, fromAmountRaw);
9378
9499
  if (isJsonOutput()) {
9379
- outputJson({ dryRun: true, quote });
9500
+ outputJson(result);
9380
9501
  } else {
9381
- printResult("FIN Swap Preview (dry-run)");
9382
- printResult(` From: ${options.fromAsset}`);
9383
- printResult(` To: ${options.toAsset}`);
9384
- printResult(` Amount (in): ${options.amount}`);
9385
- printResult(` Expected out:${quote.expectedOutput}`);
9386
- printResult(` Min out: ${quote.minimumOutput}`);
9387
- printResult(` Contract: ${quote.contractAddress}`);
9502
+ displayDryRunResult(result);
9388
9503
  }
9389
- return;
9504
+ return result;
9390
9505
  }
9506
+ const feeBalance = await vault.balance(options.fromChain);
9507
+ const discountTier = await vault.getDiscountTier();
9391
9508
  if (!isJsonOutput()) {
9392
- printResult("FIN Swap Preview");
9393
- printResult(` From: ${options.fromAsset}`);
9394
- printResult(` To: ${options.toAsset}`);
9395
- printResult(` Amount (in): ${options.amount}`);
9396
- printResult(` Expected out:${quote.expectedOutput}`);
9397
- printResult(` Min out: ${quote.minimumOutput}`);
9398
- printResult(` Contract: ${quote.contractAddress}`);
9399
- if (quote.warning) {
9400
- warn(quote.warning);
9401
- }
9402
- }
9403
- if (!options.yes) {
9404
- warn("This command will execute a swap. Re-run with -y/--yes to skip this warning.");
9405
- throw new ConfirmationRequiredError(
9406
- "Swap requires confirmation.",
9407
- "Pass --yes to confirm, or --dry-run to preview."
9408
- );
9509
+ displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9510
+ fromDecimals: quote.fromCoin.decimals,
9511
+ toDecimals: quote.toCoin.decimals,
9512
+ feeDecimals: feeBalance.decimals,
9513
+ feeSymbol: feeBalance.symbol,
9514
+ discountTier
9515
+ });
9409
9516
  }
9517
+ await confirmSwapIfNeeded(options);
9410
9518
  await ensureVaultUnlocked(vault, options.password);
9411
- const execSpinner = createSpinner("Executing FIN swap...");
9412
- const result = await client.swap.execute(quote, { slippageBps: options.slippageBps });
9413
- execSpinner.succeed("Swap submitted");
9414
- if (isJsonOutput()) {
9415
- outputJson({ quote, result });
9416
- } else {
9417
- printResult(`Tx Hash: ${result.txHash}`);
9418
- }
9419
- }
9420
- async function executeRujiraWithdraw(ctx2, options) {
9421
- const vault = await ctx2.ensureActiveVault();
9422
- if (!options.dryRun && !options.yes && isNonInteractive()) {
9423
- throw new ConfirmationRequiredError(
9424
- "Withdrawal requires confirmation.",
9425
- "Pass --yes to confirm, or --dry-run to preview."
9426
- );
9427
- }
9428
- const client = await createRujiraClient(ctx2, options);
9429
- const prepSpinner = createSpinner("Preparing withdrawal (MsgDeposit)...");
9430
- const prepared = await client.withdraw.prepare({
9431
- asset: options.asset,
9432
- amount: options.amount,
9433
- l1Address: options.l1Address,
9434
- maxFeeBps: options.maxFeeBps
9519
+ const intent = buildSwapBroadcastIntent(vault, {
9520
+ fromChain: options.fromChain,
9521
+ toChain: options.toChain,
9522
+ fromToken: options.fromToken,
9523
+ toToken: options.toToken,
9524
+ amount: fromAmountRaw,
9525
+ isMax: options.amount === "max"
9435
9526
  });
9436
- prepSpinner.succeed("Withdrawal prepared");
9437
- if (options.dryRun) {
9527
+ let signSpinner;
9528
+ try {
9529
+ const broadcast = await guardedBroadcast(intent, options.force ?? false, async () => {
9530
+ signSpinner = createSpinner("Signing swap transaction...");
9531
+ vault.on("signingProgress", ({ step }) => {
9532
+ if (signSpinner) signSpinner.text = `${step.message} (${step.progress}%)`;
9533
+ });
9534
+ const result = await vault.swap(toSwapRequest(options, semanticAmount));
9535
+ if (result.dryRun) throw new Error("unreachable");
9536
+ return result;
9537
+ });
9538
+ signSpinner?.succeed(`Swap broadcast: ${broadcast.txHash}`);
9438
9539
  if (isJsonOutput()) {
9439
- outputJson({ dryRun: true, prepared });
9540
+ outputJson({
9541
+ txHash: broadcast.txHash,
9542
+ fromChain: options.fromChain,
9543
+ toChain: options.toChain,
9544
+ quote
9545
+ });
9440
9546
  } else {
9441
- printResult("Withdraw Preview (dry-run)");
9442
- printResult(` Asset: ${prepared.asset}`);
9443
- printResult(` Amount: ${prepared.amount}`);
9444
- printResult(` Destination: ${prepared.destination}`);
9445
- printResult(` Memo: ${prepared.memo}`);
9446
- printResult(` Est. fee: ${prepared.estimatedFee}`);
9547
+ displaySwapResult(options.fromChain, options.toChain, broadcast.txHash, quote, quote.toCoin.decimals);
9447
9548
  }
9448
- return;
9549
+ return { txHash: broadcast.txHash, quote };
9550
+ } catch (err) {
9551
+ signSpinner?.stop();
9552
+ throw err;
9553
+ } finally {
9554
+ vault.removeAllListeners("signingProgress");
9449
9555
  }
9450
- if (!isJsonOutput()) {
9451
- printResult("Withdraw Preview");
9452
- printResult(` Asset: ${prepared.asset}`);
9453
- printResult(` Amount: ${prepared.amount}`);
9454
- printResult(` Destination: ${prepared.destination}`);
9455
- printResult(` Memo: ${prepared.memo}`);
9456
- printResult(` Est. fee: ${prepared.estimatedFee}`);
9556
+ }
9557
+
9558
+ // src/commands/settings.ts
9559
+ import { Chain as Chain9, fiatCurrencies as fiatCurrencies2, fiatCurrencyNameRecord as fiatCurrencyNameRecord3 } from "@vultisig/sdk";
9560
+ import chalk6 from "chalk";
9561
+ async function executeCurrency(ctx2, newCurrency) {
9562
+ const vault = await ctx2.ensureActiveVault();
9563
+ if (!newCurrency) {
9564
+ const currentCurrency = vault.currency;
9565
+ const currencyName2 = fiatCurrencyNameRecord3[currentCurrency];
9566
+ printResult(chalk6.cyan("\nCurrent Currency Preference:"));
9567
+ printResult(` ${chalk6.green(currentCurrency.toUpperCase())} - ${currencyName2}`);
9568
+ info(chalk6.gray(`
9569
+ Supported currencies: ${fiatCurrencies2.join(", ")}`));
9570
+ info(chalk6.gray('Use "vultisig currency <code>" to change'));
9571
+ return currentCurrency;
9457
9572
  }
9458
- if (!options.yes) {
9459
- warn("This command will broadcast a THORChain MsgDeposit withdrawal. Re-run with -y/--yes to proceed.");
9460
- throw new ConfirmationRequiredError(
9461
- "Withdrawal requires confirmation.",
9462
- "Pass --yes to confirm, or --dry-run to preview."
9463
- );
9573
+ const currency = newCurrency.toLowerCase();
9574
+ if (!fiatCurrencies2.includes(currency)) {
9575
+ error(`x Invalid currency: ${newCurrency}`);
9576
+ warn(`Supported currencies: ${fiatCurrencies2.join(", ")}`);
9577
+ throw new Error("Invalid currency");
9464
9578
  }
9465
- await ensureVaultUnlocked(vault, options.password);
9466
- const execSpinner = createSpinner("Broadcasting withdrawal...");
9467
- const result = await client.withdraw.execute(prepared);
9468
- execSpinner.succeed("Withdrawal submitted");
9579
+ const spinner = createSpinner("Updating currency preference...");
9580
+ await vault.setCurrency(currency);
9581
+ spinner.succeed("Currency updated");
9582
+ const currencyName = fiatCurrencyNameRecord3[currency];
9469
9583
  if (isJsonOutput()) {
9470
- outputJson({ prepared, result });
9471
- } else {
9472
- printResult(`Tx Hash: ${result.txHash}`);
9584
+ outputJson({ currency, name: currencyName, updated: true });
9585
+ return currency;
9473
9586
  }
9587
+ success(`
9588
+ + Currency preference set to ${currency.toUpperCase()} (${currencyName})`);
9589
+ return currency;
9474
9590
  }
9475
-
9476
- // src/commands/discount.ts
9477
- import {
9478
- baseAffiliateBps,
9479
- vultDiscountTierBps,
9480
- vultDiscountTierMinBalances
9481
- } from "@vultisig/sdk";
9482
- import chalk7 from "chalk";
9483
- var TIER_CONFIG = {
9484
- none: { bps: baseAffiliateBps, discount: 0 },
9485
- ...Object.fromEntries(
9486
- Object.entries(vultDiscountTierMinBalances).map(([tier, minVult]) => [
9487
- tier,
9488
- {
9489
- bps: baseAffiliateBps - vultDiscountTierBps[tier],
9490
- discount: vultDiscountTierBps[tier],
9491
- minVult
9492
- }
9493
- ])
9494
- )
9495
- };
9496
- function getTierColor(tier) {
9497
- const colors = {
9498
- none: chalk7.gray,
9499
- bronze: chalk7.hex("#CD7F32"),
9500
- silver: chalk7.hex("#C0C0C0"),
9501
- gold: chalk7.hex("#FFD700"),
9502
- platinum: chalk7.hex("#E5E4E2"),
9503
- diamond: chalk7.hex("#B9F2FF"),
9504
- ultimate: chalk7.hex("#FF00FF")
9505
- };
9506
- return colors[tier] || chalk7.white;
9591
+ async function executeServer(ctx2) {
9592
+ const spinner = createSpinner("Checking server status...");
9593
+ try {
9594
+ const status = await ctx2.sdk.getServerStatus();
9595
+ spinner.succeed("Server status retrieved");
9596
+ if (isJsonOutput()) {
9597
+ outputJson({ server: status });
9598
+ return status;
9599
+ }
9600
+ printResult(chalk6.cyan("\nServer Status:\n"));
9601
+ printResult(chalk6.bold("Fast Vault Server:"));
9602
+ printResult(` Online: ${status.fastVault.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9603
+ if (status.fastVault.latency) {
9604
+ printResult(` Latency: ${status.fastVault.latency}ms`);
9605
+ }
9606
+ printResult(chalk6.bold("\nMessage Relay:"));
9607
+ printResult(` Online: ${status.messageRelay.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9608
+ if (status.messageRelay.latency) {
9609
+ printResult(` Latency: ${status.messageRelay.latency}ms`);
9610
+ }
9611
+ return status;
9612
+ } catch (err) {
9613
+ spinner.fail("Failed to check server status");
9614
+ error(`
9615
+ x ${err.message}`);
9616
+ throw err;
9617
+ }
9507
9618
  }
9508
- function getNextTier(currentTier) {
9509
- const tierOrder = ["none", "bronze", "silver", "gold", "platinum", "diamond", "ultimate"];
9510
- const currentIndex = tierOrder.indexOf(currentTier);
9511
- if (currentIndex === -1 || currentIndex >= tierOrder.length - 1) {
9512
- return null;
9619
+ async function executeAddressBook(ctx2, options = {}) {
9620
+ if (options.add) {
9621
+ let chain = options.chain;
9622
+ let address = options.address;
9623
+ let name = options.name;
9624
+ const prompts = [];
9625
+ if (!chain) {
9626
+ prompts.push({
9627
+ type: "select",
9628
+ name: "chain",
9629
+ message: "Select chain:",
9630
+ choices: Object.values(Chain9)
9631
+ });
9632
+ }
9633
+ if (!address) {
9634
+ prompts.push({
9635
+ type: "input",
9636
+ name: "address",
9637
+ message: "Enter address:",
9638
+ validate: (input) => input.trim() !== "" || "Address is required"
9639
+ });
9640
+ }
9641
+ if (!name) {
9642
+ prompts.push({
9643
+ type: "input",
9644
+ name: "name",
9645
+ message: "Enter name/label:",
9646
+ validate: (input) => input.trim() !== "" || "Name is required"
9647
+ });
9648
+ }
9649
+ if (prompts.length > 0) {
9650
+ const answers = await prompt(prompts);
9651
+ chain = chain || answers.chain;
9652
+ address = address || answers.address?.trim();
9653
+ name = name || answers.name?.trim();
9654
+ }
9655
+ const spinner2 = createSpinner("Adding address to address book...");
9656
+ const entry = {
9657
+ chain,
9658
+ address,
9659
+ name,
9660
+ source: "saved",
9661
+ dateAdded: Date.now()
9662
+ };
9663
+ await ctx2.sdk.addAddressBookEntry([entry]);
9664
+ spinner2.succeed("Address added");
9665
+ if (isJsonOutput()) {
9666
+ outputJson({ added: entry });
9667
+ return [];
9668
+ }
9669
+ success(`
9670
+ + Added ${name} (${chain}: ${address})`);
9671
+ return [];
9513
9672
  }
9514
- const nextTierName = tierOrder[currentIndex + 1];
9515
- const config = TIER_CONFIG[nextTierName];
9516
- if ("minVult" in config) {
9517
- return { name: nextTierName, vultRequired: config.minVult };
9673
+ if (options.remove) {
9674
+ const spinner2 = createSpinner("Removing address from address book...");
9675
+ await ctx2.sdk.removeAddressBookEntry([{ address: options.remove, chain: options.chain }]);
9676
+ spinner2.succeed("Address removed");
9677
+ if (isJsonOutput()) {
9678
+ outputJson({ removed: { address: options.remove, chain: options.chain } });
9679
+ return [];
9680
+ }
9681
+ success(`
9682
+ + Removed ${options.remove}`);
9683
+ return [];
9518
9684
  }
9519
- return null;
9520
- }
9521
- async function executeDiscount(ctx2, options = {}) {
9522
- const vault = await ctx2.ensureActiveVault();
9523
- const spinner = createSpinner(options.refresh ? "Refreshing discount tier..." : "Loading discount tier...");
9524
- const tierResult = options.refresh ? await vault.updateDiscountTier() : await vault.getDiscountTier();
9525
- const tier = tierResult || "none";
9526
- const config = TIER_CONFIG[tier];
9527
- const nextTier = getNextTier(tier);
9528
- const tierInfo = {
9529
- tier,
9530
- feeBps: config.bps,
9531
- discountBps: config.discount,
9532
- nextTier
9533
- };
9534
- spinner.succeed("Discount tier loaded");
9685
+ const spinner = createSpinner("Loading address book...");
9686
+ const addressBook = await ctx2.sdk.getAddressBook(options.chain);
9687
+ spinner.succeed("Address book loaded");
9688
+ const allEntries = [...addressBook.saved, ...addressBook.vaults];
9535
9689
  if (isJsonOutput()) {
9536
- outputJson({
9537
- tier: tierInfo.tier,
9538
- feeBps: tierInfo.feeBps,
9539
- discountBps: tierInfo.discountBps,
9540
- nextTier: tierInfo.nextTier
9541
- });
9542
- return tierInfo;
9690
+ outputJson({ addressBook: allEntries, chain: options.chain });
9691
+ return allEntries;
9543
9692
  }
9544
- displayDiscountTier(tierInfo);
9545
- return tierInfo;
9546
- }
9547
- function displayDiscountTier(tierInfo) {
9548
- const tierColor = getTierColor(tierInfo.tier);
9549
- printResult(chalk7.cyan("\n+----------------------------------------+"));
9550
- printResult(chalk7.cyan("| VULT Discount Tier |"));
9551
- printResult(chalk7.cyan("+----------------------------------------+\n"));
9552
- const tierDisplay = tierInfo.tier === "none" ? chalk7.gray("No Tier") : tierColor(tierInfo.tier.charAt(0).toUpperCase() + tierInfo.tier.slice(1));
9553
- printResult(` Current Tier: ${tierDisplay}`);
9554
- if (tierInfo.tier === "none") {
9555
- printResult(` Swap Fee: ${chalk7.gray("50 bps (0.50%)")}`);
9556
- printResult(` Discount: ${chalk7.gray("None")}`);
9693
+ if (allEntries.length === 0) {
9694
+ warn(`
9695
+ No addresses in address book${options.chain ? ` for ${options.chain}` : ""}`);
9696
+ info(chalk6.gray("\nUse --add to add an address to the address book"));
9557
9697
  } else {
9558
- printResult(` Swap Fee: ${chalk7.green(`${tierInfo.feeBps} bps (${(tierInfo.feeBps / 100).toFixed(2)}%)`)}`);
9559
- printResult(` Discount: ${chalk7.green(`${tierInfo.discountBps} bps saved`)}`);
9560
- }
9561
- if (tierInfo.nextTier) {
9562
- const nextTierColor = getTierColor(tierInfo.nextTier.name);
9563
- printResult(chalk7.bold("\n Next Tier:"));
9564
- printResult(
9565
- ` ${nextTierColor(tierInfo.nextTier.name.charAt(0).toUpperCase() + tierInfo.nextTier.name.slice(1))} - requires ${tierInfo.nextTier.vultRequired.toLocaleString()} VULT`
9566
- );
9567
- } else if (tierInfo.tier === "ultimate") {
9568
- printResult(chalk7.bold("\n ") + chalk7.magenta("You have the highest tier! 0% swap fees."));
9698
+ printResult(chalk6.cyan(`
9699
+ Address Book${options.chain ? ` (${options.chain})` : ""}:
9700
+ `));
9701
+ const table = allEntries.map((entry) => ({
9702
+ Name: entry.name,
9703
+ Chain: entry.chain,
9704
+ Address: entry.address,
9705
+ Source: entry.source
9706
+ }));
9707
+ printTable(table);
9708
+ info(chalk6.gray("\nUse --add to add or --remove <address> to remove an address"));
9569
9709
  }
9570
- info(chalk7.gray("\n Tip: Thorguard NFT holders get +1 tier upgrade (up to gold)"));
9571
- printResult("");
9710
+ return allEntries;
9572
9711
  }
9573
9712
 
9574
- // src/commands/auth.ts
9575
- import { executeAuthLogout, executeAuthSetup, executeAuthStatus } from "@vultisig/client-shared";
9576
-
9577
- // src/commands/agent.ts
9578
- import chalk10 from "chalk";
9579
- import Table from "cli-table3";
9580
-
9581
- // src/agent/ask.ts
9582
- var AskInterface = class {
9583
- session;
9584
- verbose;
9585
- autoApprove;
9586
- responseParts = [];
9587
- toolCalls = [];
9588
- transactions = [];
9589
- cards = [];
9590
- warnings = [];
9591
- outcome;
9592
- error;
9593
- // Tracks whether the currently-latched `error` is a terminal one (e.g. the
9594
- // depth cap). A terminal error may overwrite a prior non-terminal one; once a
9595
- // terminal error is recorded, later frames cannot replace it. See onError.
9596
- errorIsTerminal = false;
9597
- constructor(session, verbose = false, autoApprove = false) {
9598
- this.session = session;
9599
- this.verbose = verbose;
9600
- this.autoApprove = autoApprove;
9601
- }
9602
- /**
9603
- * Whether the turn threw with a still-unacknowledged broadcast (the F1
9604
- * ack-failure case). The command's catch uses this to gate the ACK_FAILED
9605
- * re-tag so a later, unrelated retryable error after an already-acked
9606
- * broadcast keeps its own (retryable) classification instead of exit 8.
9607
- */
9608
- hasUnacknowledgedBroadcast() {
9609
- return this.session.hasUnacknowledgedBroadcast();
9610
- }
9611
- /**
9612
- * Get UI callbacks that silently collect results.
9613
- * Tool progress is logged to stderr in verbose mode.
9614
- */
9615
- getCallbacks() {
9616
- return {
9617
- onTextDelta: (_delta) => {
9618
- },
9619
- onToolCall: (_id, action, params) => {
9620
- if (this.verbose) {
9621
- const paramStr = params ? ` ${JSON.stringify(params)}` : "";
9622
- process.stderr.write(`[tool] ${action}${paramStr} ...
9623
- `);
9624
- }
9625
- },
9626
- onToolResult: (id, action, success2, data, error2, code) => {
9627
- this.toolCalls.push({ id, action, success: success2, data, error: error2, code });
9628
- if (this.verbose) {
9629
- const status = success2 ? "ok" : `error: ${error2}${code ? ` [${code}]` : ""}`;
9630
- process.stderr.write(`[tool] ${action}: ${status}
9631
- `);
9632
- }
9633
- },
9634
- onAssistantMessage: (content) => {
9635
- if (content) {
9636
- this.responseParts.push(content);
9637
- }
9638
- },
9639
- onBalanceSummary: (card) => {
9640
- this.cards.push(card);
9641
- },
9642
- onTurnOutcome: (outcome) => {
9643
- this.outcome = outcome;
9644
- },
9645
- onSuggestions: (_suggestions) => {
9646
- },
9647
- onTxStatus: (txHash, chain, status, explorerUrl) => {
9648
- const existing = this.transactions.find((t) => t.hash === txHash);
9649
- if (existing) {
9650
- existing.status = status;
9651
- if (explorerUrl) existing.explorerUrl = explorerUrl;
9652
- } else {
9653
- this.transactions.push({ hash: txHash, chain, explorerUrl, status });
9654
- }
9655
- if (this.verbose) {
9656
- process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
9657
- `);
9658
- }
9659
- },
9660
- onError: (message, code) => {
9661
- const isTerminal2 = isTerminalAgentErrorCode(code);
9662
- if (!this.error || isTerminal2 && !this.errorIsTerminal) {
9663
- this.error = { message, code };
9664
- this.errorIsTerminal = isTerminal2;
9665
- }
9666
- process.stderr.write(`[error] ${message} [${code}]
9667
- `);
9668
- },
9669
- onProtocolWarning: (warning) => {
9670
- this.warnings.push(warning);
9671
- process.stderr.write(`[warning] ${warning.message} [${warning.code}]
9672
- `);
9673
- },
9674
- onDone: () => {
9675
- },
9676
- requestPassword: async () => {
9677
- throw new Error("Password required but not provided. Use --password flag.");
9678
- },
9679
- requestConfirmation: async (message) => {
9680
- if (!this.autoApprove) {
9681
- process.stderr.write(`[confirm] signing requires --yes \u2014 NOT broadcasting: ${message}
9682
- `);
9683
- } else {
9684
- process.stderr.write(`[confirm] auto-approved (--yes): ${message}
9685
- `);
9686
- }
9687
- return this.autoApprove;
9688
- }
9689
- };
9690
- }
9691
- /**
9692
- * Send a message and wait for the complete response.
9693
- * All tool calls and actions are executed automatically.
9694
- */
9695
- async ask(message) {
9696
- this.responseParts = [];
9697
- this.toolCalls = [];
9698
- this.transactions = [];
9699
- this.cards = [];
9700
- this.warnings = [];
9701
- this.outcome = void 0;
9702
- this.error = void 0;
9703
- this.errorIsTerminal = false;
9704
- const callbacks = this.getCallbacks();
9705
- await this.session.sendMessage(message, callbacks);
9706
- return this.partialResult();
9707
- }
9708
- /**
9709
- * Snapshot of everything collected so far this turn. Identical to a normal
9710
- * `ask()` return, but callable from a catch block when `ask()` THREW mid-turn
9711
- * — e.g. the follow-up request that reports recent_actions back to the backend
9712
- * fails (timeout/5xx/auth) AFTER a tx has already broadcast and `onTxStatus`
9713
- * fired. Lets the caller still surface the already-broadcast tx hash in the
9714
- * error envelope instead of stranding funds the turn just moved.
9715
- */
9716
- partialResult() {
9717
- return {
9718
- sessionId: this.session.getConversationId() || "",
9719
- response: this.responseParts[this.responseParts.length - 1] || "",
9720
- toolCalls: this.toolCalls,
9721
- transactions: this.transactions,
9722
- cards: this.cards,
9723
- warnings: this.warnings,
9724
- error: this.error,
9725
- ...this.outcome ? { outcome: this.outcome } : {}
9726
- };
9727
- }
9728
- };
9729
-
9730
- // src/agent/auth.ts
9731
- import { randomBytes as randomBytes2 } from "node:crypto";
9732
- import { Chain as Chain10, computePersonalSignHash, formatEcdsaSignature65 } from "@vultisig/sdk";
9733
- async function authenticateVault(client, vault, password, maxAttempts = 3) {
9734
- const publicKey = vault.publicKeys.ecdsa;
9735
- const chainCode = vault.hexChainCode;
9736
- const ethAddress = await vault.address(Chain10.Ethereum);
9737
- const nonce = "0x" + randomBytes2(16).toString("hex");
9738
- const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
9739
- const authMessage = JSON.stringify({
9740
- message: "Sign into Vultisig Plugin Marketplace",
9741
- nonce,
9742
- expiresAt,
9743
- address: ethAddress
9744
- });
9745
- const messageHash = computePersonalSignHash(authMessage);
9746
- let lastError = null;
9747
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9748
- try {
9749
- if (attempt > 1) {
9750
- process.stderr.write(` Retry ${attempt}/${maxAttempts}...
9751
- `);
9752
- }
9753
- const signature = await vault.signBytes({ data: Buffer.from(messageHash), chain: Chain10.Ethereum }, {});
9754
- if (signature.recovery === void 0) {
9755
- throw new Error("Agent authentication requires an ECDSA recovery id");
9756
- }
9757
- const sigHex = formatEcdsaSignature65(signature.signature, signature.recovery);
9758
- const authResponse = await client.authenticate({
9759
- public_key: publicKey,
9760
- chain_code_hex: chainCode,
9761
- message: authMessage,
9762
- signature: sigHex
9763
- });
9764
- return {
9765
- token: authResponse.token,
9766
- expiresAt: authResponse.expires_at,
9767
- // Captured + persisted by the session token cache. The backend exposes
9768
- // POST /auth/refresh to exchange it for a fresh access token without a
9769
- // new MPC round; wiring that exchange is a future enhancement — today
9770
- // the CLI re-auths via a full MPC re-sign (authenticateVault), which is
9771
- // always available and avoids depending on refresh-token rotation.
9772
- refreshToken: authResponse.refresh_token
9773
- };
9774
- } catch (err) {
9775
- lastError = err;
9776
- if (attempt < maxAttempts && err.message?.includes("timeout")) {
9777
- continue;
9778
- }
9779
- throw err;
9713
+ // src/commands/rujira.ts
9714
+ import {
9715
+ getRoutesSummary,
9716
+ listEasyRoutes,
9717
+ RujiraClient,
9718
+ VultisigRujiraProvider
9719
+ } from "@vultisig/rujira";
9720
+ async function createRujiraClient(ctx2, options = {}) {
9721
+ const vault = await ctx2.ensureActiveVault();
9722
+ const provider = new VultisigRujiraProvider(vault);
9723
+ const client = new RujiraClient({
9724
+ signer: provider,
9725
+ rpcEndpoint: options.rpcEndpoint,
9726
+ config: {
9727
+ // Allow overriding rest endpoint via config (used for thornode calls)
9728
+ ...options.restEndpoint ? { restEndpoint: options.restEndpoint } : {}
9780
9729
  }
9781
- }
9782
- throw lastError || new Error("Authentication failed after all attempts");
9783
- }
9784
-
9785
- // src/agent/cards.ts
9786
- import chalk8 from "chalk";
9787
- var CLI_SUPPORTED_SURFACES = ["balance_summary", "turn_outcome"];
9788
- function parseTurnOutcome(raw) {
9789
- if (!raw || typeof raw !== "object") return null;
9790
- const kind = raw.kind;
9791
- if (kind !== "success" && kind !== "blocked" && kind !== "refusal" && kind !== "error") return null;
9792
- const code = raw.code;
9793
- const detail = raw.detail;
9794
- return {
9795
- kind,
9796
- ...typeof code === "string" ? { code } : {},
9797
- ...typeof detail === "string" ? { detail } : {}
9798
- };
9730
+ });
9731
+ const spinner = createSpinner("Connecting to Rujira/THORChain...");
9732
+ await client.connect();
9733
+ spinner.succeed("Connected");
9734
+ return client;
9799
9735
  }
9800
- function stripControlChars(s) {
9801
- let out = "";
9802
- for (const ch of s) {
9803
- const code = ch.codePointAt(0) ?? 0;
9804
- if (code <= 31 || code >= 127 && code <= 159) continue;
9805
- out += ch;
9736
+ async function executeRujiraBalance(ctx2, options = {}) {
9737
+ const vault = await ctx2.ensureActiveVault();
9738
+ const thorAddress = await vault.address("THORChain");
9739
+ const client = await createRujiraClient(ctx2, options);
9740
+ const spinner = createSpinner("Loading THORChain balances...");
9741
+ const balances = await client.deposit.getBalances(thorAddress);
9742
+ spinner.succeed("Balances loaded");
9743
+ const filtered = options.securedOnly ? balances.filter((b) => b.denom.includes("-") || b.denom.includes("/")) : balances;
9744
+ if (isJsonOutput()) {
9745
+ outputJson({ thorAddress, balances: filtered });
9746
+ return;
9806
9747
  }
9807
- return out;
9808
- }
9809
- function asString(v) {
9810
- return typeof v === "string" ? stripControlChars(v) : "";
9811
- }
9812
- function parseToken(v) {
9813
- if (!v || typeof v !== "object") return null;
9814
- const o = v;
9815
- const symbol = asString(o.symbol);
9816
- const amountDecimal = asString(o.amountDecimal);
9817
- if (!symbol && !amountDecimal) return null;
9818
- const token = { symbol, amountDecimal };
9819
- const amountUsd = asString(o.amountUsd);
9820
- if (amountUsd) token.amountUsd = amountUsd;
9821
- return token;
9822
- }
9823
- function parseAccount(v) {
9824
- if (!v || typeof v !== "object") return null;
9825
- const o = v;
9826
- const chainId = asString(o.chainId);
9827
- if (!chainId) return null;
9828
- const tokensRaw = Array.isArray(o.tokens) ? o.tokens : [];
9829
- const tokens = tokensRaw.map(parseToken).filter((t) => t !== null);
9830
- return { chainId, address: asString(o.address) || "\u2014", tokens };
9748
+ info(`THORChain address: ${thorAddress}`);
9749
+ if (!filtered.length) {
9750
+ printResult("No balances found");
9751
+ return;
9752
+ }
9753
+ printTable(
9754
+ filtered.map((b) => ({
9755
+ asset: b.asset,
9756
+ denom: b.denom,
9757
+ amount: b.formatted,
9758
+ raw: b.amount
9759
+ }))
9760
+ );
9831
9761
  }
9832
- function parseBalanceSummaryEnvelope(value) {
9833
- if (!value || typeof value !== "object") return null;
9834
- const o = value;
9835
- if (o.surface !== "balance_summary") return null;
9836
- if (!Array.isArray(o.accounts)) return null;
9837
- const accounts = o.accounts.map(parseAccount).filter((a) => a !== null);
9838
- if (accounts.length === 0) return null;
9839
- const card = { surface: "balance_summary", accounts };
9840
- if (o.stale === true) {
9841
- card.stale = true;
9842
- if (typeof o.stale_secs === "number") card.staleSecs = o.stale_secs;
9762
+ async function executeRujiraRoutes() {
9763
+ const routes = listEasyRoutes();
9764
+ const summary = getRoutesSummary();
9765
+ if (isJsonOutput()) {
9766
+ outputJson({ routes, summary });
9767
+ return;
9843
9768
  }
9844
- return card;
9769
+ printResult(summary);
9770
+ printResult("");
9771
+ printTable(
9772
+ routes.map((r) => ({
9773
+ name: r.name,
9774
+ from: r.from,
9775
+ to: r.to,
9776
+ liquidity: r.liquidity,
9777
+ description: r.description
9778
+ }))
9779
+ );
9845
9780
  }
9846
- function matchBrace(text, start) {
9847
- let depth = 0;
9848
- let inString = false;
9849
- let escaped = false;
9850
- for (let i = start; i < text.length; i++) {
9851
- const ch = text[i];
9852
- if (inString) {
9853
- if (escaped) escaped = false;
9854
- else if (ch === "\\") escaped = true;
9855
- else if (ch === '"') inString = false;
9856
- continue;
9857
- }
9858
- if (ch === '"') inString = true;
9859
- else if (ch === "{") depth++;
9860
- else if (ch === "}") {
9861
- depth--;
9862
- if (depth === 0) return i;
9781
+ async function executeRujiraDeposit(ctx2, options = {}) {
9782
+ const vault = await ctx2.ensureActiveVault();
9783
+ const thorAddress = await vault.address("THORChain");
9784
+ const client = await createRujiraClient(ctx2, options);
9785
+ if (!options.asset) {
9786
+ const spinner2 = createSpinner("Loading THORChain inbound addresses...");
9787
+ const inbound = await client.deposit.getInboundAddresses();
9788
+ spinner2.succeed("Inbound addresses loaded");
9789
+ if (isJsonOutput()) {
9790
+ outputJson({ thorAddress, inboundAddresses: inbound });
9791
+ return;
9863
9792
  }
9793
+ info(`THORChain address: ${thorAddress}`);
9794
+ printResult("Provide an L1 asset to get a chain-specific inbound address + memo.");
9795
+ printResult("Example: vultisig rujira deposit --asset BTC.BTC --amount 100000");
9796
+ printResult("");
9797
+ printTable(
9798
+ inbound.map((a) => ({
9799
+ chain: a.chain,
9800
+ address: a.address,
9801
+ halted: a.halted,
9802
+ globalTradingPaused: a.global_trading_paused,
9803
+ chainTradingPaused: a.chain_trading_paused
9804
+ }))
9805
+ );
9806
+ return;
9807
+ }
9808
+ const amount = options.amount ?? "1";
9809
+ const spinner = createSpinner("Preparing deposit instructions...");
9810
+ const prepared = await client.deposit.prepare({
9811
+ fromAsset: options.asset,
9812
+ amount,
9813
+ thorAddress,
9814
+ affiliate: options.affiliate,
9815
+ affiliateBps: options.affiliateBps
9816
+ });
9817
+ spinner.succeed("Deposit prepared");
9818
+ if (isJsonOutput()) {
9819
+ outputJson({ thorAddress, deposit: prepared });
9820
+ return;
9821
+ }
9822
+ info(`THORChain address: ${thorAddress}`);
9823
+ printResult("Deposit instructions (send from L1):");
9824
+ printResult(` Chain: ${prepared.chain}`);
9825
+ printResult(` Asset: ${prepared.asset}`);
9826
+ printResult(` Inbound address:${prepared.inboundAddress}`);
9827
+ printResult(` Memo: ${prepared.memo}`);
9828
+ printResult(` Min amount: ${prepared.minimumAmount}`);
9829
+ if (prepared.warning) {
9830
+ warn(prepared.warning);
9864
9831
  }
9865
- return -1;
9866
9832
  }
9867
- function extractBalanceSummaryFromText(content) {
9868
- if (!content || !content.includes("balance_summary")) return null;
9869
- if (content.length > 2e5) return null;
9870
- for (let i = content.indexOf("{"); i !== -1; i = content.indexOf("{", i + 1)) {
9871
- const end = matchBrace(content, i);
9872
- if (end === -1) break;
9873
- const blob = content.slice(i, end + 1);
9874
- if (!blob.includes("balance_summary")) continue;
9875
- let parsed;
9876
- try {
9877
- parsed = JSON.parse(blob);
9878
- } catch {
9879
- continue;
9833
+ async function executeRujiraSwap(ctx2, options) {
9834
+ const vault = await ctx2.ensureActiveVault();
9835
+ if (!options.dryRun && !options.yes && isNonInteractive()) {
9836
+ throw new ConfirmationRequiredError(
9837
+ "Swap requires confirmation.",
9838
+ "Pass --yes to confirm, or --dry-run to preview."
9839
+ );
9840
+ }
9841
+ const client = await createRujiraClient(ctx2, options);
9842
+ const destination = options.destination ?? await vault.address("THORChain");
9843
+ const quoteSpinner = createSpinner("Getting FIN swap quote...");
9844
+ const quote = await client.swap.getQuote({
9845
+ fromAsset: options.fromAsset,
9846
+ toAsset: options.toAsset,
9847
+ amount: options.amount,
9848
+ destination,
9849
+ slippageBps: options.slippageBps
9850
+ });
9851
+ quoteSpinner.succeed("Quote received");
9852
+ if (options.dryRun) {
9853
+ if (isJsonOutput()) {
9854
+ outputJson({ dryRun: true, quote });
9855
+ } else {
9856
+ printResult("FIN Swap Preview (dry-run)");
9857
+ printResult(` From: ${options.fromAsset}`);
9858
+ printResult(` To: ${options.toAsset}`);
9859
+ printResult(` Amount (in): ${options.amount}`);
9860
+ printResult(` Expected out:${quote.expectedOutput}`);
9861
+ printResult(` Min out: ${quote.minimumOutput}`);
9862
+ printResult(` Contract: ${quote.contractAddress}`);
9880
9863
  }
9881
- const card = parseBalanceSummaryEnvelope(parsed);
9882
- if (!card) continue;
9883
- const before = content.slice(0, i).replace(/```(?:json)?\s*$/i, "");
9884
- const after = content.slice(end + 1).replace(/^\s*```/, "");
9885
- const remainingText = (before + after).trim();
9886
- return { card, remainingText };
9864
+ return;
9887
9865
  }
9888
- return null;
9889
- }
9890
- function shortenAddress(address) {
9891
- if (!address || address === "\u2014") return address || "\u2014";
9892
- if (address.length <= 16) return address;
9893
- return `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
9894
- }
9895
- function parseUsd(amountUsd) {
9896
- if (!amountUsd) return null;
9897
- const cleaned = amountUsd.replace(/[$,\s]/g, "");
9898
- if (!cleaned) return null;
9899
- const n = Number(cleaned);
9900
- return Number.isFinite(n) ? n : null;
9901
- }
9902
- function formatUsd(n) {
9903
- return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
9904
- }
9905
- function renderBalanceSummaryCard(card) {
9906
- const lines = [];
9907
- const staleCue = card.stale ? chalk8.gray(` (stale${card.staleSecs ? ` ~${Math.round(card.staleSecs / 60)}m` : ""}, refreshing\u2026)`) : "";
9908
- lines.push(chalk8.bold(" Balances") + staleCue);
9909
- let total = 0;
9910
- let sawUsd = false;
9911
- for (const account of card.accounts) {
9912
- lines.push(` ${chalk8.cyan(account.chainId)} ${chalk8.gray(`(${shortenAddress(account.address)})`)}`);
9913
- if (account.tokens.length === 0) {
9914
- lines.push(chalk8.gray(" (no balances)"));
9915
- continue;
9866
+ if (!isJsonOutput()) {
9867
+ printResult("FIN Swap Preview");
9868
+ printResult(` From: ${options.fromAsset}`);
9869
+ printResult(` To: ${options.toAsset}`);
9870
+ printResult(` Amount (in): ${options.amount}`);
9871
+ printResult(` Expected out:${quote.expectedOutput}`);
9872
+ printResult(` Min out: ${quote.minimumOutput}`);
9873
+ printResult(` Contract: ${quote.contractAddress}`);
9874
+ if (quote.warning) {
9875
+ warn(quote.warning);
9916
9876
  }
9917
- for (const token of account.tokens) {
9918
- const usd = parseUsd(token.amountUsd);
9919
- if (usd !== null) {
9920
- total += usd;
9921
- sawUsd = true;
9922
- }
9923
- const symbol = token.symbol.padEnd(10);
9924
- const amount = token.amountDecimal.padStart(16);
9925
- const usdCol = token.amountUsd ? chalk8.gray(` ${token.amountUsd}`) : "";
9926
- lines.push(` ${chalk8.bold(symbol)}${amount}${usdCol}`);
9877
+ }
9878
+ if (!options.yes) {
9879
+ warn("This command will execute a swap. Re-run with -y/--yes to skip this warning.");
9880
+ throw new ConfirmationRequiredError(
9881
+ "Swap requires confirmation.",
9882
+ "Pass --yes to confirm, or --dry-run to preview."
9883
+ );
9884
+ }
9885
+ await ensureVaultUnlocked(vault, options.password);
9886
+ const execSpinner = createSpinner("Executing FIN swap...");
9887
+ const result = await client.swap.execute(quote, { slippageBps: options.slippageBps });
9888
+ execSpinner.succeed("Swap submitted");
9889
+ if (isJsonOutput()) {
9890
+ outputJson({ quote, result });
9891
+ } else {
9892
+ printResult(`Tx Hash: ${result.txHash}`);
9893
+ }
9894
+ }
9895
+ async function executeRujiraWithdraw(ctx2, options) {
9896
+ const vault = await ctx2.ensureActiveVault();
9897
+ if (!options.dryRun && !options.yes && isNonInteractive()) {
9898
+ throw new ConfirmationRequiredError(
9899
+ "Withdrawal requires confirmation.",
9900
+ "Pass --yes to confirm, or --dry-run to preview."
9901
+ );
9902
+ }
9903
+ const client = await createRujiraClient(ctx2, options);
9904
+ const prepSpinner = createSpinner("Preparing withdrawal (MsgDeposit)...");
9905
+ const prepared = await client.withdraw.prepare({
9906
+ asset: options.asset,
9907
+ amount: options.amount,
9908
+ l1Address: options.l1Address,
9909
+ maxFeeBps: options.maxFeeBps
9910
+ });
9911
+ prepSpinner.succeed("Withdrawal prepared");
9912
+ if (options.dryRun) {
9913
+ if (isJsonOutput()) {
9914
+ outputJson({ dryRun: true, prepared });
9915
+ } else {
9916
+ printResult("Withdraw Preview (dry-run)");
9917
+ printResult(` Asset: ${prepared.asset}`);
9918
+ printResult(` Amount: ${prepared.amount}`);
9919
+ printResult(` Destination: ${prepared.destination}`);
9920
+ printResult(` Memo: ${prepared.memo}`);
9921
+ printResult(` Est. fee: ${prepared.estimatedFee}`);
9927
9922
  }
9923
+ return;
9928
9924
  }
9929
- if (sawUsd) {
9930
- lines.push(chalk8.gray(" " + "\u2500".repeat(36)));
9931
- lines.push(` ${chalk8.bold("Total")} ${chalk8.green(formatUsd(total))}`);
9925
+ if (!isJsonOutput()) {
9926
+ printResult("Withdraw Preview");
9927
+ printResult(` Asset: ${prepared.asset}`);
9928
+ printResult(` Amount: ${prepared.amount}`);
9929
+ printResult(` Destination: ${prepared.destination}`);
9930
+ printResult(` Memo: ${prepared.memo}`);
9931
+ printResult(` Est. fee: ${prepared.estimatedFee}`);
9932
+ }
9933
+ if (!options.yes) {
9934
+ warn("This command will broadcast a THORChain MsgDeposit withdrawal. Re-run with -y/--yes to proceed.");
9935
+ throw new ConfirmationRequiredError(
9936
+ "Withdrawal requires confirmation.",
9937
+ "Pass --yes to confirm, or --dry-run to preview."
9938
+ );
9939
+ }
9940
+ await ensureVaultUnlocked(vault, options.password);
9941
+ const execSpinner = createSpinner("Broadcasting withdrawal...");
9942
+ const result = await client.withdraw.execute(prepared);
9943
+ execSpinner.succeed("Withdrawal submitted");
9944
+ if (isJsonOutput()) {
9945
+ outputJson({ prepared, result });
9946
+ } else {
9947
+ printResult(`Tx Hash: ${result.txHash}`);
9932
9948
  }
9933
- return lines.join("\n");
9934
9949
  }
9935
9950
 
9936
- // src/agent/client.ts
9937
- import { randomUUID } from "node:crypto";
9938
-
9939
- // src/agent/toolOutputSigning.ts
9940
- import { getChainKind as getChainKind4 } from "@vultisig/sdk";
9941
-
9942
- // src/agent/executor.ts
9951
+ // src/commands/discount.ts
9943
9952
  import {
9944
- Chain as Chain11,
9945
- chainFeeCoin,
9946
- getChainKind as getChainKind3,
9947
- resolveChainReference,
9948
- VaultError as VaultError3,
9949
- VaultErrorCode as VaultErrorCode3,
9950
- Vultisig as VultisigSdk
9953
+ baseAffiliateBps,
9954
+ vultDiscountTierBps,
9955
+ vultDiscountTierMinBalances
9951
9956
  } from "@vultisig/sdk";
9952
-
9953
- // ../../node_modules/viem/_esm/accounts/utils/publicKeyToAddress.js
9954
- init_getAddress();
9955
- init_keccak256();
9956
- function publicKeyToAddress(publicKey) {
9957
- const address = keccak256(`0x${publicKey.substring(4)}`).substring(26);
9958
- return checksumAddress(`0x${address}`);
9957
+ import chalk7 from "chalk";
9958
+ var TIER_CONFIG = {
9959
+ none: { bps: baseAffiliateBps, discount: 0 },
9960
+ ...Object.fromEntries(
9961
+ Object.entries(vultDiscountTierMinBalances).map(([tier, minVult]) => [
9962
+ tier,
9963
+ {
9964
+ bps: baseAffiliateBps - vultDiscountTierBps[tier],
9965
+ discount: vultDiscountTierBps[tier],
9966
+ minVult
9967
+ }
9968
+ ])
9969
+ )
9970
+ };
9971
+ function getTierColor(tier) {
9972
+ const colors = {
9973
+ none: chalk7.gray,
9974
+ bronze: chalk7.hex("#CD7F32"),
9975
+ silver: chalk7.hex("#C0C0C0"),
9976
+ gold: chalk7.hex("#FFD700"),
9977
+ platinum: chalk7.hex("#E5E4E2"),
9978
+ diamond: chalk7.hex("#B9F2FF"),
9979
+ ultimate: chalk7.hex("#FF00FF")
9980
+ };
9981
+ return colors[tier] || chalk7.white;
9959
9982
  }
9960
-
9961
- // ../../node_modules/viem/_esm/utils/signature/recoverPublicKey.js
9962
- init_isHex();
9963
- init_size();
9964
- init_fromHex();
9965
- init_toHex();
9966
- async function recoverPublicKey({ hash, signature }) {
9967
- const hashHex = isHex(hash) ? hash : toHex(hash);
9968
- const { secp256k1: secp256k12 } = await Promise.resolve().then(() => (init_secp256k1(), secp256k1_exports));
9969
- const signature_ = (() => {
9970
- if (typeof signature === "object" && "r" in signature && "s" in signature) {
9971
- const { r, s, v, yParity } = signature;
9972
- const yParityOrV2 = Number(yParity ?? v);
9973
- const recoveryBit2 = toRecoveryBit(yParityOrV2);
9974
- return new secp256k12.Signature(hexToBigInt(r), hexToBigInt(s)).addRecoveryBit(recoveryBit2);
9975
- }
9976
- const signatureHex = isHex(signature) ? signature : toHex(signature);
9977
- if (size(signatureHex) !== 65)
9978
- throw new Error("invalid signature length");
9979
- const yParityOrV = hexToNumber(`0x${signatureHex.slice(130)}`);
9980
- const recoveryBit = toRecoveryBit(yParityOrV);
9981
- return secp256k12.Signature.fromCompact(signatureHex.substring(2, 130)).addRecoveryBit(recoveryBit);
9982
- })();
9983
- const publicKey = signature_.recoverPublicKey(hashHex.substring(2)).toHex(false);
9984
- return `0x${publicKey}`;
9983
+ function getNextTier(currentTier) {
9984
+ const tierOrder = ["none", "bronze", "silver", "gold", "platinum", "diamond", "ultimate"];
9985
+ const currentIndex = tierOrder.indexOf(currentTier);
9986
+ if (currentIndex === -1 || currentIndex >= tierOrder.length - 1) {
9987
+ return null;
9988
+ }
9989
+ const nextTierName = tierOrder[currentIndex + 1];
9990
+ const config = TIER_CONFIG[nextTierName];
9991
+ if ("minVult" in config) {
9992
+ return { name: nextTierName, vultRequired: config.minVult };
9993
+ }
9994
+ return null;
9985
9995
  }
9986
- function toRecoveryBit(yParityOrV) {
9987
- if (yParityOrV === 0 || yParityOrV === 1)
9988
- return yParityOrV;
9989
- if (yParityOrV === 27)
9990
- return 0;
9991
- if (yParityOrV === 28)
9992
- return 1;
9993
- throw new Error("Invalid yParityOrV value");
9996
+ async function executeDiscount(ctx2, options = {}) {
9997
+ const vault = await ctx2.ensureActiveVault();
9998
+ const spinner = createSpinner(options.refresh ? "Refreshing discount tier..." : "Loading discount tier...");
9999
+ const tierResult = options.refresh ? await vault.updateDiscountTier() : await vault.getDiscountTier();
10000
+ const tier = tierResult || "none";
10001
+ const config = TIER_CONFIG[tier];
10002
+ const nextTier = getNextTier(tier);
10003
+ const tierInfo = {
10004
+ tier,
10005
+ feeBps: config.bps,
10006
+ discountBps: config.discount,
10007
+ nextTier
10008
+ };
10009
+ spinner.succeed("Discount tier loaded");
10010
+ if (isJsonOutput()) {
10011
+ outputJson({
10012
+ tier: tierInfo.tier,
10013
+ feeBps: tierInfo.feeBps,
10014
+ discountBps: tierInfo.discountBps,
10015
+ nextTier: tierInfo.nextTier
10016
+ });
10017
+ return tierInfo;
10018
+ }
10019
+ displayDiscountTier(tierInfo);
10020
+ return tierInfo;
9994
10021
  }
9995
-
9996
- // ../../node_modules/viem/_esm/utils/signature/recoverAddress.js
9997
- async function recoverAddress({ hash, signature }) {
9998
- return publicKeyToAddress(await recoverPublicKey({ hash, signature }));
10022
+ function displayDiscountTier(tierInfo) {
10023
+ const tierColor = getTierColor(tierInfo.tier);
10024
+ printResult(chalk7.cyan("\n+----------------------------------------+"));
10025
+ printResult(chalk7.cyan("| VULT Discount Tier |"));
10026
+ printResult(chalk7.cyan("+----------------------------------------+\n"));
10027
+ const tierDisplay = tierInfo.tier === "none" ? chalk7.gray("No Tier") : tierColor(tierInfo.tier.charAt(0).toUpperCase() + tierInfo.tier.slice(1));
10028
+ printResult(` Current Tier: ${tierDisplay}`);
10029
+ if (tierInfo.tier === "none") {
10030
+ printResult(` Swap Fee: ${chalk7.gray("50 bps (0.50%)")}`);
10031
+ printResult(` Discount: ${chalk7.gray("None")}`);
10032
+ } else {
10033
+ printResult(` Swap Fee: ${chalk7.green(`${tierInfo.feeBps} bps (${(tierInfo.feeBps / 100).toFixed(2)}%)`)}`);
10034
+ printResult(` Discount: ${chalk7.green(`${tierInfo.discountBps} bps saved`)}`);
10035
+ }
10036
+ if (tierInfo.nextTier) {
10037
+ const nextTierColor = getTierColor(tierInfo.nextTier.name);
10038
+ printResult(chalk7.bold("\n Next Tier:"));
10039
+ printResult(
10040
+ ` ${nextTierColor(tierInfo.nextTier.name.charAt(0).toUpperCase() + tierInfo.nextTier.name.slice(1))} - requires ${tierInfo.nextTier.vultRequired.toLocaleString()} VULT`
10041
+ );
10042
+ } else if (tierInfo.tier === "ultimate") {
10043
+ printResult(chalk7.bold("\n ") + chalk7.magenta("You have the highest tier! 0% swap fees."));
10044
+ }
10045
+ info(chalk7.gray("\n Tip: Thorguard NFT holders get +1 tier upgrade (up to gold)"));
10046
+ printResult("");
9999
10047
  }
10000
10048
 
10001
- // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
10002
- init_encodeAbiParameters();
10003
- init_concat();
10004
- init_toHex();
10005
- init_keccak256();
10049
+ // src/commands/auth.ts
10050
+ import { executeAuthLogout, executeAuthSetup, executeAuthStatus } from "@vultisig/client-shared";
10006
10051
 
10007
- // ../../node_modules/viem/_esm/utils/typedData.js
10008
- init_abi();
10009
- init_address();
10052
+ // src/commands/agent.ts
10053
+ import chalk10 from "chalk";
10054
+ import Table from "cli-table3";
10010
10055
 
10011
- // ../../node_modules/viem/_esm/errors/typedData.js
10012
- init_stringify();
10013
- init_base();
10014
- var InvalidDomainError = class extends BaseError {
10015
- constructor({ domain }) {
10016
- super(`Invalid domain "${stringify(domain)}".`, {
10017
- metaMessages: ["Must be a valid EIP-712 domain."]
10018
- });
10056
+ // src/agent/ask.ts
10057
+ var AskInterface = class {
10058
+ session;
10059
+ verbose;
10060
+ autoApprove;
10061
+ responseParts = [];
10062
+ toolCalls = [];
10063
+ transactions = [];
10064
+ cards = [];
10065
+ warnings = [];
10066
+ outcome;
10067
+ error;
10068
+ // Tracks whether the currently-latched `error` is a terminal one (e.g. the
10069
+ // depth cap). A terminal error may overwrite a prior non-terminal one; once a
10070
+ // terminal error is recorded, later frames cannot replace it. See onError.
10071
+ errorIsTerminal = false;
10072
+ constructor(session, verbose = false, autoApprove = false) {
10073
+ this.session = session;
10074
+ this.verbose = verbose;
10075
+ this.autoApprove = autoApprove;
10076
+ }
10077
+ /**
10078
+ * Whether the turn threw with a still-unacknowledged broadcast (the F1
10079
+ * ack-failure case). The command's catch uses this to gate the ACK_FAILED
10080
+ * re-tag so a later, unrelated retryable error after an already-acked
10081
+ * broadcast keeps its own (retryable) classification instead of exit 8.
10082
+ */
10083
+ hasUnacknowledgedBroadcast() {
10084
+ return this.session.hasUnacknowledgedBroadcast();
10019
10085
  }
10020
- };
10021
- var InvalidPrimaryTypeError = class extends BaseError {
10022
- constructor({ primaryType, types }) {
10023
- super(`Invalid primary type \`${primaryType}\` must be one of \`${JSON.stringify(Object.keys(types))}\`.`, {
10024
- docsPath: "/api/glossary/Errors#typeddatainvalidprimarytypeerror",
10025
- metaMessages: ["Check that the primary type is a key in `types`."]
10026
- });
10086
+ /**
10087
+ * Get UI callbacks that silently collect results.
10088
+ * Tool progress is logged to stderr in verbose mode.
10089
+ */
10090
+ getCallbacks() {
10091
+ return {
10092
+ onTextDelta: (_delta) => {
10093
+ },
10094
+ onToolCall: (_id, action, params) => {
10095
+ if (this.verbose) {
10096
+ const paramStr = params ? ` ${JSON.stringify(params)}` : "";
10097
+ process.stderr.write(`[tool] ${action}${paramStr} ...
10098
+ `);
10099
+ }
10100
+ },
10101
+ onToolResult: (id, action, success2, data, error2, code) => {
10102
+ this.toolCalls.push({ id, action, success: success2, data, error: error2, code });
10103
+ if (this.verbose) {
10104
+ const status = success2 ? "ok" : `error: ${error2}${code ? ` [${code}]` : ""}`;
10105
+ process.stderr.write(`[tool] ${action}: ${status}
10106
+ `);
10107
+ }
10108
+ },
10109
+ onAssistantMessage: (content) => {
10110
+ if (content) {
10111
+ this.responseParts.push(content);
10112
+ }
10113
+ },
10114
+ onBalanceSummary: (card) => {
10115
+ this.cards.push(card);
10116
+ },
10117
+ onTurnOutcome: (outcome) => {
10118
+ this.outcome = outcome;
10119
+ },
10120
+ onSuggestions: (_suggestions) => {
10121
+ },
10122
+ onTxStatus: (txHash, chain, status, explorerUrl) => {
10123
+ const existing = this.transactions.find((t) => t.hash === txHash);
10124
+ if (existing) {
10125
+ existing.status = status;
10126
+ if (explorerUrl) existing.explorerUrl = explorerUrl;
10127
+ } else {
10128
+ this.transactions.push({ hash: txHash, chain, explorerUrl, status });
10129
+ }
10130
+ if (this.verbose) {
10131
+ process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
10132
+ `);
10133
+ }
10134
+ },
10135
+ onError: (message, code) => {
10136
+ const isTerminal2 = isTerminalAgentErrorCode(code);
10137
+ if (!this.error || isTerminal2 && !this.errorIsTerminal) {
10138
+ this.error = { message, code };
10139
+ this.errorIsTerminal = isTerminal2;
10140
+ }
10141
+ process.stderr.write(`[error] ${message} [${code}]
10142
+ `);
10143
+ },
10144
+ onProtocolWarning: (warning) => {
10145
+ this.warnings.push(warning);
10146
+ process.stderr.write(`[warning] ${warning.message} [${warning.code}]
10147
+ `);
10148
+ },
10149
+ onDone: () => {
10150
+ },
10151
+ requestPassword: async () => {
10152
+ throw new Error("Password required but not provided. Use --password flag.");
10153
+ },
10154
+ requestConfirmation: async (message) => {
10155
+ if (!this.autoApprove) {
10156
+ process.stderr.write(`[confirm] signing requires --yes \u2014 NOT broadcasting: ${message}
10157
+ `);
10158
+ } else {
10159
+ process.stderr.write(`[confirm] auto-approved (--yes): ${message}
10160
+ `);
10161
+ }
10162
+ return this.autoApprove;
10163
+ }
10164
+ };
10027
10165
  }
10028
- };
10029
- var InvalidStructTypeError = class extends BaseError {
10030
- constructor({ type }) {
10031
- super(`Struct type "${type}" is invalid.`, {
10032
- metaMessages: ["Struct type must not be a Solidity type."],
10033
- name: "InvalidStructTypeError"
10034
- });
10166
+ /**
10167
+ * Send a message and wait for the complete response.
10168
+ * All tool calls and actions are executed automatically.
10169
+ */
10170
+ async ask(message) {
10171
+ this.responseParts = [];
10172
+ this.toolCalls = [];
10173
+ this.transactions = [];
10174
+ this.cards = [];
10175
+ this.warnings = [];
10176
+ this.outcome = void 0;
10177
+ this.error = void 0;
10178
+ this.errorIsTerminal = false;
10179
+ const callbacks = this.getCallbacks();
10180
+ await this.session.sendMessage(message, callbacks);
10181
+ return this.partialResult();
10182
+ }
10183
+ /**
10184
+ * Snapshot of everything collected so far this turn. Identical to a normal
10185
+ * `ask()` return, but callable from a catch block when `ask()` THREW mid-turn
10186
+ * — e.g. the follow-up request that reports recent_actions back to the backend
10187
+ * fails (timeout/5xx/auth) AFTER a tx has already broadcast and `onTxStatus`
10188
+ * fired. Lets the caller still surface the already-broadcast tx hash in the
10189
+ * error envelope instead of stranding funds the turn just moved.
10190
+ */
10191
+ partialResult() {
10192
+ return {
10193
+ sessionId: this.session.getConversationId() || "",
10194
+ response: this.responseParts[this.responseParts.length - 1] || "",
10195
+ toolCalls: this.toolCalls,
10196
+ transactions: this.transactions,
10197
+ cards: this.cards,
10198
+ warnings: this.warnings,
10199
+ error: this.error,
10200
+ ...this.outcome ? { outcome: this.outcome } : {}
10201
+ };
10035
10202
  }
10036
10203
  };
10037
10204
 
10038
- // ../../node_modules/viem/_esm/utils/typedData.js
10039
- init_isAddress();
10040
- init_size();
10041
- init_toHex();
10042
- init_regex();
10043
- function validateTypedData(parameters) {
10044
- const { domain, message, primaryType, types } = parameters;
10045
- const validateData = (struct, data) => {
10046
- for (const param of struct) {
10047
- const { name, type } = param;
10048
- const value = data[name];
10049
- const integerMatch = type.match(integerRegex);
10050
- if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
10051
- const [_type, base, size_] = integerMatch;
10052
- numberToHex(value, {
10053
- signed: base === "int",
10054
- size: Number.parseInt(size_, 10) / 8
10055
- });
10205
+ // src/agent/auth.ts
10206
+ import { randomBytes as randomBytes3 } from "node:crypto";
10207
+ import { Chain as Chain10, computePersonalSignHash, formatEcdsaSignature65 } from "@vultisig/sdk";
10208
+ async function authenticateVault(client, vault, password, maxAttempts = 3) {
10209
+ const publicKey = vault.publicKeys.ecdsa;
10210
+ const chainCode = vault.hexChainCode;
10211
+ const ethAddress = await vault.address(Chain10.Ethereum);
10212
+ const nonce = "0x" + randomBytes3(16).toString("hex");
10213
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
10214
+ const authMessage = JSON.stringify({
10215
+ message: "Sign into Vultisig Plugin Marketplace",
10216
+ nonce,
10217
+ expiresAt,
10218
+ address: ethAddress
10219
+ });
10220
+ const messageHash = computePersonalSignHash(authMessage);
10221
+ let lastError = null;
10222
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
10223
+ try {
10224
+ if (attempt > 1) {
10225
+ process.stderr.write(` Retry ${attempt}/${maxAttempts}...
10226
+ `);
10056
10227
  }
10057
- if (type === "address" && typeof value === "string" && !isAddress(value))
10058
- throw new InvalidAddressError2({ address: value });
10059
- const bytesMatch = type.match(bytesRegex);
10060
- if (bytesMatch) {
10061
- const [_type, size_] = bytesMatch;
10062
- if (size_ && size(value) !== Number.parseInt(size_, 10))
10063
- throw new BytesSizeMismatchError({
10064
- expectedSize: Number.parseInt(size_, 10),
10065
- givenSize: size(value)
10066
- });
10228
+ const signature = await vault.signBytes({ data: Buffer.from(messageHash), chain: Chain10.Ethereum }, {});
10229
+ if (signature.recovery === void 0) {
10230
+ throw new Error("Agent authentication requires an ECDSA recovery id");
10067
10231
  }
10068
- const struct2 = types[type];
10069
- if (struct2) {
10070
- validateReference(type);
10071
- validateData(struct2, value);
10232
+ const sigHex = formatEcdsaSignature65(signature.signature, signature.recovery);
10233
+ const authResponse = await client.authenticate({
10234
+ public_key: publicKey,
10235
+ chain_code_hex: chainCode,
10236
+ message: authMessage,
10237
+ signature: sigHex
10238
+ });
10239
+ return {
10240
+ token: authResponse.token,
10241
+ expiresAt: authResponse.expires_at,
10242
+ // Captured + persisted by the session token cache. The backend exposes
10243
+ // POST /auth/refresh to exchange it for a fresh access token without a
10244
+ // new MPC round; wiring that exchange is a future enhancement — today
10245
+ // the CLI re-auths via a full MPC re-sign (authenticateVault), which is
10246
+ // always available and avoids depending on refresh-token rotation.
10247
+ refreshToken: authResponse.refresh_token
10248
+ };
10249
+ } catch (err) {
10250
+ lastError = err;
10251
+ if (attempt < maxAttempts && err.message?.includes("timeout")) {
10252
+ continue;
10072
10253
  }
10254
+ throw err;
10073
10255
  }
10074
- };
10075
- if (types.EIP712Domain && domain) {
10076
- if (typeof domain !== "object")
10077
- throw new InvalidDomainError({ domain });
10078
- validateData(types.EIP712Domain, domain);
10079
- }
10080
- if (primaryType !== "EIP712Domain") {
10081
- if (types[primaryType])
10082
- validateData(types[primaryType], message);
10083
- else
10084
- throw new InvalidPrimaryTypeError({ primaryType, types });
10085
10256
  }
10086
- }
10087
- function getTypesForEIP712Domain({ domain }) {
10088
- return [
10089
- typeof domain?.name === "string" && { name: "name", type: "string" },
10090
- domain?.version && { name: "version", type: "string" },
10091
- (typeof domain?.chainId === "number" || typeof domain?.chainId === "bigint") && {
10092
- name: "chainId",
10093
- type: "uint256"
10094
- },
10095
- domain?.verifyingContract && {
10096
- name: "verifyingContract",
10097
- type: "address"
10098
- },
10099
- domain?.salt && { name: "salt", type: "bytes32" }
10100
- ].filter(Boolean);
10101
- }
10102
- function validateReference(type) {
10103
- if (type === "address" || type === "bool" || type === "string" || type.startsWith("bytes") || type.startsWith("uint") || type.startsWith("int"))
10104
- throw new InvalidStructTypeError({ type });
10257
+ throw lastError || new Error("Authentication failed after all attempts");
10105
10258
  }
10106
10259
 
10107
- // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
10108
- function hashTypedData(parameters) {
10109
- const { domain = {}, message, primaryType } = parameters;
10110
- const types = {
10111
- EIP712Domain: getTypesForEIP712Domain({ domain }),
10112
- ...parameters.types
10260
+ // src/agent/cards.ts
10261
+ import chalk8 from "chalk";
10262
+ var CLI_SUPPORTED_SURFACES = ["balance_summary", "turn_outcome"];
10263
+ function parseTurnOutcome(raw) {
10264
+ if (!raw || typeof raw !== "object") return null;
10265
+ const kind = raw.kind;
10266
+ if (kind !== "success" && kind !== "blocked" && kind !== "refusal" && kind !== "error") return null;
10267
+ const code = raw.code;
10268
+ const detail = raw.detail;
10269
+ return {
10270
+ kind,
10271
+ ...typeof code === "string" ? { code } : {},
10272
+ ...typeof detail === "string" ? { detail } : {}
10113
10273
  };
10114
- validateTypedData({
10115
- domain,
10116
- message,
10117
- primaryType,
10118
- types
10119
- });
10120
- const parts = ["0x1901"];
10121
- if (domain)
10122
- parts.push(hashDomain({
10123
- domain,
10124
- types
10125
- }));
10126
- if (primaryType !== "EIP712Domain")
10127
- parts.push(hashStruct({
10128
- data: message,
10129
- primaryType,
10130
- types
10131
- }));
10132
- return keccak256(concat(parts));
10133
10274
  }
10134
- function hashDomain({ domain, types }) {
10135
- return hashStruct({
10136
- data: domain,
10137
- primaryType: "EIP712Domain",
10138
- types
10139
- });
10275
+ function stripControlChars(s) {
10276
+ let out = "";
10277
+ for (const ch of s) {
10278
+ const code = ch.codePointAt(0) ?? 0;
10279
+ if (code <= 31 || code >= 127 && code <= 159) continue;
10280
+ out += ch;
10281
+ }
10282
+ return out;
10140
10283
  }
10141
- function hashStruct({ data, primaryType, types }) {
10142
- const encoded = encodeData({
10143
- data,
10144
- primaryType,
10145
- types
10146
- });
10147
- return keccak256(encoded);
10284
+ function asString(v) {
10285
+ return typeof v === "string" ? stripControlChars(v) : "";
10148
10286
  }
10149
- function encodeData({ data, primaryType, types }) {
10150
- const encodedTypes = [{ type: "bytes32" }];
10151
- const encodedValues = [hashType({ primaryType, types })];
10152
- for (const field of types[primaryType]) {
10153
- const [type, value] = encodeField({
10154
- types,
10155
- name: field.name,
10156
- type: field.type,
10157
- value: data[field.name]
10158
- });
10159
- encodedTypes.push(type);
10160
- encodedValues.push(value);
10161
- }
10162
- return encodeAbiParameters(encodedTypes, encodedValues);
10287
+ function parseToken(v) {
10288
+ if (!v || typeof v !== "object") return null;
10289
+ const o = v;
10290
+ const symbol = asString(o.symbol);
10291
+ const amountDecimal = asString(o.amountDecimal);
10292
+ if (!symbol && !amountDecimal) return null;
10293
+ const token = { symbol, amountDecimal };
10294
+ const amountUsd = asString(o.amountUsd);
10295
+ if (amountUsd) token.amountUsd = amountUsd;
10296
+ return token;
10163
10297
  }
10164
- function hashType({ primaryType, types }) {
10165
- const encodedHashType = toHex(encodeType({ primaryType, types }));
10166
- return keccak256(encodedHashType);
10298
+ function parseAccount(v) {
10299
+ if (!v || typeof v !== "object") return null;
10300
+ const o = v;
10301
+ const chainId = asString(o.chainId);
10302
+ if (!chainId) return null;
10303
+ const tokensRaw = Array.isArray(o.tokens) ? o.tokens : [];
10304
+ const tokens = tokensRaw.map(parseToken).filter((t) => t !== null);
10305
+ return { chainId, address: asString(o.address) || "\u2014", tokens };
10167
10306
  }
10168
- function encodeType({ primaryType, types }) {
10169
- let result = "";
10170
- const unsortedDeps = findTypeDependencies({ primaryType, types });
10171
- unsortedDeps.delete(primaryType);
10172
- const deps = [primaryType, ...Array.from(unsortedDeps).sort()];
10173
- for (const type of deps) {
10174
- result += `${type}(${types[type].map(({ name, type: t }) => `${t} ${name}`).join(",")})`;
10307
+ function parseBalanceSummaryEnvelope(value) {
10308
+ if (!value || typeof value !== "object") return null;
10309
+ const o = value;
10310
+ if (o.surface !== "balance_summary") return null;
10311
+ if (!Array.isArray(o.accounts)) return null;
10312
+ const accounts = o.accounts.map(parseAccount).filter((a) => a !== null);
10313
+ if (accounts.length === 0) return null;
10314
+ const card = { surface: "balance_summary", accounts };
10315
+ if (o.stale === true) {
10316
+ card.stale = true;
10317
+ if (typeof o.stale_secs === "number") card.staleSecs = o.stale_secs;
10175
10318
  }
10176
- return result;
10319
+ return card;
10177
10320
  }
10178
- function findTypeDependencies({ primaryType: primaryType_, types }, results = /* @__PURE__ */ new Set()) {
10179
- const match = primaryType_.match(/^\w*/u);
10180
- const primaryType = match?.[0];
10181
- if (results.has(primaryType) || types[primaryType] === void 0) {
10182
- return results;
10321
+ function matchBrace(text, start) {
10322
+ let depth = 0;
10323
+ let inString = false;
10324
+ let escaped = false;
10325
+ for (let i = start; i < text.length; i++) {
10326
+ const ch = text[i];
10327
+ if (inString) {
10328
+ if (escaped) escaped = false;
10329
+ else if (ch === "\\") escaped = true;
10330
+ else if (ch === '"') inString = false;
10331
+ continue;
10332
+ }
10333
+ if (ch === '"') inString = true;
10334
+ else if (ch === "{") depth++;
10335
+ else if (ch === "}") {
10336
+ depth--;
10337
+ if (depth === 0) return i;
10338
+ }
10183
10339
  }
10184
- results.add(primaryType);
10185
- for (const field of types[primaryType]) {
10186
- findTypeDependencies({ primaryType: field.type, types }, results);
10340
+ return -1;
10341
+ }
10342
+ function extractBalanceSummaryFromText(content) {
10343
+ if (!content || !content.includes("balance_summary")) return null;
10344
+ if (content.length > 2e5) return null;
10345
+ for (let i = content.indexOf("{"); i !== -1; i = content.indexOf("{", i + 1)) {
10346
+ const end = matchBrace(content, i);
10347
+ if (end === -1) break;
10348
+ const blob = content.slice(i, end + 1);
10349
+ if (!blob.includes("balance_summary")) continue;
10350
+ let parsed;
10351
+ try {
10352
+ parsed = JSON.parse(blob);
10353
+ } catch {
10354
+ continue;
10355
+ }
10356
+ const card = parseBalanceSummaryEnvelope(parsed);
10357
+ if (!card) continue;
10358
+ const before = content.slice(0, i).replace(/```(?:json)?\s*$/i, "");
10359
+ const after = content.slice(end + 1).replace(/^\s*```/, "");
10360
+ const remainingText = (before + after).trim();
10361
+ return { card, remainingText };
10187
10362
  }
10188
- return results;
10363
+ return null;
10189
10364
  }
10190
- function encodeField({ types, name, type, value }) {
10191
- if (types[type] !== void 0) {
10192
- return [
10193
- { type: "bytes32" },
10194
- keccak256(encodeData({ data: value, primaryType: type, types }))
10195
- ];
10365
+ function shortenAddress(address) {
10366
+ if (!address || address === "\u2014") return address || "\u2014";
10367
+ if (address.length <= 16) return address;
10368
+ return `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
10369
+ }
10370
+ function parseUsd(amountUsd) {
10371
+ if (!amountUsd) return null;
10372
+ const cleaned = amountUsd.replace(/[$,\s]/g, "");
10373
+ if (!cleaned) return null;
10374
+ const n = Number(cleaned);
10375
+ return Number.isFinite(n) ? n : null;
10376
+ }
10377
+ function formatUsd(n) {
10378
+ return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
10379
+ }
10380
+ function renderBalanceSummaryCard(card) {
10381
+ const lines = [];
10382
+ const staleCue = card.stale ? chalk8.gray(` (stale${card.staleSecs ? ` ~${Math.round(card.staleSecs / 60)}m` : ""}, refreshing\u2026)`) : "";
10383
+ lines.push(chalk8.bold(" Balances") + staleCue);
10384
+ let total = 0;
10385
+ let sawUsd = false;
10386
+ for (const account of card.accounts) {
10387
+ lines.push(` ${chalk8.cyan(account.chainId)} ${chalk8.gray(`(${shortenAddress(account.address)})`)}`);
10388
+ if (account.tokens.length === 0) {
10389
+ lines.push(chalk8.gray(" (no balances)"));
10390
+ continue;
10391
+ }
10392
+ for (const token of account.tokens) {
10393
+ const usd = parseUsd(token.amountUsd);
10394
+ if (usd !== null) {
10395
+ total += usd;
10396
+ sawUsd = true;
10397
+ }
10398
+ const symbol = token.symbol.padEnd(10);
10399
+ const amount = token.amountDecimal.padStart(16);
10400
+ const usdCol = token.amountUsd ? chalk8.gray(` ${token.amountUsd}`) : "";
10401
+ lines.push(` ${chalk8.bold(symbol)}${amount}${usdCol}`);
10402
+ }
10196
10403
  }
10197
- if (type === "bytes")
10198
- return [{ type: "bytes32" }, keccak256(value)];
10199
- if (type === "string")
10200
- return [{ type: "bytes32" }, keccak256(toHex(value))];
10201
- if (type.lastIndexOf("]") === type.length - 1) {
10202
- const parsedType = type.slice(0, type.lastIndexOf("["));
10203
- const typeValuePairs = value.map((item) => encodeField({
10204
- name,
10205
- type: parsedType,
10206
- types,
10207
- value: item
10208
- }));
10209
- return [
10210
- { type: "bytes32" },
10211
- keccak256(encodeAbiParameters(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v)))
10212
- ];
10404
+ if (sawUsd) {
10405
+ lines.push(chalk8.gray(" " + "\u2500".repeat(36)));
10406
+ lines.push(` ${chalk8.bold("Total")} ${chalk8.green(formatUsd(total))}`);
10213
10407
  }
10214
- return [{ type }, value];
10408
+ return lines.join("\n");
10215
10409
  }
10216
10410
 
10217
- // ../../node_modules/viem/_esm/index.js
10218
- init_formatUnits();
10411
+ // src/agent/client.ts
10412
+ import { randomUUID } from "node:crypto";
10413
+
10414
+ // src/agent/toolOutputSigning.ts
10415
+ import { getChainKind as getChainKind4 } from "@vultisig/sdk";
10416
+
10417
+ // src/agent/executor.ts
10418
+ import {
10419
+ Chain as Chain11,
10420
+ chainFeeCoin,
10421
+ getChainKind as getChainKind3,
10422
+ parseThorSwapMemo,
10423
+ resolveChainReference,
10424
+ VaultError as VaultError3,
10425
+ VaultErrorCode as VaultErrorCode3,
10426
+ Vultisig as VultisigSdk
10427
+ } from "@vultisig/sdk";
10219
10428
 
10220
10429
  // src/core/VaultStateStore.ts
10221
10430
  import * as fs3 from "node:fs";
10222
10431
  import * as path3 from "node:path";
10223
- import { getVultisigConfigDir } from "@vultisig/client-shared";
10432
+ import { getVultisigConfigDir as getVultisigConfigDir2 } from "@vultisig/client-shared";
10224
10433
  var LOCK_STALE_MS = 6e4;
10225
10434
  var LOCK_RETRY_INIT_MS = 100;
10226
10435
  var LOCK_MAX_WAIT_MS = 3e4;
@@ -10232,7 +10441,7 @@ var VaultStateStore = class {
10232
10441
  if (!safeId) {
10233
10442
  throw new Error("Invalid vaultId: must contain alphanumeric characters");
10234
10443
  }
10235
- this.baseDir = path3.join(getVultisigConfigDir(), "vault-state", safeId);
10444
+ this.baseDir = path3.join(getVultisigConfigDir2(), "vault-state", safeId);
10236
10445
  fs3.mkdirSync(this.baseDir, { recursive: true });
10237
10446
  }
10238
10447
  // -------------------------------------------------------------------------
@@ -10373,45 +10582,6 @@ function sleep2(ms) {
10373
10582
  }
10374
10583
 
10375
10584
  // src/agent/executor.ts
10376
- var THOR_MEMO_CHAIN_TO_ENUM = {
10377
- BTC: Chain11.Bitcoin,
10378
- ETH: Chain11.Ethereum,
10379
- BSC: Chain11.BSC,
10380
- AVAX: Chain11.Avalanche,
10381
- BASE: Chain11.Base,
10382
- // L2 — THORChain routinely quotes Base destinations (PR #439 review finding 1)
10383
- ARB: Chain11.Arbitrum,
10384
- // L1-via-bridge path (PR #439 review finding 1)
10385
- BCH: Chain11.BitcoinCash,
10386
- LTC: Chain11.Litecoin,
10387
- DOGE: Chain11.Dogecoin,
10388
- GAIA: Chain11.Cosmos,
10389
- THOR: Chain11.THORChain,
10390
- RUNE: Chain11.THORChain,
10391
- XRP: Chain11.Ripple,
10392
- DASH: Chain11.Dash,
10393
- ZEC: Chain11.Zcash,
10394
- MAYA: Chain11.MayaChain,
10395
- CACAO: Chain11.MayaChain
10396
- };
10397
- var THOR_MEMO_ASSET_SHORTCUTS = {
10398
- b: "BTC.BTC",
10399
- e: "ETH.ETH",
10400
- s: "BSC.BNB",
10401
- a: "AVAX.AVAX",
10402
- c: "BCH.BCH",
10403
- l: "LTC.LTC",
10404
- d: "DOGE.DOGE",
10405
- g: "GAIA.ATOM",
10406
- r: "THOR.RUNE",
10407
- x: "XRP.XRP",
10408
- cacao: "MAYA.CACAO",
10409
- dash: "DASH.DASH",
10410
- zec: "ZEC.ZEC"
10411
- // BASE / ARB don't have documented single-letter shortcuts; THORChain
10412
- // emits these as the full CHAIN.ASSET form in memos. Listed in
10413
- // THOR_MEMO_CHAIN_TO_ENUM only.
10414
- };
10415
10585
  var EVM_CHAINS = /* @__PURE__ */ new Set([
10416
10586
  "Ethereum",
10417
10587
  "BSC",
@@ -11072,13 +11242,7 @@ var AgentExecutor = class {
11072
11242
  const txArgs = serverTxData?.txArgs ?? {};
11073
11243
  const memo = typeof txArgs.memo === "string" ? txArgs.memo : "";
11074
11244
  const parsed = parseThorSwapMemo(memo);
11075
- const toChain = THOR_MEMO_CHAIN_TO_ENUM[parsed.destChainCode];
11076
- if (!toChain) {
11077
- throw new VaultError3(
11078
- VaultErrorCode3.UnsupportedChain,
11079
- `signThorMsgDepositSwap: unsupported destination chain code '${parsed.destChainCode}' in memo '${memo}'.`
11080
- );
11081
- }
11245
+ const toChain = parsed.toChain;
11082
11246
  const vaultDestAddress = await this.vault.address(toChain);
11083
11247
  const normalizeForCompare = (addr) => EVM_CHAINS.has(toChain) ? addr.toLowerCase() : addr;
11084
11248
  if (parsed.destAddress && normalizeForCompare(parsed.destAddress) !== normalizeForCompare(vaultDestAddress)) {
@@ -11887,31 +12051,6 @@ function parseNonEvmEnvelope(serverTxData, chain) {
11887
12051
  const memo = typeof txArgs.memo === "string" && txArgs.memo.length > 0 ? txArgs.memo : void 0;
11888
12052
  return { chain, to, amount: amountDecimal, symbol, memo };
11889
12053
  }
11890
- function parseThorSwapMemo(memo) {
11891
- if (!memo.startsWith("=:")) {
11892
- throw new VaultError3(
11893
- VaultErrorCode3.NotImplemented,
11894
- `parseThorSwapMemo: only swap memos (=:CHAIN.ASSET:DEST...) supported on this path; got memo='${memo}'. LP memos (+:/-:) route through signThorMsgDepositLp; loan / validator ops out of scope.`
11895
- );
11896
- }
11897
- const memoBody = memo.slice(2);
11898
- const parts = memoBody.split(":");
11899
- let chainAsset = parts[0];
11900
- if (chainAsset && !chainAsset.includes(".")) {
11901
- const expanded = THOR_MEMO_ASSET_SHORTCUTS[chainAsset.toLowerCase()];
11902
- if (expanded) chainAsset = expanded;
11903
- }
11904
- if (!chainAsset || !chainAsset.includes(".")) {
11905
- throw new VaultError3(
11906
- VaultErrorCode3.InvalidConfig,
11907
- `parseThorSwapMemo: malformed swap memo '${memo}': missing CHAIN.ASSET in first segment.`
11908
- );
11909
- }
11910
- const [destChainCode, destAssetRaw] = chainAsset.split(".");
11911
- const destAsset = destAssetRaw?.split("-")[0] ?? "";
11912
- const destAddress = typeof parts[1] === "string" ? parts[1] : "";
11913
- return { destChainCode, destAsset, destAddress };
11914
- }
11915
12054
  function resolveChainId(chainId) {
11916
12055
  return resolveChainReference(chainId) ?? null;
11917
12056
  }
@@ -13348,13 +13487,13 @@ import {
13348
13487
  statSync as statSync3,
13349
13488
  writeFileSync as writeFileSync3
13350
13489
  } from "node:fs";
13351
- import { homedir as homedir2 } from "node:os";
13490
+ import { homedir } from "node:os";
13352
13491
  import { dirname as dirname2, join as join3 } from "node:path";
13353
13492
  var LOCK_RETRY_MS = 25;
13354
13493
  var LOCK_MAX_WAIT_MS2 = 5e3;
13355
13494
  var LOCK_STALE_MS2 = 3e4;
13356
13495
  function getTokenCachePath() {
13357
- const dir = process.env.VULTISIG_CONFIG_DIR ?? join3(homedir2(), ".vultisig");
13496
+ const dir = process.env.VULTISIG_CONFIG_DIR ?? join3(homedir(), ".vultisig");
13358
13497
  return join3(dir, "agent-tokens.json");
13359
13498
  }
13360
13499
  function tokenCacheKey(scope) {
@@ -15124,7 +15263,7 @@ function formatDate(iso) {
15124
15263
  }
15125
15264
 
15126
15265
  // src/lib/version.ts
15127
- import { getVultisigConfigDir as getVultisigConfigDir2 } from "@vultisig/client-shared";
15266
+ import { getVultisigConfigDir as getVultisigConfigDir3 } from "@vultisig/client-shared";
15128
15267
  import chalk11 from "chalk";
15129
15268
  import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
15130
15269
  import { join as join4 } from "path";
@@ -15132,7 +15271,7 @@ var cachedVersion = null;
15132
15271
  function getVersion() {
15133
15272
  if (cachedVersion) return cachedVersion;
15134
15273
  if (true) {
15135
- cachedVersion = "2.22.0";
15274
+ cachedVersion = "3.0.0";
15136
15275
  return cachedVersion;
15137
15276
  }
15138
15277
  try {
@@ -15145,7 +15284,7 @@ function getVersion() {
15145
15284
  return cachedVersion;
15146
15285
  }
15147
15286
  }
15148
- var CACHE_DIR = join4(getVultisigConfigDir2(), "cache");
15287
+ var CACHE_DIR = join4(getVultisigConfigDir3(), "cache");
15149
15288
  var VERSION_CACHE_FILE = join4(CACHE_DIR, "version-check.json");
15150
15289
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
15151
15290
  function readVersionCache() {
@@ -15229,7 +15368,7 @@ function formatVersionShort() {
15229
15368
  }
15230
15369
  function formatVersionDetailed() {
15231
15370
  const lines = [];
15232
- const configDir = getVultisigConfigDir2();
15371
+ const configDir = getVultisigConfigDir3();
15233
15372
  lines.push(chalk11.bold(`Vultisig CLI v${getVersion()}`));
15234
15373
  lines.push("");
15235
15374
  lines.push(` Node.js: ${process.version}`);
@@ -16529,7 +16668,7 @@ Error: ${error2.message}`));
16529
16668
  const [fromChainStr, toChainStr, amountStr, ...rest] = args;
16530
16669
  const fromChain = findChainByName(fromChainStr) || fromChainStr;
16531
16670
  const toChain = findChainByName(toChainStr) || toChainStr;
16532
- const amount = parseFloat(amountStr);
16671
+ const amount = amountStr;
16533
16672
  let fromToken;
16534
16673
  let toToken;
16535
16674
  for (let i = 0; i < rest.length; i++) {
@@ -16555,7 +16694,7 @@ Error: ${error2.message}`));
16555
16694
  const [fromChainStr, toChainStr, amountStr, ...rest] = args;
16556
16695
  const fromChain = findChainByName(fromChainStr) || fromChainStr;
16557
16696
  const toChain = findChainByName(toChainStr) || toChainStr;
16558
- const amount = parseFloat(amountStr);
16697
+ const amount = amountStr;
16559
16698
  let fromToken;
16560
16699
  let toToken;
16561
16700
  let slippage;
@@ -16669,10 +16808,10 @@ Error: ${error2.message}`));
16669
16808
  };
16670
16809
 
16671
16810
  // src/lib/completion.ts
16811
+ import { getVultisigConfigDir as getVultisigConfigDir4 } from "@vultisig/client-shared";
16672
16812
  import { SUPPORTED_CHAINS as SUPPORTED_CHAINS2 } from "@vultisig/sdk";
16673
16813
  import { program } from "commander";
16674
16814
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5 } from "fs";
16675
- import { homedir as homedir3 } from "os";
16676
16815
  import { join as join5 } from "path";
16677
16816
  var tabtab = null;
16678
16817
  async function getTabtab() {
@@ -16696,7 +16835,7 @@ function getChains() {
16696
16835
  }
16697
16836
  function getVaultNames() {
16698
16837
  try {
16699
- const vaultDir = join5(homedir3(), ".vultisig", "vaults");
16838
+ const vaultDir = getVultisigConfigDir4();
16700
16839
  if (!existsSync3(vaultDir)) return [];
16701
16840
  const files = readdirSync(vaultDir);
16702
16841
  const names = [];
@@ -16704,9 +16843,10 @@ function getVaultNames() {
16704
16843
  if (file.startsWith("vault:") && file.endsWith(".json")) {
16705
16844
  try {
16706
16845
  const content = readFileSync5(join5(vaultDir, file), "utf-8");
16707
- const vault = JSON.parse(content);
16708
- if (vault.name) names.push(vault.name);
16709
- if (vault.id) names.push(vault.id);
16846
+ const parsed = JSON.parse(content);
16847
+ const vault = parsed?.value ?? parsed;
16848
+ if (vault?.name) names.push(vault.name);
16849
+ if (vault?.id) names.push(vault.id);
16710
16850
  } catch {
16711
16851
  }
16712
16852
  }
@@ -16948,12 +17088,10 @@ complete -c vsig -n "__fish_seen_subcommand_from import export" -a "(__fish_comp
16948
17088
  }
16949
17089
 
16950
17090
  // src/lib/config.ts
17091
+ import { getVultisigConfigDir as getVultisigConfigDir5 } from "@vultisig/client-shared";
16951
17092
  import { FileStorage } from "@vultisig/sdk/node";
16952
- import { homedir as homedir4 } from "os";
16953
- import { join as join6 } from "path";
16954
17093
  function getConfigDir() {
16955
- const override = process.env.VULTISIG_CONFIG_DIR?.trim();
16956
- return override ? override : join6(homedir4(), ".vultisig");
17094
+ return getVultisigConfigDir5();
16957
17095
  }
16958
17096
  function createVaultStorage() {
16959
17097
  return new FileStorage({ basePath: getConfigDir() });
@@ -17511,9 +17649,13 @@ program2.command("delete [vault]").description("Delete a vault from local storag
17511
17649
  });
17512
17650
  })
17513
17651
  );
17514
- program2.command("tokens <chain>").description("List and manage tokens for a chain").option("--add <contractAddress>", "Add a token by contract address").option("--remove <tokenId>", "Remove a token by ID").option("--discover", "Auto-discover tokens with balances on the chain").addHelpText(
17652
+ program2.command("tokens <chain>").description("List and manage tokens for a chain").option("--add <contractAddress>", "Add a token by contract address").option("--remove <tokenId>", "Remove a token by ID").option("--discover", "Find tokens with balances on the chain and save them to this vault").addHelpText(
17515
17653
  "after",
17516
17654
  `
17655
+ --discover writes to the vault: every token it finds is saved to the tracked
17656
+ list, so it also changes what portfolio and balance --tokens report. Use
17657
+ --remove <tokenId> to stop tracking one.
17658
+
17517
17659
  Examples:
17518
17660
  vultisig tokens Ethereum
17519
17661
  vultisig tokens Ethereum --discover --output json
@@ -17558,7 +17700,7 @@ Examples:
17558
17700
  await executeSwapQuote(context, {
17559
17701
  fromChain,
17560
17702
  toChain,
17561
- amount: options.max ? "max" : parseFloat(amountStr),
17703
+ amount: options.max ? "max" : amountStr,
17562
17704
  fromToken: options.fromToken,
17563
17705
  toToken: options.toToken
17564
17706
  });
@@ -17583,7 +17725,7 @@ See also: swap-quote, swap-chains, balance`
17583
17725
  await executeSwap(context, {
17584
17726
  fromChain: findChainByName(fromChainStr) || fromChainStr,
17585
17727
  toChain: findChainByName(toChainStr) || toChainStr,
17586
- amount: options.max ? "max" : parseFloat(amountStr),
17728
+ amount: options.max ? "max" : amountStr,
17587
17729
  fromToken: options.fromToken,
17588
17730
  toToken: options.toToken,
17589
17731
  slippage: options.slippage ? parseFloat(options.slippage) : void 0,