@vultisig/cli 2.21.1 → 2.23.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 +58 -0
  2. package/dist/index.js +1423 -1226
  3. package/package.json +4 -4
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);
@@ -5121,7 +5121,15 @@ var EVM_PERMANENT_BROADCAST_INPUT_RE = /failed to decode signed transaction|coul
5121
5121
  var UTXO_PERMANENT_BROADCAST_INPUT_RE = /\bTX decode failed\b/i;
5122
5122
  var SOLANA_PERMANENT_BROADCAST_INPUT_RE = /failed to deserialize(?: transaction)?|failed to sanitize|(?:transaction )?signature verification (?:failed|failure)|non-base58 character|invalid base58/i;
5123
5123
  var SUI_REQUIRED_FIELDS_GUARD_RE = /Sui broadcast requires JSON with "unsignedTx" and "signature" fields/i;
5124
- var SUI_PERMANENT_EXECUTION_MESSAGE_RE = /invalid (?:user )?signature|signature verification (?:failed|failure)|malformed transaction|invalid transaction (?:data|bytes)/i;
5124
+ var SUI_PERMANENT_GRPC_STATUS = "INVALID_ARGUMENT";
5125
+ var SUI_PERMANENT_EXECUTION_MESSAGE_RE = /invalid (?:user )?signature|signature verification (?:failed|failure)|malformed transaction|invalid transaction(?: (?:data|bytes))?\b/i;
5126
+ var decodeRpcDetails = (details) => {
5127
+ try {
5128
+ return decodeURIComponent(details);
5129
+ } catch {
5130
+ return details;
5131
+ }
5132
+ };
5125
5133
  var COSMOS_PERMANENT_SDK_CODES = {
5126
5134
  2: /tx parse error/i,
5127
5135
  4: /unauthorized|signature verification failed/i,
@@ -5151,7 +5159,8 @@ var permanentBroadcastInputClassifiers = {
5151
5159
  sui: (err, details) => {
5152
5160
  if (SUI_REQUIRED_FIELDS_GUARD_RE.test(details)) return true;
5153
5161
  const rpcCode = err.originalError?.code;
5154
- return rpcCode === -32002 && SUI_PERMANENT_EXECUTION_MESSAGE_RE.test(details);
5162
+ if (rpcCode !== SUI_PERMANENT_GRPC_STATUS) return false;
5163
+ return SUI_PERMANENT_EXECUTION_MESSAGE_RE.test(details) || SUI_PERMANENT_EXECUTION_MESSAGE_RE.test(decodeRpcDetails(details));
5155
5164
  },
5156
5165
  cosmos: (_err, details) => isCosmosPermanentBroadcastInput(details)
5157
5166
  };
@@ -8881,1332 +8890,1479 @@ Please specify more characters of the vault ID.`
8881
8890
  throw new Error(`Vault not found: "${idOrName}"`);
8882
8891
  }
8883
8892
 
8884
- // src/commands/swap.ts
8885
- async function executeSwapChains(ctx2) {
8886
- const vault = await ctx2.ensureActiveVault();
8887
- const spinner = createSpinner("Loading supported swap chains...");
8888
- const chains = await vault.getSupportedSwapChains();
8889
- spinner.succeed("Swap chains loaded");
8890
- if (isJsonOutput()) {
8891
- outputJson({ swapChains: [...chains] });
8892
- return chains;
8893
- }
8894
- displaySwapChains(chains);
8895
- return chains;
8896
- }
8897
- async function executeSwapQuote(ctx2, options) {
8898
- const vault = await ctx2.ensureActiveVault();
8899
- const isMax = options.amount === "max";
8900
- if (!isMax && (isNaN(options.amount) || options.amount <= 0)) {
8901
- throw new Error("Invalid amount");
8902
- }
8903
- const spinner = createSpinner("Getting swap quote...");
8904
- const result = await vault.swap({
8905
- fromChain: options.fromChain,
8906
- fromSymbol: options.fromToken || "",
8907
- toChain: options.toChain,
8908
- toSymbol: options.toToken || "",
8909
- amount: isMax ? "max" : String(options.amount),
8910
- dryRun: true
8911
- });
8912
- if (!result.dryRun) throw new Error("unreachable");
8913
- spinner.succeed("Quote received");
8914
- const quote = result.quote;
8915
- const fromAmountDisplay = isMax ? `${formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals)} (max)` : String(options.amount);
8916
- if (isJsonOutput()) {
8917
- outputJson({
8918
- fromChain: options.fromChain,
8919
- toChain: options.toChain,
8920
- amount: isMax ? "max" : options.amount,
8921
- isMax,
8922
- quote
8923
- });
8924
- return quote;
8925
- }
8926
- const feeBalance = await vault.balance(options.fromChain);
8927
- const discountTier = await vault.getDiscountTier();
8928
- displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
8929
- fromDecimals: quote.fromCoin.decimals,
8930
- toDecimals: quote.toCoin.decimals,
8931
- feeDecimals: feeBalance.decimals,
8932
- feeSymbol: feeBalance.symbol,
8933
- discountTier
8934
- });
8935
- info('\nTo execute this swap, use the "swap" command');
8936
- return quote;
8937
- }
8938
- function validateSwapAmount(amount) {
8939
- if (amount === "max") return;
8940
- if (isNaN(amount) || amount <= 0) throw new Error("Invalid amount");
8941
- }
8942
- function getSwapAmountString(amount) {
8943
- return amount === "max" ? "max" : String(amount);
8944
- }
8945
- function toSwapRequest(options, amount, dryRun) {
8946
- return {
8947
- fromChain: options.fromChain,
8948
- fromSymbol: options.fromToken || "",
8949
- toChain: options.toChain,
8950
- toSymbol: options.toToken || "",
8951
- amount,
8952
- ...options.slippage !== void 0 && { slippageTolerance: options.slippage },
8953
- ...dryRun && { dryRun: true }
8954
- };
8955
- }
8956
- function toDryRunResult(options, quote, fromAmountRaw) {
8957
- const result = {
8958
- dryRun: true,
8959
- fromChain: String(options.fromChain),
8960
- fromToken: quote.fromCoin.ticker,
8961
- toChain: String(options.toChain),
8962
- toToken: quote.toCoin.ticker,
8963
- inputAmount: fromAmountRaw,
8964
- ...options.amount === "max" && { isMax: true },
8965
- estimatedOutput: formatBigintAmount(quote.estimatedOutput, quote.toCoin.decimals),
8966
- provider: quote.provider
8967
- };
8968
- if (quote.estimatedOutputFiat != null) result.estimatedOutputFiat = parseFloat(quote.estimatedOutputFiat.toFixed(2));
8969
- if (quote.requiresApproval) result.requiresApproval = true;
8970
- if (quote.warnings?.length) result.warnings = [...quote.warnings];
8971
- return result;
8893
+ // ../../node_modules/viem/_esm/accounts/utils/publicKeyToAddress.js
8894
+ init_getAddress();
8895
+ init_keccak256();
8896
+ function publicKeyToAddress(publicKey) {
8897
+ const address = keccak256(`0x${publicKey.substring(4)}`).substring(26);
8898
+ return checksumAddress(`0x${address}`);
8972
8899
  }
8973
- function displayDryRunResult(result) {
8974
- info(`
8975
- Dry-run preview:`);
8976
- info(` From: ${result.inputAmount} ${result.fromToken} (${result.fromChain})`);
8977
- info(` To: ${result.estimatedOutput} ${result.toToken} (${result.toChain})`);
8978
- info(` Provider: ${result.provider}`);
8979
- if (result.estimatedOutputFiat != null) info(` Est. value (USD): $${result.estimatedOutputFiat}`);
8980
- if (result.requiresApproval) info(` Requires approval: yes`);
8981
- if (result.warnings?.length) result.warnings.forEach((w) => warn(` Warning: ${w}`));
8900
+
8901
+ // ../../node_modules/viem/_esm/utils/signature/recoverPublicKey.js
8902
+ init_isHex();
8903
+ init_size();
8904
+ init_fromHex();
8905
+ init_toHex();
8906
+ async function recoverPublicKey({ hash, signature }) {
8907
+ const hashHex = isHex(hash) ? hash : toHex(hash);
8908
+ const { secp256k1: secp256k12 } = await Promise.resolve().then(() => (init_secp256k1(), secp256k1_exports));
8909
+ const signature_ = (() => {
8910
+ if (typeof signature === "object" && "r" in signature && "s" in signature) {
8911
+ const { r, s, v, yParity } = signature;
8912
+ const yParityOrV2 = Number(yParity ?? v);
8913
+ const recoveryBit2 = toRecoveryBit(yParityOrV2);
8914
+ return new secp256k12.Signature(hexToBigInt(r), hexToBigInt(s)).addRecoveryBit(recoveryBit2);
8915
+ }
8916
+ const signatureHex = isHex(signature) ? signature : toHex(signature);
8917
+ if (size(signatureHex) !== 65)
8918
+ throw new Error("invalid signature length");
8919
+ const yParityOrV = hexToNumber(`0x${signatureHex.slice(130)}`);
8920
+ const recoveryBit = toRecoveryBit(yParityOrV);
8921
+ return secp256k12.Signature.fromCompact(signatureHex.substring(2, 130)).addRecoveryBit(recoveryBit);
8922
+ })();
8923
+ const publicKey = signature_.recoverPublicKey(hashHex.substring(2)).toHex(false);
8924
+ return `0x${publicKey}`;
8982
8925
  }
8983
- function refuseSwapWhenNonInteractive() {
8984
- throw new ConfirmationRequiredError(
8985
- "Swap requires confirmation.",
8986
- "Pass --yes to confirm, or --dry-run to preview without signing."
8987
- );
8926
+ function toRecoveryBit(yParityOrV) {
8927
+ if (yParityOrV === 0 || yParityOrV === 1)
8928
+ return yParityOrV;
8929
+ if (yParityOrV === 27)
8930
+ return 0;
8931
+ if (yParityOrV === 28)
8932
+ return 1;
8933
+ throw new Error("Invalid yParityOrV value");
8988
8934
  }
8989
- async function confirmSwapIfNeeded(options) {
8990
- if (options.yes) return;
8991
- if (isNonInteractive()) {
8992
- refuseSwapWhenNonInteractive();
8993
- }
8994
- const confirmed = await confirmSwap();
8995
- if (!confirmed) {
8996
- throw new ConfirmationRequiredError("Swap declined at the confirmation prompt");
8997
- }
8935
+
8936
+ // ../../node_modules/viem/_esm/utils/signature/recoverAddress.js
8937
+ async function recoverAddress({ hash, signature }) {
8938
+ return publicKeyToAddress(await recoverPublicKey({ hash, signature }));
8998
8939
  }
8999
- async function executeSwap(ctx2, options) {
9000
- const vault = await ctx2.ensureActiveVault();
9001
- validateSwapAmount(options.amount);
9002
- const amountStr = getSwapAmountString(options.amount);
9003
- if (!options.dryRun && !options.yes && isNonInteractive()) {
9004
- refuseSwapWhenNonInteractive();
9005
- }
9006
- const quoteSpinner = createSpinner("Getting swap quote...");
9007
- const dryResult = await vault.swap(toSwapRequest(options, amountStr, true));
9008
- if (!dryResult.dryRun) throw new Error("unreachable");
9009
- quoteSpinner.succeed("Quote received");
9010
- const quote = dryResult.quote;
9011
- const fromAmountRaw = options.amount === "max" ? formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals) : String(options.amount);
9012
- const fromAmountDisplay = options.amount === "max" ? `${fromAmountRaw} (max)` : fromAmountRaw;
9013
- if (options.dryRun) {
9014
- const result = toDryRunResult(options, quote, fromAmountRaw);
9015
- if (isJsonOutput()) {
9016
- outputJson(result);
9017
- } else {
9018
- displayDryRunResult(result);
9019
- }
9020
- return result;
8940
+
8941
+ // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
8942
+ init_encodeAbiParameters();
8943
+ init_concat();
8944
+ init_toHex();
8945
+ init_keccak256();
8946
+
8947
+ // ../../node_modules/viem/_esm/utils/typedData.js
8948
+ init_abi();
8949
+ init_address();
8950
+
8951
+ // ../../node_modules/viem/_esm/errors/typedData.js
8952
+ init_stringify();
8953
+ init_base();
8954
+ var InvalidDomainError = class extends BaseError {
8955
+ constructor({ domain }) {
8956
+ super(`Invalid domain "${stringify(domain)}".`, {
8957
+ metaMessages: ["Must be a valid EIP-712 domain."]
8958
+ });
9021
8959
  }
9022
- const feeBalance = await vault.balance(options.fromChain);
9023
- const discountTier = await vault.getDiscountTier();
9024
- if (!isJsonOutput()) {
9025
- displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9026
- fromDecimals: quote.fromCoin.decimals,
9027
- toDecimals: quote.toCoin.decimals,
9028
- feeDecimals: feeBalance.decimals,
9029
- feeSymbol: feeBalance.symbol,
9030
- discountTier
8960
+ };
8961
+ var InvalidPrimaryTypeError = class extends BaseError {
8962
+ constructor({ primaryType, types }) {
8963
+ super(`Invalid primary type \`${primaryType}\` must be one of \`${JSON.stringify(Object.keys(types))}\`.`, {
8964
+ docsPath: "/api/glossary/Errors#typeddatainvalidprimarytypeerror",
8965
+ metaMessages: ["Check that the primary type is a key in `types`."]
9031
8966
  });
9032
8967
  }
