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