9033
- await confirmSwapIfNeeded(options);
9034
- await ensureVaultUnlocked(vault, options.password);
9035
- const intent = buildSwapBroadcastIntent(vault, {
9036
- fromChain: options.fromChain,
9037
- toChain: options.toChain,
9038
- fromToken: options.fromToken,
9039
- toToken: options.toToken,
9040
- amount: fromAmountRaw,
9041
- isMax: options.amount === "max"
9042
- });
9043
- let signSpinner;
9044
- try {
9045
- const broadcast = await guardedBroadcast(intent, options.force ?? false, async () => {
9046
- signSpinner = createSpinner("Signing swap transaction...");
9047
- vault.on("signingProgress", ({ step }) => {
9048
- if (signSpinner) signSpinner.text = `${step.message} (${step.progress}%)`;
9049
- });
9050
- const result = await vault.swap(toSwapRequest(options, amountStr));
9051
- if (result.dryRun) throw new Error("unreachable");
9052
- return result;
8968
+ };
8969
+ var InvalidStructTypeError = class extends BaseError {
8970
+ constructor({ type }) {
8971
+ super(`Struct type "${type}" is invalid.`, {
8972
+ metaMessages: ["Struct type must not be a Solidity type."],
8973
+ name: "InvalidStructTypeError"
9053
8974
  });
9054
- signSpinner?.succeed(`Swap broadcast: ${broadcast.txHash}`);
9055
- if (isJsonOutput()) {
9056
- outputJson({
9057
- txHash: broadcast.txHash,
9058
- fromChain: options.fromChain,
9059
- toChain: options.toChain,
9060
- quote
9061
- });
9062
- } else {
9063
- displaySwapResult(options.fromChain, options.toChain, broadcast.txHash, quote, quote.toCoin.decimals);
9064
- }
9065
- return { txHash: broadcast.txHash, quote };
9066
- } catch (err) {
9067
- signSpinner?.stop();
9068
- throw err;
9069
- } finally {
9070
- vault.removeAllListeners("signingProgress");
9071
8975
  }
9072
- }
8976
+ };
9073
8977
 
9074
- // src/commands/settings.ts
9075
- import { Chain as Chain9, fiatCurrencies as fiatCurrencies2, fiatCurrencyNameRecord as fiatCurrencyNameRecord3 } from "@vultisig/sdk";
9076
- import chalk6 from "chalk";
9077
- async function executeCurrency(ctx2, newCurrency) {
9078
- const vault = await ctx2.ensureActiveVault();
9079
- if (!newCurrency) {
9080
- const currentCurrency = vault.currency;
9081
- const currencyName2 = fiatCurrencyNameRecord3[currentCurrency];
9082
- printResult(chalk6.cyan("\nCurrent Currency Preference:"));
9083
- printResult(` ${chalk6.green(currentCurrency.toUpperCase())} - ${currencyName2}`);
9084
- info(chalk6.gray(`
9085
- Supported currencies: ${fiatCurrencies2.join(", ")}`));
9086
- info(chalk6.gray('Use "vultisig currency <code>" to change'));
9087
- return currentCurrency;
9088
- }
9089
- const currency = newCurrency.toLowerCase();
9090
- if (!fiatCurrencies2.includes(currency)) {
9091
- error(`x Invalid currency: ${newCurrency}`);
9092
- warn(`Supported currencies: ${fiatCurrencies2.join(", ")}`);
9093
- throw new Error("Invalid currency");
8978
+ // ../../node_modules/viem/_esm/utils/typedData.js
8979
+ init_isAddress();
8980
+ init_size();
8981
+ init_toHex();
8982
+ init_regex();
8983
+ function validateTypedData(parameters) {
8984
+ const { domain, message, primaryType, types } = parameters;
8985
+ const validateData = (struct, data) => {
8986
+ for (const param of struct) {
8987
+ const { name, type } = param;
8988
+ const value = data[name];
8989
+ const integerMatch = type.match(integerRegex);
8990
+ if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
8991
+ const [_type, base, size_] = integerMatch;
8992
+ numberToHex(value, {
8993
+ signed: base === "int",
8994
+ size: Number.parseInt(size_, 10) / 8
8995
+ });
8996
+ }
8997
+ if (type === "address" && typeof value === "string" && !isAddress(value))
8998
+ throw new InvalidAddressError2({ address: value });
8999
+ const bytesMatch = type.match(bytesRegex);
9000
+ if (bytesMatch) {
9001
+ const [_type, size_] = bytesMatch;
9002
+ if (size_ && size(value) !== Number.parseInt(size_, 10))
9003
+ throw new BytesSizeMismatchError({
9004
+ expectedSize: Number.parseInt(size_, 10),
9005
+ givenSize: size(value)
9006
+ });
9007
+ }
9008
+ const struct2 = types[type];
9009
+ if (struct2) {
9010
+ validateReference(type);
9011
+ validateData(struct2, value);
9012
+ }
9013
+ }
9014
+ };
9015
+ if (types.EIP712Domain && domain) {
9016
+ if (typeof domain !== "object")
9017
+ throw new InvalidDomainError({ domain });
9018
+ validateData(types.EIP712Domain, domain);
9094
9019
  }
9095
- const spinner = createSpinner("Updating currency preference...");
9096
- await vault.setCurrency(currency);
9097
- spinner.succeed("Currency updated");
9098
- const currencyName = fiatCurrencyNameRecord3[currency];
9099
- if (isJsonOutput()) {
9100
- outputJson({ currency, name: currencyName, updated: true });
9101
- return currency;
9020
+ if (primaryType !== "EIP712Domain") {
9021
+ if (types[primaryType])
9022
+ validateData(types[primaryType], message);
9023
+ else
9024
+ throw new InvalidPrimaryTypeError({ primaryType, types });
9102
9025
  }
9103
- success(`
9104
- + Currency preference set to ${currency.toUpperCase()} (${currencyName})`);
9105
- return currency;
9106
9026
  }
9107
- async function executeServer(ctx2) {
9108
- const spinner = createSpinner("Checking server status...");
9109
- try {
9110
- const status = await ctx2.sdk.getServerStatus();
9111
- spinner.succeed("Server status retrieved");
9112
- if (isJsonOutput()) {
9113
- outputJson({ server: status });
9114
- return status;
9115
- }
9116
- printResult(chalk6.cyan("\nServer Status:\n"));
9117
- printResult(chalk6.bold("Fast Vault Server:"));
9118
- printResult(` Online: ${status.fastVault.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9119
- if (status.fastVault.latency) {
9120
- printResult(` Latency: ${status.fastVault.latency}ms`);
9121
- }
9122
- printResult(chalk6.bold("\nMessage Relay:"));
9123
- printResult(` Online: ${status.messageRelay.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9124
- if (status.messageRelay.latency) {
9125
- printResult(` Latency: ${status.messageRelay.latency}ms`);
9126
- }
9127
- return status;
9128
- } catch (err) {
9129
- spinner.fail("Failed to check server status");
9130
- error(`
9131
- x ${err.message}`);
9132
- throw err;
9133
- }
9027
+ function getTypesForEIP712Domain({ domain }) {
9028
+ return [
9029
+ typeof domain?.name === "string" && { name: "name", type: "string" },
9030
+ domain?.version && { name: "version", type: "string" },
9031
+ (typeof domain?.chainId === "number" || typeof domain?.chainId === "bigint") && {
9032
+ name: "chainId",
9033
+ type: "uint256"
9034
+ },
9035
+ domain?.verifyingContract && {
9036
+ name: "verifyingContract",
9037
+ type: "address"
9038
+ },
9039
+ domain?.salt && { name: "salt", type: "bytes32" }
9040
+ ].filter(Boolean);
9134
9041
  }
9135
- async function executeAddressBook(ctx2, options = {}) {
9136
- if (options.add) {
9137
- let chain = options.chain;
9138
- let address = options.address;
9139
- let name = options.name;
9140
- const prompts = [];
9141
- if (!chain) {
9142
- prompts.push({
9143
- type: "select",
9144
- name: "chain",
9145
- message: "Select chain:",
9146
- choices: Object.values(Chain9)
9147
- });
9148
- }
9149
- if (!address) {
9150
- prompts.push({
9151
- type: "input",
9152
- name: "address",
9153
- message: "Enter address:",
9154
- validate: (input) => input.trim() !== "" || "Address is required"
9155
- });
9156
- }
9157
- if (!name) {
9158
- prompts.push({
9159
- type: "input",
9160
- name: "name",
9161
- message: "Enter name/label:",
9162
- validate: (input) => input.trim() !== "" || "Name is required"
9163
- });
9164
- }
9165
- if (prompts.length > 0) {
9166
- const answers = await prompt(prompts);
9167
- chain = chain || answers.chain;
9168
- address = address || answers.address?.trim();
9169
- name = name || answers.name?.trim();
9170
- }
9171
- const spinner2 = createSpinner("Adding address to address book...");
9172
- const entry = {
9173
- chain,
9174
- address,
9175
- name,
9176
- source: "saved",
9177
- dateAdded: Date.now()
9178
- };
9179
- await ctx2.sdk.addAddressBookEntry([entry]);
9180
- spinner2.succeed("Address added");
9181
- if (isJsonOutput()) {
9182
- outputJson({ added: entry });
9183
- return [];
9184
- }
9185
- success(`
9186
- + Added ${name} (${chain}: ${address})`);
9187
- return [];
9188
- }
9189
- if (options.remove) {
9190
- const spinner2 = createSpinner("Removing address from address book...");
9191
- await ctx2.sdk.removeAddressBookEntry([{ address: options.remove, chain: options.chain }]);
9192
- spinner2.succeed("Address removed");
9193
- if (isJsonOutput()) {
9194
- outputJson({ removed: { address: options.remove, chain: options.chain } });
9195
- return [];
9196
- }
9197
- success(`
9198
- + Removed ${options.remove}`);
9199
- return [];
9200
- }
9201
- const spinner = createSpinner("Loading address book...");
9202
- const addressBook = await ctx2.sdk.getAddressBook(options.chain);
9203
- spinner.succeed("Address book loaded");
9204
- const allEntries = [...addressBook.saved, ...addressBook.vaults];
9205
- if (isJsonOutput()) {
9206
- outputJson({ addressBook: allEntries, chain: options.chain });
9207
- return allEntries;
9208
- }
9209
- if (allEntries.length === 0) {
9210
- warn(`
9211
- No addresses in address book${options.chain ? ` for ${options.chain}` : ""}`);
9212
- info(chalk6.gray("\nUse --add to add an address to the address book"));
9213
- } else {
9214
- printResult(chalk6.cyan(`
9215
- Address Book${options.chain ? ` (${options.chain})` : ""}:
9216
- `));
9217
- const table = allEntries.map((entry) => ({
9218
- Name: entry.name,
9219
- Chain: entry.chain,
9220
- Address: entry.address,
9221
- Source: entry.source
9222
- }));
9223
- printTable(table);
9224
- info(chalk6.gray("\nUse --add to add or --remove <address> to remove an address"));
9225
- }
9226
- return allEntries;
9042
+ function validateReference(type) {
9043
+ if (type === "address" || type === "bool" || type === "string" || type.startsWith("bytes") || type.startsWith("uint") || type.startsWith("int"))
9044
+ throw new InvalidStructTypeError({ type });
9227
9045
  }
9228
9046
 
9229
- // src/commands/rujira.ts
9230
- import {
9231
- getRoutesSummary,
9232
- listEasyRoutes,
9233
- RujiraClient,
9234
- VultisigRujiraProvider
9235
- } from "@vultisig/rujira";
9236
- async function createRujiraClient(ctx2, options = {}) {
9237
- const vault = await ctx2.ensureActiveVault();
9238
- const provider = new VultisigRujiraProvider(vault);
9239
- const client = new RujiraClient({
9240
- signer: provider,
9241
- rpcEndpoint: options.rpcEndpoint,
9242
- config: {
9243
- // Allow overriding rest endpoint via config (used for thornode calls)
9244
- ...options.restEndpoint ? { restEndpoint: options.restEndpoint } : {}
9245
- }
9047
+ // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
9048
+ function hashTypedData(parameters) {
9049
+ const { domain = {}, message, primaryType } = parameters;
9050
+ const types = {
9051
+ EIP712Domain: getTypesForEIP712Domain({ domain }),
9052
+ ...parameters.types
9053
+ };
9054
+ validateTypedData({
9055
+ domain,
9056
+ message,
9057
+ primaryType,
9058
+ types
9246
9059
  });
9247
- const spinner = createSpinner("Connecting to Rujira/THORChain...");
9248
- await client.connect();
9249
- spinner.succeed("Connected");
9250
- return client;
9060
+ const parts = ["0x1901"];
9061
+ if (domain)
9062
+ parts.push(hashDomain({
9063
+ domain,
9064
+ types
9065
+ }));
9066
+ if (primaryType !== "EIP712Domain")
9067
+ parts.push(hashStruct({
9068
+ data: message,
9069
+ primaryType,
9070
+ types
9071
+ }));
9072
+ return keccak256(concat(parts));
9251
9073
  }
9252
- async function executeRujiraBalance(ctx2, options = {}) {
9253
- const vault = await ctx2.ensureActiveVault();
9254
- const thorAddress = await vault.address("THORChain");
9255
- const client = await createRujiraClient(ctx2, options);
9256
- const spinner = createSpinner("Loading THORChain balances...");
9257
- const balances = await client.deposit.getBalances(thorAddress);
9258
- spinner.succeed("Balances loaded");
9259
- const filtered = options.securedOnly ? balances.filter((b) => b.denom.includes("-") || b.denom.includes("/")) : balances;
9260
- if (isJsonOutput()) {
9261
- outputJson({ thorAddress, balances: filtered });
9262
- return;
9263
- }
9264
- info(`THORChain address: ${thorAddress}`);
9265
- if (!filtered.length) {
9266
- printResult("No balances found");
9267
- return;
9074
+ function hashDomain({ domain, types }) {
9075
+ return hashStruct({
9076
+ data: domain,
9077
+ primaryType: "EIP712Domain",
9078
+ types
9079
+ });
9080
+ }
9081
+ function hashStruct({ data, primaryType, types }) {
9082
+ const encoded = encodeData({
9083
+ data,
9084
+ primaryType,
9085
+ types
9086
+ });
9087
+ return keccak256(encoded);
9088
+ }
9089
+ function encodeData({ data, primaryType, types }) {
9090
+ const encodedTypes = [{ type: "bytes32" }];
9091
+ const encodedValues = [hashType({ primaryType, types })];
9092
+ for (const field of types[primaryType]) {
9093
+ const [type, value] = encodeField({
9094
+ types,
9095
+ name: field.name,
9096
+ type: field.type,
9097
+ value: data[field.name]
9098
+ });
9099
+ encodedTypes.push(type);
9100
+ encodedValues.push(value);
9268
9101
  }
9269
- printTable(
9270
- filtered.map((b) => ({
9271
- asset: b.asset,
9272
- denom: b.denom,
9273
- amount: b.formatted,
9274
- raw: b.amount
9275
- }))
9276
- );
9102
+ return encodeAbiParameters(encodedTypes, encodedValues);
9277
9103
  }
9278
- async function executeRujiraRoutes() {
9279
- const routes = listEasyRoutes();
9280
- const summary = getRoutesSummary();
9281
- if (isJsonOutput()) {
9282
- outputJson({ routes, summary });
9283
- return;
9104
+ function hashType({ primaryType, types }) {
9105
+ const encodedHashType = toHex(encodeType({ primaryType, types }));
9106
+ return keccak256(encodedHashType);
9107
+ }
9108
+ function encodeType({ primaryType, types }) {
9109
+ let result = "";
9110
+ const unsortedDeps = findTypeDependencies({ primaryType, types });
9111
+ unsortedDeps.delete(primaryType);
9112
+ const deps = [primaryType, ...Array.from(unsortedDeps).sort()];
9113
+ for (const type of deps) {
9114
+ result += `${type}(${types[type].map(({ name, type: t }) => `${t} ${name}`).join(",")})`;
9284
9115
  }
9285
- printResult(summary);
9286
- printResult("");
9287
- printTable(
9288
- routes.map((r) => ({
9289
- name: r.name,
9290
- from: r.from,
9291
- to: r.to,
9292
- liquidity: r.liquidity,
9293
- description: r.description
9294
- }))
9295
- );
9116
+ return result;
9296
9117
  }
9297
- async function executeRujiraDeposit(ctx2, options = {}) {
9298
- const vault = await ctx2.ensureActiveVault();
9299
- const thorAddress = await vault.address("THORChain");
9300
- const client = await createRujiraClient(ctx2, options);
9301
- if (!options.asset) {
9302
- const spinner2 = createSpinner("Loading THORChain inbound addresses...");
9303
- const inbound = await client.deposit.getInboundAddresses();
9304
- spinner2.succeed("Inbound addresses loaded");
9305
- if (isJsonOutput()) {
9306
- outputJson({ thorAddress, inboundAddresses: inbound });
9307
- return;
9308
- }
9309
- info(`THORChain address: ${thorAddress}`);
9310
- printResult("Provide an L1 asset to get a chain-specific inbound address + memo.");
9311
- printResult("Example: vultisig rujira deposit --asset BTC.BTC --amount 100000");
9312
- printResult("");
9313
- printTable(
9314
- inbound.map((a) => ({
9315
- chain: a.chain,
9316
- address: a.address,
9317
- halted: a.halted,
9318
- globalTradingPaused: a.global_trading_paused,
9319
- chainTradingPaused: a.chain_trading_paused
9320
- }))
9321
- );
9322
- return;
9118
+ function findTypeDependencies({ primaryType: primaryType_, types }, results = /* @__PURE__ */ new Set()) {
9119
+ const match = primaryType_.match(/^\w*/u);
9120
+ const primaryType = match?.[0];
9121
+ if (results.has(primaryType) || types[primaryType] === void 0) {
9122
+ return results;
9323
9123
  }
9324
- const amount = options.amount ?? "1";
9325
- const spinner = createSpinner("Preparing deposit instructions...");
9326
- const prepared = await client.deposit.prepare({
9327
- fromAsset: options.asset,
9328
- amount,
9329
- thorAddress,
9330
- affiliate: options.affiliate,
9331
- affiliateBps: options.affiliateBps
9332
- });
9333
- spinner.succeed("Deposit prepared");
9334
- if (isJsonOutput()) {
9335
- outputJson({ thorAddress, deposit: prepared });
9336
- return;
9124
+ results.add(primaryType);
9125
+ for (const field of types[primaryType]) {
9126
+ findTypeDependencies({ primaryType: field.type, types }, results);
9337
9127
  }
9338
- info(`THORChain address: ${thorAddress}`);
9339
- printResult("Deposit instructions (send from L1):");
9340
- printResult(` Chain: ${prepared.chain}`);
9341
- printResult(` Asset: ${prepared.asset}`);
9342
- printResult(` Inbound address:${prepared.inboundAddress}`);
9343
- printResult(` Memo: ${prepared.memo}`);
9344
- printResult(` Min amount: ${prepared.minimumAmount}`);
9345
- if (prepared.warning) {
9346
- warn(prepared.warning);
9128
+ return results;
9129
+ }
9130
+ function encodeField({ types, name, type, value }) {
9131
+ if (types[type] !== void 0) {
9132
+ return [
9133
+ { type: "bytes32" },
9134
+ keccak256(encodeData({ data: value, primaryType: type, types }))
9135
+ ];
9136
+ }
9137
+ if (type === "bytes")
9138
+ return [{ type: "bytes32" }, keccak256(value)];
9139
+ if (type === "string")
9140
+ return [{ type: "bytes32" }, keccak256(toHex(value))];
9141
+ if (type.lastIndexOf("]") === type.length - 1) {
9142
+ const parsedType = type.slice(0, type.lastIndexOf("["));
9143
+ const typeValuePairs = value.map((item) => encodeField({
9144
+ name,
9145
+ type: parsedType,
9146
+ types,
9147
+ value: item
9148
+ }));
9149
+ return [
9150
+ { type: "bytes32" },
9151
+ keccak256(encodeAbiParameters(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v)))
9152
+ ];
9347
9153
  }
9154
+ return [{ type }, value];
9348
9155
  }
9349
- async function executeRujiraSwap(ctx2, options) {
9350
- const vault = await ctx2.ensureActiveVault();
9351
- if (!options.dryRun && !options.yes && isNonInteractive()) {
9352
- throw new ConfirmationRequiredError(
9353
- "Swap requires confirmation.",
9354
- "Pass --yes to confirm, or --dry-run to preview."
9355
- );
9156
+
9157
+ // ../../node_modules/viem/_esm/errors/unit.js
9158
+ init_base();
9159
+ var InvalidDecimalNumberError = class extends BaseError {
9160
+ constructor({ value }) {
9161
+ super(`Number \`${value}\` is not a valid decimal number.`, {
9162
+ name: "InvalidDecimalNumberError"
9163
+ });
9356
9164
  }
9357
- const client = await createRujiraClient(ctx2, options);
9358
- const destination = options.destination ?? await vault.address("THORChain");
9359
- const quoteSpinner = createSpinner("Getting FIN swap quote...");
9360
- const quote = await client.swap.getQuote({
9361
- fromAsset: options.fromAsset,
9362
- toAsset: options.toAsset,
9363
- amount: options.amount,
9364
- destination,
9365
- slippageBps: options.slippageBps
9366
- });
9367
- quoteSpinner.succeed("Quote received");
9368
- if (options.dryRun) {
9369
- if (isJsonOutput()) {
9370
- outputJson({ dryRun: true, quote });
9371
- } else {
9372
- printResult("FIN Swap Preview (dry-run)");
9373
- printResult(` From: ${options.fromAsset}`);
9374
- printResult(` To: ${options.toAsset}`);
9375
- printResult(` Amount (in): ${options.amount}`);
9376
- printResult(` Expected out:${quote.expectedOutput}`);
9377
- printResult(` Min out: ${quote.minimumOutput}`);
9378
- printResult(` Contract: ${quote.contractAddress}`);
9165
+ };
9166
+
9167
+ // ../../node_modules/viem/_esm/utils/unit/parseUnits.js
9168
+ function parseUnits(value, decimals) {
9169
+ if (!/^(-?)([0-9]*)\.?([0-9]*)$/.test(value))
9170
+ throw new InvalidDecimalNumberError({ value });
9171
+ let [integer, fraction = "0"] = value.split(".");
9172
+ const negative = integer.startsWith("-");
9173
+ if (negative)
9174
+ integer = integer.slice(1);
9175
+ fraction = fraction.replace(/(0+)$/, "");
9176
+ if (decimals === 0) {
9177
+ if (Math.round(Number(`.${fraction}`)) === 1)
9178
+ integer = `${BigInt(integer) + 1n}`;
9179
+ fraction = "";
9180
+ } else if (fraction.length > decimals) {
9181
+ const [left, unit, right] = [
9182
+ fraction.slice(0, decimals - 1),
9183
+ fraction.slice(decimals - 1, decimals),
9184
+ fraction.slice(decimals)
9185
+ ];
9186
+ const rounded = Math.round(Number(`${unit}.${right}`));
9187
+ if (rounded > 9)
9188
+ fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, "0");
9189
+ else
9190
+ fraction = `${left}${rounded}`;
9191
+ if (fraction.length > decimals) {
9192
+ fraction = fraction.slice(1);
9193
+ integer = `${BigInt(integer) + 1n}`;
9379
9194
  }
9380
- return;
9195
+ fraction = fraction.slice(0, decimals);
9196
+ } else {
9197
+ fraction = fraction.padEnd(decimals, "0");
9381
9198
  }
9382
- if (!isJsonOutput()) {
9383
- printResult("FIN Swap Preview");
9384
- printResult(` From: ${options.fromAsset}`);
9385
- printResult(` To: ${options.toAsset}`);
9386
- printResult(` Amount (in): ${options.amount}`);
9387
- printResult(` Expected out:${quote.expectedOutput}`);
9388
- printResult(` Min out: ${quote.minimumOutput}`);
9389
- printResult(` Contract: ${quote.contractAddress}`);
9390
- if (quote.warning) {
9391
- warn(quote.warning);
9392
- }
9199
+ return BigInt(`${negative ? "-" : ""}${integer}${fraction}`);
9200
+ }
9201
+
9202
+ // ../../node_modules/viem/_esm/index.js
9203
+ init_formatUnits();
9204
+
9205
+ // ../../packages/core/chain/dist/amount/toChainAmount.js
9206
+ var ChainAmountParseError = class extends Error {
9207
+ name = "ChainAmountParseError";
9208
+ constructor(message) {
9209
+ super(message);
9393
9210
  }
9394
- if (!options.yes) {
9395
- warn("This command will execute a swap. Re-run with -y/--yes to skip this warning.");
9396
- throw new ConfirmationRequiredError(
9397
- "Swap requires confirmation.",
9398
- "Pass --yes to confirm, or --dry-run to preview."
9399
- );
9211
+ };
9212
+ var SCIENTIFIC_DECIMAL = /^([+-]?)(?:(\d+)\.?(\d*)|\.(\d+))[eE]([+-]?\d+)$/i;
9213
+ var MAX_SCALE_ABS = 10000n;
9214
+ var padFractionDigits = (frac, totalLen) => {
9215
+ const need = totalLen - BigInt(frac.length);
9216
+ if (need <= 0n) {
9217
+ return frac;
9400
9218
  }
9401
- await ensureVaultUnlocked(vault, options.password);
9402
- const execSpinner = createSpinner("Executing FIN swap...");
9403
- const result = await client.swap.execute(quote, { slippageBps: options.slippageBps });
9404
- execSpinner.succeed("Swap submitted");
9405
- if (isJsonOutput()) {
9406
- outputJson({ quote, result });
9219
+ if (need <= BigInt(Number.MAX_SAFE_INTEGER)) {
9220
+ return `${"0".repeat(Number(need))}${frac}`;
9221
+ }
9222
+ let out = frac;
9223
+ while (BigInt(out.length) < totalLen) {
9224
+ out = `0${out}`;
9225
+ }
9226
+ return out;
9227
+ };
9228
+ var expandScientificNotationToDecimalString = (s) => {
9229
+ const m = SCIENTIFIC_DECIMAL.exec(s.trim());
9230
+ if (!m) {
9231
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9232
+ }
9233
+ const signNeg = m[1] === "-";
9234
+ let digitStr;
9235
+ let fracLen;
9236
+ if (m[4] !== void 0) {
9237
+ digitStr = m[4];
9238
+ fracLen = digitStr.length;
9407
9239
  } else {
9408
- printResult(`Tx Hash: ${result.txHash}`);
9240
+ digitStr = `${m[2] ?? ""}${m[3] ?? ""}`;
9241
+ fracLen = (m[3] ?? "").length;
9242
+ }
9243
+ if (!/^\d+$/.test(digitStr)) {
9244
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9245
+ }
9246
+ const expStr = m[5] ?? "";
9247
+ if (expStr === "" || expStr === "+" || expStr === "-") {
9248
+ throw new ChainAmountParseError(`Invalid amount: "${s}"`);
9249
+ }
9250
+ const allDigits = BigInt(digitStr);
9251
+ const exp = BigInt(expStr);
9252
+ const scale = exp - BigInt(fracLen);
9253
+ const scaleAbs = scale < 0n ? -scale : scale;
9254
+ if (scaleAbs > MAX_SCALE_ABS) {
9255
+ throw new ChainAmountParseError(`Amount exponent out of supported range: "${s}"`);
9256
+ }
9257
+ let absResult;
9258
+ if (scale >= 0n) {
9259
+ const mult = 10n ** scale;
9260
+ absResult = (allDigits * mult).toString();
9261
+ } else {
9262
+ const k = -scale;
9263
+ const divisor = 10n ** k;
9264
+ const intPart = allDigits / divisor;
9265
+ const rem = allDigits % divisor;
9266
+ const frac = padFractionDigits(rem.toString(), k);
9267
+ absResult = intPart === 0n ? `0.${frac}` : `${intPart.toString()}.${frac}`;
9409
9268
  }
9410
- }
9411
- async function executeRujiraWithdraw(ctx2, options) {
9412
- const vault = await ctx2.ensureActiveVault();
9413
- if (!options.dryRun && !options.yes && isNonInteractive()) {
9414
- throw new ConfirmationRequiredError(
9415
- "Withdrawal requires confirmation.",
9416
- "Pass --yes to confirm, or --dry-run to preview."
9417
- );
9269
+ if (signNeg && allDigits !== 0n) {
9270
+ return `-${absResult}`;
9418
9271
  }
9419
- const client = await createRujiraClient(ctx2, options);
9420
- const prepSpinner = createSpinner("Preparing withdrawal (MsgDeposit)...");
9421
- const prepared = await client.withdraw.prepare({
9422
- asset: options.asset,
9423
- amount: options.amount,
9424
- l1Address: options.l1Address,
9425
- maxFeeBps: options.maxFeeBps
9426
- });
9427
- prepSpinner.succeed("Withdrawal prepared");
9428
- if (options.dryRun) {
9429
- if (isJsonOutput()) {
9430
- outputJson({ dryRun: true, prepared });
9431
- } else {
9432
- printResult("Withdraw Preview (dry-run)");
9433
- printResult(` Asset: ${prepared.asset}`);
9434
- printResult(` Amount: ${prepared.amount}`);
9435
- printResult(` Destination: ${prepared.destination}`);
9436
- printResult(` Memo: ${prepared.memo}`);
9437
- printResult(` Est. fee: ${prepared.estimatedFee}`);
9272
+ return absResult;
9273
+ };
9274
+ var formatNumberAmount = (amount) => {
9275
+ const str2 = amount.toString();
9276
+ return /[eE]/.test(str2) ? expandScientificNotationToDecimalString(str2) : str2;
9277
+ };
9278
+ var truncateToDecimals = (s, decimals) => {
9279
+ const dotIdx = s.indexOf(".");
9280
+ if (dotIdx === -1)
9281
+ return s;
9282
+ if (decimals === 0)
9283
+ return s.slice(0, dotIdx);
9284
+ const fracPart = s.slice(dotIdx + 1);
9285
+ if (fracPart.length <= decimals)
9286
+ return s;
9287
+ return `${s.slice(0, dotIdx + 1)}${fracPart.slice(0, decimals)}`;
9288
+ };
9289
+ var toChainAmount = (amount, decimals) => {
9290
+ if (typeof amount === "string") {
9291
+ const trimmed = amount.trim();
9292
+ if (!trimmed) {
9293
+ throw new ChainAmountParseError("Amount cannot be empty");
9438
9294
  }
9439
- return;
9440
- }
9441
- if (!isJsonOutput()) {
9442
- printResult("Withdraw Preview");
9443
- printResult(` Asset: ${prepared.asset}`);
9444
- printResult(` Amount: ${prepared.amount}`);
9445
- printResult(` Destination: ${prepared.destination}`);
9446
- printResult(` Memo: ${prepared.memo}`);
9447
- printResult(` Est. fee: ${prepared.estimatedFee}`);
9295
+ if (/[eE]/.test(trimmed)) {
9296
+ const expanded = expandScientificNotationToDecimalString(trimmed);
9297
+ return parseUnits(truncateToDecimals(expanded, decimals), decimals);
9298
+ }
9299
+ return parseUnits(truncateToDecimals(trimmed, decimals), decimals);
9448
9300
  }
9449
- if (!options.yes) {
9450
- warn("This command will broadcast a THORChain MsgDeposit withdrawal. Re-run with -y/--yes to proceed.");
9451
- throw new ConfirmationRequiredError(
9452
- "Withdrawal requires confirmation.",
9453
- "Pass --yes to confirm, or --dry-run to preview."
9454
- );
9301
+ return parseUnits(truncateToDecimals(formatNumberAmount(amount), decimals), decimals);
9302
+ };
9303
+
9304
+ // src/commands/swap.ts
9305
+ async function executeSwapChains(ctx2) {
9306
+ const vault = await ctx2.ensureActiveVault();
9307
+ const spinner = createSpinner("Loading supported swap chains...");
9308
+ const chains = await vault.getSupportedSwapChains();
9309
+ spinner.succeed("Swap chains loaded");
9310
+ if (isJsonOutput()) {
9311
+ outputJson({ swapChains: [...chains] });
9312
+ return chains;
9455
9313
  }
9456
- await ensureVaultUnlocked(vault, options.password);
9457
- const execSpinner = createSpinner("Broadcasting withdrawal...");
9458
- const result = await client.withdraw.execute(prepared);
9459
- execSpinner.succeed("Withdrawal submitted");
9314
+ displaySwapChains(chains);
9315
+ return chains;
9316
+ }
9317
+ async function executeSwapQuote(ctx2, options) {
9318
+ const isMax = options.amount === "max";
9319
+ const amount = normalizeSwapAmount(options.amount);
9320
+ const vault = await ctx2.ensureActiveVault();
9321
+ const spinner = createSpinner("Getting swap quote...");
9322
+ const result = await vault.swap({
9323
+ fromChain: options.fromChain,
9324
+ fromSymbol: options.fromToken || "",
9325
+ toChain: options.toChain,
9326
+ toSymbol: options.toToken || "",
9327
+ amount,
9328
+ dryRun: true
9329
+ });
9330
+ if (!result.dryRun) throw new Error("unreachable");
9331
+ spinner.succeed("Quote received");
9332
+ const quote = result.quote;
9333
+ const semanticAmount = isMax ? amount : normalizeSwapAmount(amount, quote.fromCoin.decimals);
9334
+ const fromAmountDisplay = isMax ? `${formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals)} (max)` : semanticAmount;
9460
9335
  if (isJsonOutput()) {
9461
- outputJson({ prepared, result });
9462
- } else {
9463
- printResult(`Tx Hash: ${result.txHash}`);
9336
+ outputJson({
9337
+ fromChain: options.fromChain,
9338
+ toChain: options.toChain,
9339
+ amount: semanticAmount,
9340
+ isMax,
9341
+ quote
9342
+ });
9343
+ return quote;
9464
9344
  }
9345
+ const feeBalance = await vault.balance(options.fromChain);
9346
+ const discountTier = await vault.getDiscountTier();
9347
+ displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9348
+ fromDecimals: quote.fromCoin.decimals,
9349
+ toDecimals: quote.toCoin.decimals,
9350
+ feeDecimals: feeBalance.decimals,
9351
+ feeSymbol: feeBalance.symbol,
9352
+ discountTier
9353
+ });
9354
+ info('\nTo execute this swap, use the "swap" command');
9355
+ return quote;
9465
9356
  }
9466
-
9467
- // src/commands/discount.ts
9468
- import {
9469
- baseAffiliateBps,
9470
- vultDiscountTierBps,
9471
- vultDiscountTierMinBalances
9472
- } from "@vultisig/sdk";
9473
- import chalk7 from "chalk";
9474
- var TIER_CONFIG = {
9475
- none: { bps: baseAffiliateBps, discount: 0 },
9476
- ...Object.fromEntries(
9477
- Object.entries(vultDiscountTierMinBalances).map(([tier, minVult]) => [
9478
- tier,
9479
- {
9480
- bps: baseAffiliateBps - vultDiscountTierBps[tier],
9481
- discount: vultDiscountTierBps[tier],
9482
- minVult
9483
- }
9484
- ])
9485
- )
9486
- };
9487
- function getTierColor(tier) {
9488
- const colors = {
9489
- none: chalk7.gray,
9490
- bronze: chalk7.hex("#CD7F32"),
9491
- silver: chalk7.hex("#C0C0C0"),
9492
- gold: chalk7.hex("#FFD700"),
9493
- platinum: chalk7.hex("#E5E4E2"),
9494
- diamond: chalk7.hex("#B9F2FF"),
9495
- ultimate: chalk7.hex("#FF00FF")
9357
+ var SWAP_AMOUNT_CANONICAL_DECIMALS = 1e4;
9358
+ function normalizeSwapAmount(amount, decimals = SWAP_AMOUNT_CANONICAL_DECIMALS) {
9359
+ if (amount === "max") return amount;
9360
+ try {
9361
+ const chainAmount = toChainAmount(amount, decimals);
9362
+ if (chainAmount <= 0n) throw new Error("Invalid amount");
9363
+ return formatUnits(chainAmount, decimals);
9364
+ } catch {
9365
+ throw new Error("Invalid amount");
9366
+ }
9367
+ }
9368
+ function toSwapRequest(options, amount, dryRun) {
9369
+ return {
9370
+ fromChain: options.fromChain,
9371
+ fromSymbol: options.fromToken || "",
9372
+ toChain: options.toChain,
9373
+ toSymbol: options.toToken || "",
9374
+ amount,
9375
+ ...options.slippage !== void 0 && { slippageTolerance: options.slippage },
9376
+ ...dryRun && { dryRun: true }
9496
9377
  };
9497
- return colors[tier] || chalk7.white;
9498
9378
  }
9499
- function getNextTier(currentTier) {
9500
- const tierOrder = ["none", "bronze", "silver", "gold", "platinum", "diamond", "ultimate"];
9501
- const currentIndex = tierOrder.indexOf(currentTier);
9502
- if (currentIndex === -1 || currentIndex >= tierOrder.length - 1) {
9503
- return null;
9379
+ function toDryRunResult(options, quote, fromAmountRaw) {
9380
+ const result = {
9381
+ dryRun: true,
9382
+ fromChain: String(options.fromChain),
9383
+ fromToken: quote.fromCoin.ticker,
9384
+ toChain: String(options.toChain),
9385
+ toToken: quote.toCoin.ticker,
9386
+ inputAmount: fromAmountRaw,
9387
+ ...options.amount === "max" && { isMax: true },
9388
+ estimatedOutput: formatBigintAmount(quote.estimatedOutput, quote.toCoin.decimals),
9389
+ provider: quote.provider
9390
+ };
9391
+ if (quote.estimatedOutputFiat != null) result.estimatedOutputFiat = parseFloat(quote.estimatedOutputFiat.toFixed(2));
9392
+ if (quote.requiresApproval) result.requiresApproval = true;
9393
+ if (quote.warnings?.length) result.warnings = [...quote.warnings];
9394
+ return result;
9395
+ }
9396
+ function displayDryRunResult(result) {
9397
+ info(`
9398
+ Dry-run preview:`);
9399
+ info(` From: ${result.inputAmount} ${result.fromToken} (${result.fromChain})`);
9400
+ info(` To: ${result.estimatedOutput} ${result.toToken} (${result.toChain})`);
9401
+ info(` Provider: ${result.provider}`);
9402
+ if (result.estimatedOutputFiat != null) info(` Est. value (USD): $${result.estimatedOutputFiat}`);
9403
+ if (result.requiresApproval) info(` Requires approval: yes`);
9404
+ if (result.warnings?.length) result.warnings.forEach((w) => warn(` Warning: ${w}`));
9405
+ }
9406
+ function refuseSwapWhenNonInteractive() {
9407
+ throw new ConfirmationRequiredError(
9408
+ "Swap requires confirmation.",
9409
+ "Pass --yes to confirm, or --dry-run to preview without signing."
9410
+ );
9411
+ }
9412
+ async function confirmSwapIfNeeded(options) {
9413
+ if (options.yes) return;
9414
+ if (isNonInteractive()) {
9415
+ refuseSwapWhenNonInteractive();
9504
9416
  }
9505
- const nextTierName = tierOrder[currentIndex + 1];
9506
- const config = TIER_CONFIG[nextTierName];
9507
- if ("minVult" in config) {
9508
- return { name: nextTierName, vultRequired: config.minVult };
9417
+ const confirmed = await confirmSwap();
9418
+ if (!confirmed) {
9419
+ throw new ConfirmationRequiredError("Swap declined at the confirmation prompt");
9509
9420
  }
9510
- return null;
9511
9421
  }
9512
- async function executeDiscount(ctx2, options = {}) {
9422
+ async function executeSwap(ctx2, options) {
9423
+ const amountStr = normalizeSwapAmount(options.amount);
9513
9424
  const vault = await ctx2.ensureActiveVault();
9514
- const spinner = createSpinner(options.refresh ? "Refreshing discount tier..." : "Loading discount tier...");
9515
- const tierResult = options.refresh ? await vault.updateDiscountTier() : await vault.getDiscountTier();
9516
- const tier = tierResult || "none";
9517
- const config = TIER_CONFIG[tier];
9518
- const nextTier = getNextTier(tier);
9519
- const tierInfo = {
9520
- tier,
9521
- feeBps: config.bps,
9522
- discountBps: config.discount,
9523
- nextTier
9524
- };
9525
- spinner.succeed("Discount tier loaded");
9526
- if (isJsonOutput()) {
9527
- outputJson({
9528
- tier: tierInfo.tier,
9529
- feeBps: tierInfo.feeBps,
9530
- discountBps: tierInfo.discountBps,
9531
- nextTier: tierInfo.nextTier
9532
- });
9533
- return tierInfo;
9425
+ if (!options.dryRun && !options.yes && isNonInteractive()) {
9426
+ refuseSwapWhenNonInteractive();
9534
9427
  }
9535
- displayDiscountTier(tierInfo);
9536
- return tierInfo;
9537
- }
9538
- function displayDiscountTier(tierInfo) {
9539
- const tierColor = getTierColor(tierInfo.tier);
9540
- printResult(chalk7.cyan("\n+----------------------------------------+"));
9541
- printResult(chalk7.cyan("| VULT Discount Tier |"));
9542
- printResult(chalk7.cyan("+----------------------------------------+\n"));
9543
- const tierDisplay = tierInfo.tier === "none" ? chalk7.gray("No Tier") : tierColor(tierInfo.tier.charAt(0).toUpperCase() + tierInfo.tier.slice(1));
9544
- printResult(` Current Tier: ${tierDisplay}`);
9545
- if (tierInfo.tier === "none") {
9546
- printResult(` Swap Fee: ${chalk7.gray("50 bps (0.50%)")}`);
9547
- printResult(` Discount: ${chalk7.gray("None")}`);
9548
- } else {
9549
- printResult(` Swap Fee: ${chalk7.green(`${tierInfo.feeBps} bps (${(tierInfo.feeBps / 100).toFixed(2)}%)`)}`);
9550
- printResult(` Discount: ${chalk7.green(`${tierInfo.discountBps} bps saved`)}`);
9428
+ const quoteSpinner = createSpinner("Getting swap quote...");
9429
+ const dryResult = await vault.swap(toSwapRequest(options, amountStr, true));
9430
+ if (!dryResult.dryRun) throw new Error("unreachable");
9431
+ quoteSpinner.succeed("Quote received");
9432
+ const quote = dryResult.quote;
9433
+ const semanticAmount = options.amount === "max" ? amountStr : normalizeSwapAmount(amountStr, quote.fromCoin.decimals);
9434
+ const fromAmountRaw = options.amount === "max" ? formatBigintAmount(quote.maxSwapable, quote.fromCoin.decimals) : semanticAmount;
9435
+ const fromAmountDisplay = options.amount === "max" ? `${fromAmountRaw} (max)` : fromAmountRaw;
9436
+ if (options.dryRun) {
9437
+ const result = toDryRunResult(options, quote, fromAmountRaw);
9438
+ if (isJsonOutput()) {
9439
+ outputJson(result);
9440
+ } else {
9441
+ displayDryRunResult(result);
9442
+ }
9443
+ return result;
9551
9444
  }
9552
- if (tierInfo.nextTier) {
9553
- const nextTierColor = getTierColor(tierInfo.nextTier.name);
9554
- printResult(chalk7.bold("\n Next Tier:"));
9555
- printResult(
9556
- ` ${nextTierColor(tierInfo.nextTier.name.charAt(0).toUpperCase() + tierInfo.nextTier.name.slice(1))} - requires ${tierInfo.nextTier.vultRequired.toLocaleString()} VULT`
9557
- );
9558
- } else if (tierInfo.tier === "ultimate") {
9559
- printResult(chalk7.bold("\n ") + chalk7.magenta("You have the highest tier! 0% swap fees."));
9445
+ const feeBalance = await vault.balance(options.fromChain);
9446
+ const discountTier = await vault.getDiscountTier();
9447
+ if (!isJsonOutput()) {
9448
+ displaySwapPreview(quote, fromAmountDisplay, quote.fromCoin.ticker, quote.toCoin.ticker, {
9449
+ fromDecimals: quote.fromCoin.decimals,
9450
+ toDecimals: quote.toCoin.decimals,
9451
+ feeDecimals: feeBalance.decimals,
9452
+ feeSymbol: feeBalance.symbol,
9453
+ discountTier
9454
+ });
9455
+ }
9456
+ await confirmSwapIfNeeded(options);
9457
+ await ensureVaultUnlocked(vault, options.password);
9458
+ const intent = buildSwapBroadcastIntent(vault, {
9459
+ fromChain: options.fromChain,
9460
+ toChain: options.toChain,
9461
+ fromToken: options.fromToken,
9462
+ toToken: options.toToken,
9463
+ amount: fromAmountRaw,
9464
+ isMax: options.amount === "max"
9465
+ });
9466
+ let signSpinner;
9467
+ try {
9468
+ const broadcast = await guardedBroadcast(intent, options.force ?? false, async () => {
9469
+ signSpinner = createSpinner("Signing swap transaction...");
9470
+ vault.on("signingProgress", ({ step }) => {
9471
+ if (signSpinner) signSpinner.text = `${step.message} (${step.progress}%)`;
9472
+ });
9473
+ const result = await vault.swap(toSwapRequest(options, semanticAmount));
9474
+ if (result.dryRun) throw new Error("unreachable");
9475
+ return result;
9476
+ });
9477
+ signSpinner?.succeed(`Swap broadcast: ${broadcast.txHash}`);
9478
+ if (isJsonOutput()) {
9479
+ outputJson({
9480
+ txHash: broadcast.txHash,
9481
+ fromChain: options.fromChain,
9482
+ toChain: options.toChain,
9483
+ quote
9484
+ });
9485
+ } else {
9486
+ displaySwapResult(options.fromChain, options.toChain, broadcast.txHash, quote, quote.toCoin.decimals);
9487
+ }
9488
+ return { txHash: broadcast.txHash, quote };
9489
+ } catch (err) {
9490
+ signSpinner?.stop();
9491
+ throw err;
9492
+ } finally {
9493
+ vault.removeAllListeners("signingProgress");
9560
9494
  }
9561
- info(chalk7.gray("\n Tip: Thorguard NFT holders get +1 tier upgrade (up to gold)"));
9562
- printResult("");
9563
9495
  }
9564
9496
 
9565
- // src/commands/auth.ts
9566
- import { executeAuthLogout, executeAuthSetup, executeAuthStatus } from "@vultisig/client-shared";
9567
-
9568
- // src/commands/agent.ts
9569
- import chalk10 from "chalk";
9570
- import Table from "cli-table3";
9571
-
9572
- // src/agent/ask.ts
9573
- var AskInterface = class {
9574
- session;
9575
- verbose;
9576
- autoApprove;
9577
- responseParts = [];
9578
- toolCalls = [];
9579
- transactions = [];
9580
- cards = [];
9581
- warnings = [];
9582
- outcome;
9583
- error;
9584
- // Tracks whether the currently-latched `error` is a terminal one (e.g. the
9585
- // depth cap). A terminal error may overwrite a prior non-terminal one; once a
9586
- // terminal error is recorded, later frames cannot replace it. See onError.
9587
- errorIsTerminal = false;
9588
- constructor(session, verbose = false, autoApprove = false) {
9589
- this.session = session;
9590
- this.verbose = verbose;
9591
- this.autoApprove = autoApprove;
9592
- }
9593
- /**
9594
- * Whether the turn threw with a still-unacknowledged broadcast (the F1
9595
- * ack-failure case). The command's catch uses this to gate the ACK_FAILED
9596
- * re-tag so a later, unrelated retryable error after an already-acked
9597
- * broadcast keeps its own (retryable) classification instead of exit 8.
9598
- */
9599
- hasUnacknowledgedBroadcast() {
9600
- return this.session.hasUnacknowledgedBroadcast();
9601
- }
9602
- /**
9603
- * Get UI callbacks that silently collect results.
9604
- * Tool progress is logged to stderr in verbose mode.
9605
- */
9606
- getCallbacks() {
9607
- return {
9608
- onTextDelta: (_delta) => {
9609
- },
9610
- onToolCall: (_id, action, params) => {
9611
- if (this.verbose) {
9612
- const paramStr = params ? ` ${JSON.stringify(params)}` : "";
9613
- process.stderr.write(`[tool] ${action}${paramStr} ...
9614
- `);
9615
- }
9616
- },
9617
- onToolResult: (id, action, success2, data, error2, code) => {
9618
- this.toolCalls.push({ id, action, success: success2, data, error: error2, code });
9619
- if (this.verbose) {
9620
- const status = success2 ? "ok" : `error: ${error2}${code ? ` [${code}]` : ""}`;
9621
- process.stderr.write(`[tool] ${action}: ${status}
9622
- `);
9623
- }
9624
- },
9625
- onAssistantMessage: (content) => {
9626
- if (content) {
9627
- this.responseParts.push(content);
9628
- }
9629
- },
9630
- onBalanceSummary: (card) => {
9631
- this.cards.push(card);
9632
- },
9633
- onTurnOutcome: (outcome) => {
9634
- this.outcome = outcome;
9635
- },
9636
- onSuggestions: (_suggestions) => {
9637
- },
9638
- onTxStatus: (txHash, chain, status, explorerUrl) => {
9639
- const existing = this.transactions.find((t) => t.hash === txHash);
9640
- if (existing) {
9641
- existing.status = status;
9642
- if (explorerUrl) existing.explorerUrl = explorerUrl;
9643
- } else {
9644
- this.transactions.push({ hash: txHash, chain, explorerUrl, status });
9645
- }
9646
- if (this.verbose) {
9647
- process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
9648
- `);
9649
- }
9650
- },
9651
- onError: (message, code) => {
9652
- const isTerminal2 = isTerminalAgentErrorCode(code);
9653
- if (!this.error || isTerminal2 && !this.errorIsTerminal) {
9654
- this.error = { message, code };
9655
- this.errorIsTerminal = isTerminal2;
9656
- }
9657
- process.stderr.write(`[error] ${message} [${code}]
9658
- `);
9659
- },
9660
- onProtocolWarning: (warning) => {
9661
- this.warnings.push(warning);
9662
- process.stderr.write(`[warning] ${warning.message} [${warning.code}]
9663
- `);
9664
- },
9665
- onDone: () => {
9666
- },
9667
- requestPassword: async () => {
9668
- throw new Error("Password required but not provided. Use --password flag.");
9669
- },
9670
- requestConfirmation: async (message) => {
9671
- if (!this.autoApprove) {
9672
- process.stderr.write(`[confirm] signing requires --yes \u2014 NOT broadcasting: ${message}
9673
- `);
9674
- } else {
9675
- process.stderr.write(`[confirm] auto-approved (--yes): ${message}
9676
- `);
9677
- }
9678
- return this.autoApprove;
9679
- }
9680
- };
9497
+ // src/commands/settings.ts
9498
+ import { Chain as Chain9, fiatCurrencies as fiatCurrencies2, fiatCurrencyNameRecord as fiatCurrencyNameRecord3 } from "@vultisig/sdk";
9499
+ import chalk6 from "chalk";
9500
+ async function executeCurrency(ctx2, newCurrency) {
9501
+ const vault = await ctx2.ensureActiveVault();
9502
+ if (!newCurrency) {
9503
+ const currentCurrency = vault.currency;
9504
+ const currencyName2 = fiatCurrencyNameRecord3[currentCurrency];
9505
+ printResult(chalk6.cyan("\nCurrent Currency Preference:"));
9506
+ printResult(` ${chalk6.green(currentCurrency.toUpperCase())} - ${currencyName2}`);
9507
+ info(chalk6.gray(`
9508
+ Supported currencies: ${fiatCurrencies2.join(", ")}`));
9509
+ info(chalk6.gray('Use "vultisig currency <code>" to change'));
9510
+ return currentCurrency;
9681
9511
  }
9682
- /**
9683
- * Send a message and wait for the complete response.
9684
- * All tool calls and actions are executed automatically.
9685
- */
9686
- async ask(message) {
9687
- this.responseParts = [];
9688
- this.toolCalls = [];
9689
- this.transactions = [];
9690
- this.cards = [];
9691
- this.warnings = [];
9692
- this.outcome = void 0;
9693
- this.error = void 0;
9694
- this.errorIsTerminal = false;
9695
- const callbacks = this.getCallbacks();
9696
- await this.session.sendMessage(message, callbacks);
9697
- return this.partialResult();
9512
+ const currency = newCurrency.toLowerCase();
9513
+ if (!fiatCurrencies2.includes(currency)) {
9514
+ error(`x Invalid currency: ${newCurrency}`);
9515
+ warn(`Supported currencies: ${fiatCurrencies2.join(", ")}`);
9516
+ throw new Error("Invalid currency");
9698
9517
  }
9699
- /**
9700
- * Snapshot of everything collected so far this turn. Identical to a normal
9701
- * `ask()` return, but callable from a catch block when `ask()` THREW mid-turn
9702
- * e.g. the follow-up request that reports recent_actions back to the backend
9703
- * fails (timeout/5xx/auth) AFTER a tx has already broadcast and `onTxStatus`
9704
- * fired. Lets the caller still surface the already-broadcast tx hash in the
9705
- * error envelope instead of stranding funds the turn just moved.
9706
- */
9707
- partialResult() {
9708
- return {
9709
- sessionId: this.session.getConversationId() || "",
9710
- response: this.responseParts[this.responseParts.length - 1] || "",
9711
- toolCalls: this.toolCalls,
9712
- transactions: this.transactions,
9713
- cards: this.cards,
9714
- warnings: this.warnings,
9715
- error: this.error,
9716
- ...this.outcome ? { outcome: this.outcome } : {}
9717
- };
9518
+ const spinner = createSpinner("Updating currency preference...");
9519
+ await vault.setCurrency(currency);
9520
+ spinner.succeed("Currency updated");
9521
+ const currencyName = fiatCurrencyNameRecord3[currency];
9522
+ if (isJsonOutput()) {
9523
+ outputJson({ currency, name: currencyName, updated: true });
9524
+ return currency;
9718
9525
  }
9719
- };
9720
-
9721
- // src/agent/auth.ts
9722
- import { randomBytes as randomBytes2 } from "node:crypto";
9723
- import { Chain as Chain10, computePersonalSignHash, formatEcdsaSignature65 } from "@vultisig/sdk";
9724
- async function authenticateVault(client, vault, password, maxAttempts = 3) {
9725
- const publicKey = vault.publicKeys.ecdsa;
9726
- const chainCode = vault.hexChainCode;
9727
- const ethAddress = await vault.address(Chain10.Ethereum);
9728
- const nonce = "0x" + randomBytes2(16).toString("hex");
9729
- const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
9730
- const authMessage = JSON.stringify({
9731
- message: "Sign into Vultisig Plugin Marketplace",
9732
- nonce,
9733
- expiresAt,
9734
- address: ethAddress
9735
- });
9736
- const messageHash = computePersonalSignHash(authMessage);
9737
- let lastError = null;
9738
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9739
- try {
9740
- if (attempt > 1) {
9741
- process.stderr.write(` Retry ${attempt}/${maxAttempts}...
9742
- `);
9743
- }
9744
- const signature = await vault.signBytes({ data: Buffer.from(messageHash), chain: Chain10.Ethereum }, {});
9745
- if (signature.recovery === void 0) {
9746
- throw new Error("Agent authentication requires an ECDSA recovery id");
9747
- }
9748
- const sigHex = formatEcdsaSignature65(signature.signature, signature.recovery);
9749
- const authResponse = await client.authenticate({
9750
- public_key: publicKey,
9751
- chain_code_hex: chainCode,
9752
- message: authMessage,
9753
- signature: sigHex
9526
+ success(`
9527
+ + Currency preference set to ${currency.toUpperCase()} (${currencyName})`);
9528
+ return currency;
9529
+ }
9530
+ async function executeServer(ctx2) {
9531
+ const spinner = createSpinner("Checking server status...");
9532
+ try {
9533
+ const status = await ctx2.sdk.getServerStatus();
9534
+ spinner.succeed("Server status retrieved");
9535
+ if (isJsonOutput()) {
9536
+ outputJson({ server: status });
9537
+ return status;
9538
+ }
9539
+ printResult(chalk6.cyan("\nServer Status:\n"));
9540
+ printResult(chalk6.bold("Fast Vault Server:"));
9541
+ printResult(` Online: ${status.fastVault.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9542
+ if (status.fastVault.latency) {
9543
+ printResult(` Latency: ${status.fastVault.latency}ms`);
9544
+ }
9545
+ printResult(chalk6.bold("\nMessage Relay:"));
9546
+ printResult(` Online: ${status.messageRelay.online ? chalk6.green("Yes") : chalk6.red("No")}`);
9547
+ if (status.messageRelay.latency) {
9548
+ printResult(` Latency: ${status.messageRelay.latency}ms`);
9549
+ }
9550
+ return status;
9551
+ } catch (err) {
9552
+ spinner.fail("Failed to check server status");
9553
+ error(`
9554
+ x ${err.message}`);
9555
+ throw err;
9556
+ }
9557
+ }
9558
+ async function executeAddressBook(ctx2, options = {}) {
9559
+ if (options.add) {
9560
+ let chain = options.chain;
9561
+ let address = options.address;
9562
+ let name = options.name;
9563
+ const prompts = [];
9564
+ if (!chain) {
9565
+ prompts.push({
9566
+ type: "select",
9567
+ name: "chain",
9568
+ message: "Select chain:",
9569
+ choices: Object.values(Chain9)
9754
9570
  });
9755
- return {
9756
- token: authResponse.token,
9757
- expiresAt: authResponse.expires_at,
9758
- // Captured + persisted by the session token cache. The backend exposes
9759
- // POST /auth/refresh to exchange it for a fresh access token without a
9760
- // new MPC round; wiring that exchange is a future enhancement — today
9761
- // the CLI re-auths via a full MPC re-sign (authenticateVault), which is
9762
- // always available and avoids depending on refresh-token rotation.
9763
- refreshToken: authResponse.refresh_token
9764
- };
9765
- } catch (err) {
9766
- lastError = err;
9767
- if (attempt < maxAttempts && err.message?.includes("timeout")) {
9768
- continue;
9769
- }
9770
- throw err;
9771
9571
  }
9572
+ if (!address) {
9573
+ prompts.push({
9574
+ type: "input",
9575
+ name: "address",
9576
+ message: "Enter address:",
9577
+ validate: (input) => input.trim() !== "" || "Address is required"
9578
+ });
9579
+ }
9580
+ if (!name) {
9581
+ prompts.push({
9582
+ type: "input",
9583
+ name: "name",
9584
+ message: "Enter name/label:",
9585
+ validate: (input) => input.trim() !== "" || "Name is required"
9586
+ });
9587
+ }
9588
+ if (prompts.length > 0) {
9589
+ const answers = await prompt(prompts);
9590
+ chain = chain || answers.chain;
9591
+ address = address || answers.address?.trim();
9592
+ name = name || answers.name?.trim();
9593
+ }
9594
+ const spinner2 = createSpinner("Adding address to address book...");
9595
+ const entry = {
9596
+ chain,
9597
+ address,
9598
+ name,
9599
+ source: "saved",
9600
+ dateAdded: Date.now()
9601
+ };
9602
+ await ctx2.sdk.addAddressBookEntry([entry]);
9603
+ spinner2.succeed("Address added");
9604
+ if (isJsonOutput()) {
9605
+ outputJson({ added: entry });
9606
+ return [];
9607
+ }
9608
+ success(`
9609
+ + Added ${name} (${chain}: ${address})`);
9610
+ return [];
9772
9611
  }
9773
- throw lastError || new Error("Authentication failed after all attempts");
9612
+ if (options.remove) {
9613
+ const spinner2 = createSpinner("Removing address from address book...");
9614
+ await ctx2.sdk.removeAddressBookEntry([{ address: options.remove, chain: options.chain }]);
9615
+ spinner2.succeed("Address removed");
9616
+ if (isJsonOutput()) {
9617
+ outputJson({ removed: { address: options.remove, chain: options.chain } });
9618
+ return [];
9619
+ }
9620
+ success(`
9621
+ + Removed ${options.remove}`);
9622
+ return [];
9623
+ }
9624
+ const spinner = createSpinner("Loading address book...");
9625
+ const addressBook = await ctx2.sdk.getAddressBook(options.chain);
9626
+ spinner.succeed("Address book loaded");
9627
+ const allEntries = [...addressBook.saved, ...addressBook.vaults];
9628
+ if (isJsonOutput()) {
9629
+ outputJson({ addressBook: allEntries, chain: options.chain });
9630
+ return allEntries;
9631
+ }
9632
+ if (allEntries.length === 0) {
9633
+ warn(`
9634
+ No addresses in address book${options.chain ? ` for ${options.chain}` : ""}`);
9635
+ info(chalk6.gray("\nUse --add to add an address to the address book"));
9636
+ } else {
9637
+ printResult(chalk6.cyan(`
9638
+ Address Book${options.chain ? ` (${options.chain})` : ""}:
9639
+ `));
9640
+ const table = allEntries.map((entry) => ({
9641
+ Name: entry.name,
9642
+ Chain: entry.chain,
9643
+ Address: entry.address,
9644
+ Source: entry.source
9645
+ }));
9646
+ printTable(table);
9647
+ info(chalk6.gray("\nUse --add to add or --remove <address> to remove an address"));
9648
+ }
9649
+ return allEntries;
9774
9650
  }
9775
9651
 
9776
- // src/agent/cards.ts
9777
- import chalk8 from "chalk";
9778
- var CLI_SUPPORTED_SURFACES = ["balance_summary", "turn_outcome"];
9779
- function parseTurnOutcome(raw) {
9780
- if (!raw || typeof raw !== "object") return null;
9781
- const kind = raw.kind;
9782
- if (kind !== "success" && kind !== "blocked" && kind !== "refusal" && kind !== "error") return null;
9783
- const code = raw.code;
9784
- const detail = raw.detail;
9785
- return {
9786
- kind,
9787
- ...typeof code === "string" ? { code } : {},
9788
- ...typeof detail === "string" ? { detail } : {}
9789
- };
9652
+ // src/commands/rujira.ts
9653
+ import {
9654
+ getRoutesSummary,
9655
+ listEasyRoutes,
9656
+ RujiraClient,
9657
+ VultisigRujiraProvider
9658
+ } from "@vultisig/rujira";
9659
+ async function createRujiraClient(ctx2, options = {}) {
9660
+ const vault = await ctx2.ensureActiveVault();
9661
+ const provider = new VultisigRujiraProvider(vault);
9662
+ const client = new RujiraClient({
9663
+ signer: provider,
9664
+ rpcEndpoint: options.rpcEndpoint,
9665
+ config: {
9666
+ // Allow overriding rest endpoint via config (used for thornode calls)
9667
+ ...options.restEndpoint ? { restEndpoint: options.restEndpoint } : {}
9668
+ }
9669
+ });
9670
+ const spinner = createSpinner("Connecting to Rujira/THORChain...");
9671
+ await client.connect();
9672
+ spinner.succeed("Connected");
9673
+ return client;
9790
9674
  }
9791
- function stripControlChars(s) {
9792
- let out = "";
9793
- for (const ch of s) {
9794
- const code = ch.codePointAt(0) ?? 0;
9795
- if (code <= 31 || code >= 127 && code <= 159) continue;
9796
- out += ch;
9675
+ async function executeRujiraBalance(ctx2, options = {}) {
9676
+ const vault = await ctx2.ensureActiveVault();
9677
+ const thorAddress = await vault.address("THORChain");
9678
+ const client = await createRujiraClient(ctx2, options);
9679
+ const spinner = createSpinner("Loading THORChain balances...");
9680
+ const balances = await client.deposit.getBalances(thorAddress);
9681
+ spinner.succeed("Balances loaded");
9682
+ const filtered = options.securedOnly ? balances.filter((b) => b.denom.includes("-") || b.denom.includes("/")) : balances;
9683
+ if (isJsonOutput()) {
9684
+ outputJson({ thorAddress, balances: filtered });
9685
+ return;
9797
9686
  }
9798
- return out;
9799
- }
9800
- function asString(v) {
9801
- return typeof v === "string" ? stripControlChars(v) : "";
9802
- }
9803
- function parseToken(v) {
9804
- if (!v || typeof v !== "object") return null;
9805
- const o = v;
9806
- const symbol = asString(o.symbol);
9807
- const amountDecimal = asString(o.amountDecimal);
9808
- if (!symbol && !amountDecimal) return null;
9809
- const token = { symbol, amountDecimal };
9810
- const amountUsd = asString(o.amountUsd);
9811
- if (amountUsd) token.amountUsd = amountUsd;
9812
- return token;
9813
- }
9814
- function parseAccount(v) {
9815
- if (!v || typeof v !== "object") return null;
9816
- const o = v;
9817
- const chainId = asString(o.chainId);
9818
- if (!chainId) return null;
9819
- const tokensRaw = Array.isArray(o.tokens) ? o.tokens : [];
9820
- const tokens = tokensRaw.map(parseToken).filter((t) => t !== null);
9821
- return { chainId, address: asString(o.address) || "\u2014", tokens };
9687
+ info(`THORChain address: ${thorAddress}`);
9688
+ if (!filtered.length) {
9689
+ printResult("No balances found");
9690
+ return;
9691
+ }
9692
+ printTable(
9693
+ filtered.map((b) => ({
9694
+ asset: b.asset,
9695
+ denom: b.denom,
9696
+ amount: b.formatted,
9697
+ raw: b.amount
9698
+ }))
9699
+ );
9822
9700
  }
9823
- function parseBalanceSummaryEnvelope(value) {
9824
- if (!value || typeof value !== "object") return null;
9825
- const o = value;
9826
- if (o.surface !== "balance_summary") return null;
9827
- if (!Array.isArray(o.accounts)) return null;
9828
- const accounts = o.accounts.map(parseAccount).filter((a) => a !== null);
9829
- if (accounts.length === 0) return null;
9830
- const card = { surface: "balance_summary", accounts };
9831
- if (o.stale === true) {
9832
- card.stale = true;
9833
- if (typeof o.stale_secs === "number") card.staleSecs = o.stale_secs;
9701
+ async function executeRujiraRoutes() {
9702
+ const routes = listEasyRoutes();
9703
+ const summary = getRoutesSummary();
9704
+ if (isJsonOutput()) {
9705
+ outputJson({ routes, summary });
9706
+ return;
9834
9707
  }
9835
- return card;
9708
+ printResult(summary);
9709
+ printResult("");
9710
+ printTable(
9711
+ routes.map((r) => ({
9712
+ name: r.name,
9713
+ from: r.from,
9714
+ to: r.to,
9715
+ liquidity: r.liquidity,
9716
+ description: r.description
9717
+ }))
9718
+ );
9836
9719
  }
9837
- function matchBrace(text, start) {
9838
- let depth = 0;
9839
- let inString = false;
9840
- let escaped = false;
9841
- for (let i = start; i < text.length; i++) {
9842
- const ch = text[i];
9843
- if (inString) {
9844
- if (escaped) escaped = false;
9845
- else if (ch === "\\") escaped = true;
9846
- else if (ch === '"') inString = false;
9847
- continue;
9848
- }
9849
- if (ch === '"') inString = true;
9850
- else if (ch === "{") depth++;
9851
- else if (ch === "}") {
9852
- depth--;
9853
- if (depth === 0) return i;
9720
+ async function executeRujiraDeposit(ctx2, options = {}) {
9721
+ const vault = await ctx2.ensureActiveVault();
9722
+ const thorAddress = await vault.address("THORChain");
9723
+ const client = await createRujiraClient(ctx2, options);
9724
+ if (!options.asset) {
9725
+ const spinner2 = createSpinner("Loading THORChain inbound addresses...");
9726
+ const inbound = await client.deposit.getInboundAddresses();
9727
+ spinner2.succeed("Inbound addresses loaded");
9728
+ if (isJsonOutput()) {
9729
+ outputJson({ thorAddress, inboundAddresses: inbound });
9730
+ return;
9854
9731
  }
9732
+ info(`THORChain address: ${thorAddress}`);
9733
+ printResult("Provide an L1 asset to get a chain-specific inbound address + memo.");
9734
+ printResult("Example: vultisig rujira deposit --asset BTC.BTC --amount 100000");
9735
+ printResult("");
9736
+ printTable(
9737
+ inbound.map((a) => ({
9738
+ chain: a.chain,
9739
+ address: a.address,
9740
+ halted: a.halted,
9741
+ globalTradingPaused: a.global_trading_paused,
9742
+ chainTradingPaused: a.chain_trading_paused
9743
+ }))
9744
+ );
9745
+ return;
9746
+ }
9747
+ const amount = options.amount ?? "1";
9748
+ const spinner = createSpinner("Preparing deposit instructions...");
9749
+ const prepared = await client.deposit.prepare({
9750
+ fromAsset: options.asset,
9751
+ amount,
9752
+ thorAddress,
9753
+ affiliate: options.affiliate,
9754
+ affiliateBps: options.affiliateBps
9755
+ });
9756
+ spinner.succeed("Deposit prepared");
9757
+ if (isJsonOutput()) {
9758
+ outputJson({ thorAddress, deposit: prepared });
9759
+ return;
9760
+ }
9761
+ info(`THORChain address: ${thorAddress}`);
9762
+ printResult("Deposit instructions (send from L1):");
9763
+ printResult(` Chain: ${prepared.chain}`);
9764
+ printResult(` Asset: ${prepared.asset}`);
9765
+ printResult(` Inbound address:${prepared.inboundAddress}`);
9766
+ printResult(` Memo: ${prepared.memo}`);
9767
+ printResult(` Min amount: ${prepared.minimumAmount}`);
9768
+ if (prepared.warning) {
9769
+ warn(prepared.warning);
9855
9770
  }
9856
- return -1;
9857
9771
  }
9858
- function extractBalanceSummaryFromText(content) {
9859
- if (!content || !content.includes("balance_summary")) return null;
9860
- if (content.length > 2e5) return null;
9861
- for (let i = content.indexOf("{"); i !== -1; i = content.indexOf("{", i + 1)) {
9862
- const end = matchBrace(content, i);
9863
- if (end === -1) break;
9864
- const blob = content.slice(i, end + 1);
9865
- if (!blob.includes("balance_summary")) continue;
9866
- let parsed;
9867
- try {
9868
- parsed = JSON.parse(blob);
9869
- } catch {
9870
- continue;
9772
+ async function executeRujiraSwap(ctx2, options) {
9773
+ const vault = await ctx2.ensureActiveVault();
9774
+ if (!options.dryRun && !options.yes && isNonInteractive()) {
9775
+ throw new ConfirmationRequiredError(
9776
+ "Swap requires confirmation.",
9777
+ "Pass --yes to confirm, or --dry-run to preview."
9778
+ );
9779
+ }
9780
+ const client = await createRujiraClient(ctx2, options);
9781
+ const destination = options.destination ?? await vault.address("THORChain");
9782
+ const quoteSpinner = createSpinner("Getting FIN swap quote...");
9783
+ const quote = await client.swap.getQuote({
9784
+ fromAsset: options.fromAsset,
9785
+ toAsset: options.toAsset,
9786
+ amount: options.amount,
9787
+ destination,
9788
+ slippageBps: options.slippageBps
9789
+ });
9790
+ quoteSpinner.succeed("Quote received");
9791
+ if (options.dryRun) {
9792
+ if (isJsonOutput()) {
9793
+ outputJson({ dryRun: true, quote });
9794
+ } else {
9795
+ printResult("FIN Swap Preview (dry-run)");
9796
+ printResult(` From: ${options.fromAsset}`);
9797
+ printResult(` To: ${options.toAsset}`);
9798
+ printResult(` Amount (in): ${options.amount}`);
9799
+ printResult(` Expected out:${quote.expectedOutput}`);
9800
+ printResult(` Min out: ${quote.minimumOutput}`);
9801
+ printResult(` Contract: ${quote.contractAddress}`);
9871
9802
  }
9872
- const card = parseBalanceSummaryEnvelope(parsed);
9873
- if (!card) continue;
9874
- const before = content.slice(0, i).replace(/```(?:json)?\s*$/i, "");
9875
- const after = content.slice(end + 1).replace(/^\s*```/, "");
9876
- const remainingText = (before + after).trim();
9877
- return { card, remainingText };
9803
+ return;
9878
9804
  }
9879
- return null;
9880
- }
9881
- function shortenAddress(address) {
9882
- if (!address || address === "\u2014") return address || "\u2014";
9883
- if (address.length <= 16) return address;
9884
- return `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
9885
- }
9886
- function parseUsd(amountUsd) {
9887
- if (!amountUsd) return null;
9888
- const cleaned = amountUsd.replace(/[$,\s]/g, "");
9889
- if (!cleaned) return null;
9890
- const n = Number(cleaned);
9891
- return Number.isFinite(n) ? n : null;
9892
- }
9893
- function formatUsd(n) {
9894
- return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
9895
- }
9896
- function renderBalanceSummaryCard(card) {
9897
- const lines = [];
9898
- const staleCue = card.stale ? chalk8.gray(` (stale${card.staleSecs ? ` ~${Math.round(card.staleSecs / 60)}m` : ""}, refreshing\u2026)`) : "";
9899
- lines.push(chalk8.bold(" Balances") + staleCue);
9900
- let total = 0;
9901
- let sawUsd = false;
9902
- for (const account of card.accounts) {
9903
- lines.push(` ${chalk8.cyan(account.chainId)} ${chalk8.gray(`(${shortenAddress(account.address)})`)}`);
9904
- if (account.tokens.length === 0) {
9905
- lines.push(chalk8.gray(" (no balances)"));
9906
- continue;
9805
+ if (!isJsonOutput()) {
9806
+ printResult("FIN Swap Preview");
9807
+ printResult(` From: ${options.fromAsset}`);
9808
+ printResult(` To: ${options.toAsset}`);
9809
+ printResult(` Amount (in): ${options.amount}`);
9810
+ printResult(` Expected out:${quote.expectedOutput}`);
9811
+ printResult(` Min out: ${quote.minimumOutput}`);
9812
+ printResult(` Contract: ${quote.contractAddress}`);
9813
+ if (quote.warning) {
9814
+ warn(quote.warning);
9907
9815
  }
9908
- for (const token of account.tokens) {
9909
- const usd = parseUsd(token.amountUsd);
9910
- if (usd !== null) {
9911
- total += usd;
9912
- sawUsd = true;
9913
- }
9914
- const symbol = token.symbol.padEnd(10);
9915
- const amount = token.amountDecimal.padStart(16);
9916
- const usdCol = token.amountUsd ? chalk8.gray(` ${token.amountUsd}`) : "";
9917
- lines.push(` ${chalk8.bold(symbol)}${amount}${usdCol}`);
9816
+ }
9817
+ if (!options.yes) {
9818
+ warn("This command will execute a swap. Re-run with -y/--yes to skip this warning.");
9819
+ throw new ConfirmationRequiredError(
9820
+ "Swap requires confirmation.",
9821
+ "Pass --yes to confirm, or --dry-run to preview."
9822
+ );
9823
+ }
9824
+ await ensureVaultUnlocked(vault, options.password);
9825
+ const execSpinner = createSpinner("Executing FIN swap...");
9826
+ const result = await client.swap.execute(quote, { slippageBps: options.slippageBps });
9827
+ execSpinner.succeed("Swap submitted");
9828
+ if (isJsonOutput()) {
9829
+ outputJson({ quote, result });
9830
+ } else {
9831
+ printResult(`Tx Hash: ${result.txHash}`);
9832
+ }
9833
+ }
9834
+ async function executeRujiraWithdraw(ctx2, options) {
9835
+ const vault = await ctx2.ensureActiveVault();
9836
+ if (!options.dryRun && !options.yes && isNonInteractive()) {
9837
+ throw new ConfirmationRequiredError(
9838
+ "Withdrawal requires confirmation.",
9839
+ "Pass --yes to confirm, or --dry-run to preview."
9840
+ );
9841
+ }
9842
+ const client = await createRujiraClient(ctx2, options);
9843
+ const prepSpinner = createSpinner("Preparing withdrawal (MsgDeposit)...");
9844
+ const prepared = await client.withdraw.prepare({
9845
+ asset: options.asset,
9846
+ amount: options.amount,
9847
+ l1Address: options.l1Address,
9848
+ maxFeeBps: options.maxFeeBps
9849
+ });
9850
+ prepSpinner.succeed("Withdrawal prepared");
9851
+ if (options.dryRun) {
9852
+ if (isJsonOutput()) {
9853
+ outputJson({ dryRun: true, prepared });
9854
+ } else {
9855
+ printResult("Withdraw Preview (dry-run)");
9856
+ printResult(` Asset: ${prepared.asset}`);
9857
+ printResult(` Amount: ${prepared.amount}`);
9858
+ printResult(` Destination: ${prepared.destination}`);
9859
+ printResult(` Memo: ${prepared.memo}`);
9860
+ printResult(` Est. fee: ${prepared.estimatedFee}`);
9918
9861
  }
9862
+ return;
9919
9863
  }
9920
- if (sawUsd) {
9921
- lines.push(chalk8.gray(" " + "\u2500".repeat(36)));
9922
- lines.push(` ${chalk8.bold("Total")} ${chalk8.green(formatUsd(total))}`);
9864
+ if (!isJsonOutput()) {
9865
+ printResult("Withdraw Preview");
9866
+ printResult(` Asset: ${prepared.asset}`);
9867
+ printResult(` Amount: ${prepared.amount}`);
9868
+ printResult(` Destination: ${prepared.destination}`);
9869
+ printResult(` Memo: ${prepared.memo}`);
9870
+ printResult(` Est. fee: ${prepared.estimatedFee}`);
9871
+ }
9872
+ if (!options.yes) {
9873
+ warn("This command will broadcast a THORChain MsgDeposit withdrawal. Re-run with -y/--yes to proceed.");
9874
+ throw new ConfirmationRequiredError(
9875
+ "Withdrawal requires confirmation.",
9876
+ "Pass --yes to confirm, or --dry-run to preview."
9877
+ );
9878
+ }
9879
+ await ensureVaultUnlocked(vault, options.password);
9880
+ const execSpinner = createSpinner("Broadcasting withdrawal...");
9881
+ const result = await client.withdraw.execute(prepared);
9882
+ execSpinner.succeed("Withdrawal submitted");
9883
+ if (isJsonOutput()) {
9884
+ outputJson({ prepared, result });
9885
+ } else {
9886
+ printResult(`Tx Hash: ${result.txHash}`);
9923
9887
  }
9924
- return lines.join("\n");
9925
9888
  }
9926
9889
 
9927
- // src/agent/client.ts
9928
- import { randomUUID } from "node:crypto";
9929
-
9930
- // src/agent/toolOutputSigning.ts
9931
- import { getChainKind as getChainKind4 } from "@vultisig/sdk";
9932
-
9933
- // src/agent/executor.ts
9890
+ // src/commands/discount.ts
9934
9891
  import {
9935
- Chain as Chain11,
9936
- chainFeeCoin,
9937
- getChainKind as getChainKind3,
9938
- resolveChainReference,
9939
- VaultError as VaultError3,
9940
- VaultErrorCode as VaultErrorCode3,
9941
- Vultisig as VultisigSdk
9892
+ baseAffiliateBps,
9893
+ vultDiscountTierBps,
9894
+ vultDiscountTierMinBalances
9942
9895
  } from "@vultisig/sdk";
9943
-
9944
- // ../../node_modules/viem/_esm/accounts/utils/publicKeyToAddress.js
9945
- init_getAddress();
9946
- init_keccak256();
9947
- function publicKeyToAddress(publicKey) {
9948
- const address = keccak256(`0x${publicKey.substring(4)}`).substring(26);
9949
- return checksumAddress(`0x${address}`);
9896
+ import chalk7 from "chalk";
9897
+ var TIER_CONFIG = {
9898
+ none: { bps: baseAffiliateBps, discount: 0 },
9899
+ ...Object.fromEntries(
9900
+ Object.entries(vultDiscountTierMinBalances).map(([tier, minVult]) => [
9901
+ tier,
9902
+ {
9903
+ bps: baseAffiliateBps - vultDiscountTierBps[tier],
9904
+ discount: vultDiscountTierBps[tier],
9905
+ minVult
9906
+ }
9907
+ ])
9908
+ )
9909
+ };
9910
+ function getTierColor(tier) {
9911
+ const colors = {
9912
+ none: chalk7.gray,
9913
+ bronze: chalk7.hex("#CD7F32"),
9914
+ silver: chalk7.hex("#C0C0C0"),
9915
+ gold: chalk7.hex("#FFD700"),
9916
+ platinum: chalk7.hex("#E5E4E2"),
9917
+ diamond: chalk7.hex("#B9F2FF"),
9918
+ ultimate: chalk7.hex("#FF00FF")
9919
+ };
9920
+ return colors[tier] || chalk7.white;
9950
9921
  }
9951
-
9952
- // ../../node_modules/viem/_esm/utils/signature/recoverPublicKey.js
9953
- init_isHex();
9954
- init_size();
9955
- init_fromHex();
9956
- init_toHex();
9957
- async function recoverPublicKey({ hash, signature }) {
9958
- const hashHex = isHex(hash) ? hash : toHex(hash);
9959
- const { secp256k1: secp256k12 } = await Promise.resolve().then(() => (init_secp256k1(), secp256k1_exports));
9960
- const signature_ = (() => {
9961
- if (typeof signature === "object" && "r" in signature && "s" in signature) {
9962
- const { r, s, v, yParity } = signature;
9963
- const yParityOrV2 = Number(yParity ?? v);
9964
- const recoveryBit2 = toRecoveryBit(yParityOrV2);
9965
- return new secp256k12.Signature(hexToBigInt(r), hexToBigInt(s)).addRecoveryBit(recoveryBit2);
9966
- }
9967
- const signatureHex = isHex(signature) ? signature : toHex(signature);
9968
- if (size(signatureHex) !== 65)
9969
- throw new Error("invalid signature length");
9970
- const yParityOrV = hexToNumber(`0x${signatureHex.slice(130)}`);
9971
- const recoveryBit = toRecoveryBit(yParityOrV);
9972
- return secp256k12.Signature.fromCompact(signatureHex.substring(2, 130)).addRecoveryBit(recoveryBit);
9973
- })();
9974
- const publicKey = signature_.recoverPublicKey(hashHex.substring(2)).toHex(false);
9975
- return `0x${publicKey}`;
9922
+ function getNextTier(currentTier) {
9923
+ const tierOrder = ["none", "bronze", "silver", "gold", "platinum", "diamond", "ultimate"];
9924
+ const currentIndex = tierOrder.indexOf(currentTier);
9925
+ if (currentIndex === -1 || currentIndex >= tierOrder.length - 1) {
9926
+ return null;
9927
+ }
9928
+ const nextTierName = tierOrder[currentIndex + 1];
9929
+ const config = TIER_CONFIG[nextTierName];
9930
+ if ("minVult" in config) {
9931
+ return { name: nextTierName, vultRequired: config.minVult };
9932
+ }
9933
+ return null;
9976
9934
  }
9977
- function toRecoveryBit(yParityOrV) {
9978
- if (yParityOrV === 0 || yParityOrV === 1)
9979
- return yParityOrV;
9980
- if (yParityOrV === 27)
9981
- return 0;
9982
- if (yParityOrV === 28)
9983
- return 1;
9984
- throw new Error("Invalid yParityOrV value");
9935
+ async function executeDiscount(ctx2, options = {}) {
9936
+ const vault = await ctx2.ensureActiveVault();
9937
+ const spinner = createSpinner(options.refresh ? "Refreshing discount tier..." : "Loading discount tier...");
9938
+ const tierResult = options.refresh ? await vault.updateDiscountTier() : await vault.getDiscountTier();
9939
+ const tier = tierResult || "none";
9940
+ const config = TIER_CONFIG[tier];
9941
+ const nextTier = getNextTier(tier);
9942
+ const tierInfo = {
9943
+ tier,
9944
+ feeBps: config.bps,
9945
+ discountBps: config.discount,
9946
+ nextTier
9947
+ };
9948
+ spinner.succeed("Discount tier loaded");
9949
+ if (isJsonOutput()) {
9950
+ outputJson({
9951
+ tier: tierInfo.tier,
9952
+ feeBps: tierInfo.feeBps,
9953
+ discountBps: tierInfo.discountBps,
9954
+ nextTier: tierInfo.nextTier
9955
+ });
9956
+ return tierInfo;
9957
+ }
9958
+ displayDiscountTier(tierInfo);
9959
+ return tierInfo;
9985
9960
  }
9986
-
9987
- // ../../node_modules/viem/_esm/utils/signature/recoverAddress.js
9988
- async function recoverAddress({ hash, signature }) {
9989
- return publicKeyToAddress(await recoverPublicKey({ hash, signature }));
9961
+ function displayDiscountTier(tierInfo) {
9962
+ const tierColor = getTierColor(tierInfo.tier);
9963
+ printResult(chalk7.cyan("\n+----------------------------------------+"));
9964
+ printResult(chalk7.cyan("| VULT Discount Tier |"));
9965
+ printResult(chalk7.cyan("+----------------------------------------+\n"));
9966
+ const tierDisplay = tierInfo.tier === "none" ? chalk7.gray("No Tier") : tierColor(tierInfo.tier.charAt(0).toUpperCase() + tierInfo.tier.slice(1));
9967
+ printResult(` Current Tier: ${tierDisplay}`);
9968
+ if (tierInfo.tier === "none") {
9969
+ printResult(` Swap Fee: ${chalk7.gray("50 bps (0.50%)")}`);
9970
+ printResult(` Discount: ${chalk7.gray("None")}`);
9971
+ } else {
9972
+ printResult(` Swap Fee: ${chalk7.green(`${tierInfo.feeBps} bps (${(tierInfo.feeBps / 100).toFixed(2)}%)`)}`);
9973
+ printResult(` Discount: ${chalk7.green(`${tierInfo.discountBps} bps saved`)}`);
9974
+ }
9975
+ if (tierInfo.nextTier) {
9976
+ const nextTierColor = getTierColor(tierInfo.nextTier.name);
9977
+ printResult(chalk7.bold("\n Next Tier:"));
9978
+ printResult(
9979
+ ` ${nextTierColor(tierInfo.nextTier.name.charAt(0).toUpperCase() + tierInfo.nextTier.name.slice(1))} - requires ${tierInfo.nextTier.vultRequired.toLocaleString()} VULT`
9980
+ );
9981
+ } else if (tierInfo.tier === "ultimate") {
9982
+ printResult(chalk7.bold("\n ") + chalk7.magenta("You have the highest tier! 0% swap fees."));
9983
+ }
9984
+ info(chalk7.gray("\n Tip: Thorguard NFT holders get +1 tier upgrade (up to gold)"));
9985
+ printResult("");
9990
9986
  }
9991
9987
 
9992
- // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
9993
- init_encodeAbiParameters();
9994
- init_concat();
9995
- init_toHex();
9996
- init_keccak256();
9988
+ // src/commands/auth.ts
9989
+ import { executeAuthLogout, executeAuthSetup, executeAuthStatus } from "@vultisig/client-shared";
9997
9990
 
9998
- // ../../node_modules/viem/_esm/utils/typedData.js
9999
- init_abi();
10000
- init_address();
9991
+ // src/commands/agent.ts
9992
+ import chalk10 from "chalk";
9993
+ import Table from "cli-table3";
10001
9994
 
10002
- // ../../node_modules/viem/_esm/errors/typedData.js
10003
- init_stringify();
10004
- init_base();
10005
- var InvalidDomainError = class extends BaseError {
10006
- constructor({ domain }) {
10007
- super(`Invalid domain "${stringify(domain)}".`, {
10008
- metaMessages: ["Must be a valid EIP-712 domain."]
10009
- });
9995
+ // src/agent/ask.ts
9996
+ var AskInterface = class {
9997
+ session;
9998
+ verbose;
9999
+ autoApprove;
10000
+ responseParts = [];
10001
+ toolCalls = [];
10002
+ transactions = [];
10003
+ cards = [];
10004
+ warnings = [];
10005
+ outcome;
10006
+ error;
10007
+ // Tracks whether the currently-latched `error` is a terminal one (e.g. the
10008
+ // depth cap). A terminal error may overwrite a prior non-terminal one; once a
10009
+ // terminal error is recorded, later frames cannot replace it. See onError.
10010
+ errorIsTerminal = false;
10011
+ constructor(session, verbose = false, autoApprove = false) {
10012
+ this.session = session;
10013
+ this.verbose = verbose;
10014
+ this.autoApprove = autoApprove;
10010
10015
  }
10011
- };
10012
- var InvalidPrimaryTypeError = class extends BaseError {
10013
- constructor({ primaryType, types }) {
10014
- super(`Invalid primary type \`${primaryType}\` must be one of \`${JSON.stringify(Object.keys(types))}\`.`, {
10015
- docsPath: "/api/glossary/Errors#typeddatainvalidprimarytypeerror",
10016
- metaMessages: ["Check that the primary type is a key in `types`."]
10017
- });
10016
+ /**
10017
+ * Whether the turn threw with a still-unacknowledged broadcast (the F1
10018
+ * ack-failure case). The command's catch uses this to gate the ACK_FAILED
10019
+ * re-tag so a later, unrelated retryable error after an already-acked
10020
+ * broadcast keeps its own (retryable) classification instead of exit 8.
10021
+ */
10022
+ hasUnacknowledgedBroadcast() {
10023
+ return this.session.hasUnacknowledgedBroadcast();
10018
10024
  }
10019
- };
10020
- var InvalidStructTypeError = class extends BaseError {
10021
- constructor({ type }) {
10022
- super(`Struct type "${type}" is invalid.`, {
10023
- metaMessages: ["Struct type must not be a Solidity type."],
10024
- name: "InvalidStructTypeError"
10025
- });
10025
+ /**
10026
+ * Get UI callbacks that silently collect results.
10027
+ * Tool progress is logged to stderr in verbose mode.
10028
+ */
10029
+ getCallbacks() {
10030
+ return {
10031
+ onTextDelta: (_delta) => {
10032
+ },
10033
+ onToolCall: (_id, action, params) => {
10034
+ if (this.verbose) {
10035
+ const paramStr = params ? ` ${JSON.stringify(params)}` : "";
10036
+ process.stderr.write(`[tool] ${action}${paramStr} ...
10037
+ `);
10038
+ }
10039
+ },
10040
+ onToolResult: (id, action, success2, data, error2, code) => {
10041
+ this.toolCalls.push({ id, action, success: success2, data, error: error2, code });
10042
+ if (this.verbose) {
10043
+ const status = success2 ? "ok" : `error: ${error2}${code ? ` [${code}]` : ""}`;
10044
+ process.stderr.write(`[tool] ${action}: ${status}
10045
+ `);
10046
+ }
10047
+ },
10048
+ onAssistantMessage: (content) => {
10049
+ if (content) {
10050
+ this.responseParts.push(content);
10051
+ }
10052
+ },
10053
+ onBalanceSummary: (card) => {
10054
+ this.cards.push(card);
10055
+ },
10056
+ onTurnOutcome: (outcome) => {
10057
+ this.outcome = outcome;
10058
+ },
10059
+ onSuggestions: (_suggestions) => {
10060
+ },
10061
+ onTxStatus: (txHash, chain, status, explorerUrl) => {
10062
+ const existing = this.transactions.find((t) => t.hash === txHash);
10063
+ if (existing) {
10064
+ existing.status = status;
10065
+ if (explorerUrl) existing.explorerUrl = explorerUrl;
10066
+ } else {
10067
+ this.transactions.push({ hash: txHash, chain, explorerUrl, status });
10068
+ }
10069
+ if (this.verbose) {
10070
+ process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
10071
+ `);
10072
+ }
10073
+ },
10074
+ onError: (message, code) => {
10075
+ const isTerminal2 = isTerminalAgentErrorCode(code);
10076
+ if (!this.error || isTerminal2 && !this.errorIsTerminal) {
10077
+ this.error = { message, code };
10078
+ this.errorIsTerminal = isTerminal2;
10079
+ }
10080
+ process.stderr.write(`[error] ${message} [${code}]
10081
+ `);
10082
+ },
10083
+ onProtocolWarning: (warning) => {
10084
+ this.warnings.push(warning);
10085
+ process.stderr.write(`[warning] ${warning.message} [${warning.code}]
10086
+ `);
10087
+ },
10088
+ onDone: () => {
10089
+ },
10090
+ requestPassword: async () => {
10091
+ throw new Error("Password required but not provided. Use --password flag.");
10092
+ },
10093
+ requestConfirmation: async (message) => {
10094
+ if (!this.autoApprove) {
10095
+ process.stderr.write(`[confirm] signing requires --yes \u2014 NOT broadcasting: ${message}
10096
+ `);
10097
+ } else {
10098
+ process.stderr.write(`[confirm] auto-approved (--yes): ${message}
10099
+ `);
10100
+ }
10101
+ return this.autoApprove;
10102
+ }
10103
+ };
10104
+ }
10105
+ /**
10106
+ * Send a message and wait for the complete response.
10107
+ * All tool calls and actions are executed automatically.
10108
+ */
10109
+ async ask(message) {
10110
+ this.responseParts = [];
10111
+ this.toolCalls = [];
10112
+ this.transactions = [];
10113
+ this.cards = [];
10114
+ this.warnings = [];
10115
+ this.outcome = void 0;
10116
+ this.error = void 0;
10117
+ this.errorIsTerminal = false;
10118
+ const callbacks = this.getCallbacks();
10119
+ await this.session.sendMessage(message, callbacks);
10120
+ return this.partialResult();
10121
+ }
10122
+ /**
10123
+ * Snapshot of everything collected so far this turn. Identical to a normal
10124
+ * `ask()` return, but callable from a catch block when `ask()` THREW mid-turn
10125
+ * — e.g. the follow-up request that reports recent_actions back to the backend
10126
+ * fails (timeout/5xx/auth) AFTER a tx has already broadcast and `onTxStatus`
10127
+ * fired. Lets the caller still surface the already-broadcast tx hash in the
10128
+ * error envelope instead of stranding funds the turn just moved.
10129
+ */
10130
+ partialResult() {
10131
+ return {
10132
+ sessionId: this.session.getConversationId() || "",
10133
+ response: this.responseParts[this.responseParts.length - 1] || "",
10134
+ toolCalls: this.toolCalls,
10135
+ transactions: this.transactions,
10136
+ cards: this.cards,
10137
+ warnings: this.warnings,
10138
+ error: this.error,
10139
+ ...this.outcome ? { outcome: this.outcome } : {}
10140
+ };
10026
10141
  }
10027
10142
  };
10028
10143
 
10029
- // ../../node_modules/viem/_esm/utils/typedData.js
10030
- init_isAddress();
10031
- init_size();
10032
- init_toHex();
10033
- init_regex();
10034
- function validateTypedData(parameters) {
10035
- const { domain, message, primaryType, types } = parameters;
10036
- const validateData = (struct, data) => {
10037
- for (const param of struct) {
10038
- const { name, type } = param;
10039
- const value = data[name];
10040
- const integerMatch = type.match(integerRegex);
10041
- if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
10042
- const [_type, base, size_] = integerMatch;
10043
- numberToHex(value, {
10044
- signed: base === "int",
10045
- size: Number.parseInt(size_, 10) / 8
10046
- });
10144
+ // src/agent/auth.ts
10145
+ import { randomBytes as randomBytes3 } from "node:crypto";
10146
+ import { Chain as Chain10, computePersonalSignHash, formatEcdsaSignature65 } from "@vultisig/sdk";
10147
+ async function authenticateVault(client, vault, password, maxAttempts = 3) {
10148
+ const publicKey = vault.publicKeys.ecdsa;
10149
+ const chainCode = vault.hexChainCode;
10150
+ const ethAddress = await vault.address(Chain10.Ethereum);
10151
+ const nonce = "0x" + randomBytes3(16).toString("hex");
10152
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
10153
+ const authMessage = JSON.stringify({
10154
+ message: "Sign into Vultisig Plugin Marketplace",
10155
+ nonce,
10156
+ expiresAt,
10157
+ address: ethAddress
10158
+ });
10159
+ const messageHash = computePersonalSignHash(authMessage);
10160
+ let lastError = null;
10161
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
10162
+ try {
10163
+ if (attempt > 1) {
10164
+ process.stderr.write(` Retry ${attempt}/${maxAttempts}...
10165
+ `);
10047
10166
  }
10048
- if (type === "address" && typeof value === "string" && !isAddress(value))
10049
- throw new InvalidAddressError2({ address: value });
10050
- const bytesMatch = type.match(bytesRegex);
10051
- if (bytesMatch) {
10052
- const [_type, size_] = bytesMatch;
10053
- if (size_ && size(value) !== Number.parseInt(size_, 10))
10054
- throw new BytesSizeMismatchError({
10055
- expectedSize: Number.parseInt(size_, 10),
10056
- givenSize: size(value)
10057
- });
10167
+ const signature = await vault.signBytes({ data: Buffer.from(messageHash), chain: Chain10.Ethereum }, {});
10168
+ if (signature.recovery === void 0) {
10169
+ throw new Error("Agent authentication requires an ECDSA recovery id");
10058
10170
  }
10059
- const struct2 = types[type];
10060
- if (struct2) {
10061
- validateReference(type);
10062
- validateData(struct2, value);
10171
+ const sigHex = formatEcdsaSignature65(signature.signature, signature.recovery);
10172
+ const authResponse = await client.authenticate({
10173
+ public_key: publicKey,
10174
+ chain_code_hex: chainCode,
10175
+ message: authMessage,
10176
+ signature: sigHex
10177
+ });
10178
+ return {
10179
+ token: authResponse.token,
10180
+ expiresAt: authResponse.expires_at,
10181
+ // Captured + persisted by the session token cache. The backend exposes
10182
+ // POST /auth/refresh to exchange it for a fresh access token without a
10183
+ // new MPC round; wiring that exchange is a future enhancement — today
10184
+ // the CLI re-auths via a full MPC re-sign (authenticateVault), which is
10185
+ // always available and avoids depending on refresh-token rotation.
10186
+ refreshToken: authResponse.refresh_token
10187
+ };
10188
+ } catch (err) {
10189
+ lastError = err;
10190
+ if (attempt < maxAttempts && err.message?.includes("timeout")) {
10191
+ continue;
10063
10192
  }
10193
+ throw err;
10064
10194
  }
10065
- };
10066
- if (types.EIP712Domain && domain) {
10067
- if (typeof domain !== "object")
10068
- throw new InvalidDomainError({ domain });
10069
- validateData(types.EIP712Domain, domain);
10070
- }
10071
- if (primaryType !== "EIP712Domain") {
10072
- if (types[primaryType])
10073
- validateData(types[primaryType], message);
10074
- else
10075
- throw new InvalidPrimaryTypeError({ primaryType, types });
10076
10195
  }
10077
- }
10078
- function getTypesForEIP712Domain({ domain }) {
10079
- return [
10080
- typeof domain?.name === "string" && { name: "name", type: "string" },
10081
- domain?.version && { name: "version", type: "string" },
10082
- (typeof domain?.chainId === "number" || typeof domain?.chainId === "bigint") && {
10083
- name: "chainId",
10084
- type: "uint256"
10085
- },
10086
- domain?.verifyingContract && {
10087
- name: "verifyingContract",
10088
- type: "address"
10089
- },
10090
- domain?.salt && { name: "salt", type: "bytes32" }
10091
- ].filter(Boolean);
10092
- }
10093
- function validateReference(type) {
10094
- if (type === "address" || type === "bool" || type === "string" || type.startsWith("bytes") || type.startsWith("uint") || type.startsWith("int"))
10095
- throw new InvalidStructTypeError({ type });
10196
+ throw lastError || new Error("Authentication failed after all attempts");
10096
10197
  }
10097
10198
 
10098
- // ../../node_modules/viem/_esm/utils/signature/hashTypedData.js
10099
- function hashTypedData(parameters) {
10100
- const { domain = {}, message, primaryType } = parameters;
10101
- const types = {
10102
- EIP712Domain: getTypesForEIP712Domain({ domain }),
10103
- ...parameters.types
10199
+ // src/agent/cards.ts
10200
+ import chalk8 from "chalk";
10201
+ var CLI_SUPPORTED_SURFACES = ["balance_summary", "turn_outcome"];
10202
+ function parseTurnOutcome(raw) {
10203
+ if (!raw || typeof raw !== "object") return null;
10204
+ const kind = raw.kind;
10205
+ if (kind !== "success" && kind !== "blocked" && kind !== "refusal" && kind !== "error") return null;
10206
+ const code = raw.code;
10207
+ const detail = raw.detail;
10208
+ return {
10209
+ kind,
10210
+ ...typeof code === "string" ? { code } : {},
10211
+ ...typeof detail === "string" ? { detail } : {}
10104
10212
  };
10105
- validateTypedData({
10106
- domain,
10107
- message,
10108
- primaryType,
10109
- types
10110
- });
10111
- const parts = ["0x1901"];
10112
- if (domain)
10113
- parts.push(hashDomain({
10114
- domain,
10115
- types
10116
- }));
10117
- if (primaryType !== "EIP712Domain")
10118
- parts.push(hashStruct({
10119
- data: message,
10120
- primaryType,
10121
- types
10122
- }));
10123
- return keccak256(concat(parts));
10124
10213
  }
10125
- function hashDomain({ domain, types }) {
10126
- return hashStruct({
10127
- data: domain,
10128
- primaryType: "EIP712Domain",
10129
- types
10130
- });
10214
+ function stripControlChars(s) {
10215
+ let out = "";
10216
+ for (const ch of s) {
10217
+ const code = ch.codePointAt(0) ?? 0;
10218
+ if (code <= 31 || code >= 127 && code <= 159) continue;
10219
+ out += ch;
10220
+ }
10221
+ return out;
10131
10222
  }
10132
- function hashStruct({ data, primaryType, types }) {
10133
- const encoded = encodeData({
10134
- data,
10135
- primaryType,
10136
- types
10137
- });
10138
- return keccak256(encoded);
10223
+ function asString(v) {
10224
+ return typeof v === "string" ? stripControlChars(v) : "";
10139
10225
  }
10140
- function encodeData({ data, primaryType, types }) {
10141
- const encodedTypes = [{ type: "bytes32" }];
10142
- const encodedValues = [hashType({ primaryType, types })];
10143
- for (const field of types[primaryType]) {
10144
- const [type, value] = encodeField({
10145
- types,
10146
- name: field.name,
10147
- type: field.type,
10148
- value: data[field.name]
10149
- });
10150
- encodedTypes.push(type);
10151
- encodedValues.push(value);
10152
- }
10153
- return encodeAbiParameters(encodedTypes, encodedValues);
10226
+ function parseToken(v) {
10227
+ if (!v || typeof v !== "object") return null;
10228
+ const o = v;
10229
+ const symbol = asString(o.symbol);
10230
+ const amountDecimal = asString(o.amountDecimal);
10231
+ if (!symbol && !amountDecimal) return null;
10232
+ const token = { symbol, amountDecimal };
10233
+ const amountUsd = asString(o.amountUsd);
10234
+ if (amountUsd) token.amountUsd = amountUsd;
10235
+ return token;
10154
10236
  }
10155
- function hashType({ primaryType, types }) {
10156
- const encodedHashType = toHex(encodeType({ primaryType, types }));
10157
- return keccak256(encodedHashType);
10237
+ function parseAccount(v) {
10238
+ if (!v || typeof v !== "object") return null;
10239
+ const o = v;
10240
+ const chainId = asString(o.chainId);
10241
+ if (!chainId) return null;
10242
+ const tokensRaw = Array.isArray(o.tokens) ? o.tokens : [];
10243
+ const tokens = tokensRaw.map(parseToken).filter((t) => t !== null);
10244
+ return { chainId, address: asString(o.address) || "\u2014", tokens };
10158
10245
  }
10159
- function encodeType({ primaryType, types }) {
10160
- let result = "";
10161
- const unsortedDeps = findTypeDependencies({ primaryType, types });
10162
- unsortedDeps.delete(primaryType);
10163
- const deps = [primaryType, ...Array.from(unsortedDeps).sort()];
10164
- for (const type of deps) {
10165
- result += `${type}(${types[type].map(({ name, type: t }) => `${t} ${name}`).join(",")})`;
10246
+ function parseBalanceSummaryEnvelope(value) {
10247
+ if (!value || typeof value !== "object") return null;
10248
+ const o = value;
10249
+ if (o.surface !== "balance_summary") return null;
10250
+ if (!Array.isArray(o.accounts)) return null;
10251
+ const accounts = o.accounts.map(parseAccount).filter((a) => a !== null);
10252
+ if (accounts.length === 0) return null;
10253
+ const card = { surface: "balance_summary", accounts };
10254
+ if (o.stale === true) {
10255
+ card.stale = true;
10256
+ if (typeof o.stale_secs === "number") card.staleSecs = o.stale_secs;
10166
10257
  }
10167
- return result;
10258
+ return card;
10168
10259
  }
10169
- function findTypeDependencies({ primaryType: primaryType_, types }, results = /* @__PURE__ */ new Set()) {
10170
- const match = primaryType_.match(/^\w*/u);
10171
- const primaryType = match?.[0];
10172
- if (results.has(primaryType) || types[primaryType] === void 0) {
10173
- return results;
10260
+ function matchBrace(text, start) {
10261
+ let depth = 0;
10262
+ let inString = false;
10263
+ let escaped = false;
10264
+ for (let i = start; i < text.length; i++) {
10265
+ const ch = text[i];
10266
+ if (inString) {
10267
+ if (escaped) escaped = false;
10268
+ else if (ch === "\\") escaped = true;
10269
+ else if (ch === '"') inString = false;
10270
+ continue;
10271
+ }
10272
+ if (ch === '"') inString = true;
10273
+ else if (ch === "{") depth++;
10274
+ else if (ch === "}") {
10275
+ depth--;
10276
+ if (depth === 0) return i;
10277
+ }
10174
10278
  }
10175
- results.add(primaryType);
10176
- for (const field of types[primaryType]) {
10177
- findTypeDependencies({ primaryType: field.type, types }, results);
10279
+ return -1;
10280
+ }
10281
+ function extractBalanceSummaryFromText(content) {
10282
+ if (!content || !content.includes("balance_summary")) return null;
10283
+ if (content.length > 2e5) return null;
10284
+ for (let i = content.indexOf("{"); i !== -1; i = content.indexOf("{", i + 1)) {
10285
+ const end = matchBrace(content, i);
10286
+ if (end === -1) break;
10287
+ const blob = content.slice(i, end + 1);
10288
+ if (!blob.includes("balance_summary")) continue;
10289
+ let parsed;
10290
+ try {
10291
+ parsed = JSON.parse(blob);
10292
+ } catch {
10293
+ continue;
10294
+ }
10295
+ const card = parseBalanceSummaryEnvelope(parsed);
10296
+ if (!card) continue;
10297
+ const before = content.slice(0, i).replace(/```(?:json)?\s*$/i, "");
10298
+ const after = content.slice(end + 1).replace(/^\s*```/, "");
10299
+ const remainingText = (before + after).trim();
10300
+ return { card, remainingText };
10178
10301
  }
10179
- return results;
10302
+ return null;
10180
10303
  }
10181
- function encodeField({ types, name, type, value }) {
10182
- if (types[type] !== void 0) {
10183
- return [
10184
- { type: "bytes32" },
10185
- keccak256(encodeData({ data: value, primaryType: type, types }))
10186
- ];
10304
+ function shortenAddress(address) {
10305
+ if (!address || address === "\u2014") return address || "\u2014";
10306
+ if (address.length <= 16) return address;
10307
+ return `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
10308
+ }
10309
+ function parseUsd(amountUsd) {
10310
+ if (!amountUsd) return null;
10311
+ const cleaned = amountUsd.replace(/[$,\s]/g, "");
10312
+ if (!cleaned) return null;
10313
+ const n = Number(cleaned);
10314
+ return Number.isFinite(n) ? n : null;
10315
+ }
10316
+ function formatUsd(n) {
10317
+ return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
10318
+ }
10319
+ function renderBalanceSummaryCard(card) {
10320
+ const lines = [];
10321
+ const staleCue = card.stale ? chalk8.gray(` (stale${card.staleSecs ? ` ~${Math.round(card.staleSecs / 60)}m` : ""}, refreshing\u2026)`) : "";
10322
+ lines.push(chalk8.bold(" Balances") + staleCue);
10323
+ let total = 0;
10324
+ let sawUsd = false;
10325
+ for (const account of card.accounts) {
10326
+ lines.push(` ${chalk8.cyan(account.chainId)} ${chalk8.gray(`(${shortenAddress(account.address)})`)}`);
10327
+ if (account.tokens.length === 0) {
10328
+ lines.push(chalk8.gray(" (no balances)"));
10329
+ continue;
10330
+ }
10331
+ for (const token of account.tokens) {
10332
+ const usd = parseUsd(token.amountUsd);
10333
+ if (usd !== null) {
10334
+ total += usd;
10335
+ sawUsd = true;
10336
+ }
10337
+ const symbol = token.symbol.padEnd(10);
10338
+ const amount = token.amountDecimal.padStart(16);
10339
+ const usdCol = token.amountUsd ? chalk8.gray(` ${token.amountUsd}`) : "";
10340
+ lines.push(` ${chalk8.bold(symbol)}${amount}${usdCol}`);
10341
+ }
10187
10342
  }
10188
- if (type === "bytes")
10189
- return [{ type: "bytes32" }, keccak256(value)];
10190
- if (type === "string")
10191
- return [{ type: "bytes32" }, keccak256(toHex(value))];
10192
- if (type.lastIndexOf("]") === type.length - 1) {
10193
- const parsedType = type.slice(0, type.lastIndexOf("["));
10194
- const typeValuePairs = value.map((item) => encodeField({
10195
- name,
10196
- type: parsedType,
10197
- types,
10198
- value: item
10199
- }));
10200
- return [
10201
- { type: "bytes32" },
10202
- keccak256(encodeAbiParameters(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v)))
10203
- ];
10343
+ if (sawUsd) {
10344
+ lines.push(chalk8.gray(" " + "\u2500".repeat(36)));
10345
+ lines.push(` ${chalk8.bold("Total")} ${chalk8.green(formatUsd(total))}`);
10204
10346
  }
10205
- return [{ type }, value];
10347
+ return lines.join("\n");
10206
10348
  }
10207
10349
 
10208
- // ../../node_modules/viem/_esm/index.js
10209
- init_formatUnits();
10350
+ // src/agent/client.ts
10351
+ import { randomUUID } from "node:crypto";
10352
+
10353
+ // src/agent/toolOutputSigning.ts
10354
+ import { getChainKind as getChainKind4 } from "@vultisig/sdk";
10355
+
10356
+ // src/agent/executor.ts
10357
+ import {
10358
+ Chain as Chain11,
10359
+ chainFeeCoin,
10360
+ getChainKind as getChainKind3,
10361
+ resolveChainReference,
10362
+ VaultError as VaultError3,
10363
+ VaultErrorCode as VaultErrorCode3,
10364
+ Vultisig as VultisigSdk
10365
+ } from "@vultisig/sdk";
10210
10366
 
10211
10367
  // src/core/VaultStateStore.ts
10212
10368
  import * as fs3 from "node:fs";
@@ -11992,6 +12148,7 @@ var CLI_SIGNABLE_PREP_TOOLS = /* @__PURE__ */ new Set([
11992
12148
  "execute_send",
11993
12149
  "execute_contract_call"
11994
12150
  ]);
12151
+ var CLI_SIGNABLE_YIELD_TOOLS = /* @__PURE__ */ new Set(["yield_enter", "yield_exit"]);
11995
12152
  function asRecord(value) {
11996
12153
  if (typeof value === "string") {
11997
12154
  try {
@@ -12092,11 +12249,50 @@ function buildTxReadyFromToolOutput(toolName, output) {
12092
12249
  }
12093
12250
  return { __buildTx: true, chain: chainStr, chain_id: chainIdStr, ...action ? { action } : {}, tx: main };
12094
12251
  }
12252
+ function buildTxReadyFromYieldOutput(toolName, output) {
12253
+ if (!CLI_SIGNABLE_YIELD_TOOLS.has(toolName)) return null;
12254
+ const env = asRecord(output);
12255
+ if (!env) return null;
12256
+ if (env.status === "error" || "error" in env) return null;
12257
+ const chain = asChainString(env.chain);
12258
+ if (!chain) return null;
12259
+ const resolved = resolveChain(chain);
12260
+ if (!resolved || getChainKind4(resolved) !== "evm") return null;
12261
+ const chainStr = chain;
12262
+ const rawTxs = env.transactions;
12263
+ if (!Array.isArray(rawTxs) || rawTxs.length === 0) return null;
12264
+ const legs = [];
12265
+ for (const raw of rawTxs) {
12266
+ const legObj = asRecord(raw);
12267
+ if (!legObj) return null;
12268
+ if (legObj.status === "error" || "error" in legObj) return null;
12269
+ const leg = extractLeg(legObj);
12270
+ if (!leg) return null;
12271
+ legs.push(leg);
12272
+ }
12273
+ if (legs.length > 2) return null;
12274
+ const action = typeof env.action === "string" && env.action !== "" ? env.action : void 0;
12275
+ if (legs.length === 2) {
12276
+ const [approveLeg, mainLeg] = legs;
12277
+ return {
12278
+ __buildTx: true,
12279
+ chain: chainStr,
12280
+ ...action ? { action } : {},
12281
+ approvalTxArgs: { chain: chainStr, tx: approveLeg },
12282
+ txArgs: { chain: chainStr, tx: mainLeg }
12283
+ };
12284
+ }
12285
+ return { __buildTx: true, chain: chainStr, ...action ? { action } : {}, tx: legs[0] };
12286
+ }
12095
12287
  function deriveToolOutputCandidate(toolName, output) {
12096
12288
  if (CLI_SIGNABLE_FLAT_TOOLS.has(toolName)) {
12097
12289
  const payload = buildTxReadyFromToolOutput(toolName, output);
12098
12290
  return payload ? { payload, source: "flat", toolName } : null;
12099
12291
  }
12292
+ if (CLI_SIGNABLE_YIELD_TOOLS.has(toolName)) {
12293
+ const payload = buildTxReadyFromYieldOutput(toolName, output);
12294
+ return payload ? { payload, source: "yield", toolName } : null;
12295
+ }
12100
12296
  if (CLI_SIGNABLE_PREP_TOOLS.has(toolName)) {
12101
12297
  const env = asRecord(output);
12102
12298
  if (!env || env.status === "error" || "error" in env) return null;
@@ -12713,7 +12909,8 @@ var AgentClient = class {
12713
12909
  maybeSignToolOutput(status, toolName, output, callbacks, v1Type) {
12714
12910
  if (v1Type === "tool-output-error") return;
12715
12911
  if (status !== "done" || !toolName || !callbacks.onToolOutputTx) return;
12716
- if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !CLI_SIGNABLE_PREP_TOOLS.has(toolName)) return;
12912
+ if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !CLI_SIGNABLE_PREP_TOOLS.has(toolName) && !CLI_SIGNABLE_YIELD_TOOLS.has(toolName))
12913
+ return;
12717
12914
  const candidate = deriveToolOutputCandidate(toolName, output);
12718
12915
  if (!candidate) return;
12719
12916
  if (this.verbose)
@@ -15082,7 +15279,7 @@ var cachedVersion = null;
15082
15279
  function getVersion() {
15083
15280
  if (cachedVersion) return cachedVersion;
15084
15281
  if (true) {
15085
- cachedVersion = "2.21.1";
15282
+ cachedVersion = "2.23.0";
15086
15283
  return cachedVersion;
15087
15284
  }
15088
15285
  try {
@@ -16479,7 +16676,7 @@ Error: ${error2.message}`));
16479
16676
  const [fromChainStr, toChainStr, amountStr, ...rest] = args;
16480
16677
  const fromChain = findChainByName(fromChainStr) || fromChainStr;
16481
16678
  const toChain = findChainByName(toChainStr) || toChainStr;
16482
- const amount = parseFloat(amountStr);
16679
+ const amount = amountStr;
16483
16680
  let fromToken;
16484
16681
  let toToken;
16485
16682
  for (let i = 0; i < rest.length; i++) {
@@ -16505,7 +16702,7 @@ Error: ${error2.message}`));
16505
16702
  const [fromChainStr, toChainStr, amountStr, ...rest] = args;
16506
16703
  const fromChain = findChainByName(fromChainStr) || fromChainStr;
16507
16704
  const toChain = findChainByName(toChainStr) || toChainStr;
16508
- const amount = parseFloat(amountStr);
16705
+ const amount = amountStr;
16509
16706
  let fromToken;
16510
16707
  let toToken;
16511
16708
  let slippage;
@@ -17508,7 +17705,7 @@ Examples:
17508
17705
  await executeSwapQuote(context, {
17509
17706
  fromChain,
17510
17707
  toChain,
17511
- amount: options.max ? "max" : parseFloat(amountStr),
17708
+ amount: options.max ? "max" : amountStr,
17512
17709
  fromToken: options.fromToken,
17513
17710
  toToken: options.toToken
17514
17711
  });
@@ -17533,7 +17730,7 @@ See also: swap-quote, swap-chains, balance`
17533
17730
  await executeSwap(context, {
17534
17731
  fromChain: findChainByName(fromChainStr) || fromChainStr,
17535
17732
  toChain: findChainByName(toChainStr) || toChainStr,
17536
- amount: options.max ? "max" : parseFloat(amountStr),
17733
+ amount: options.max ? "max" : amountStr,
17537
17734
  fromToken: options.fromToken,
17538
17735
  toToken: options.toToken,
17539
17736
  slippage: options.slippage ? parseFloat(options.slippage) : void 0,