@vultisig/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/index.js +895 -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
|
}
|
|
@@ -5959,23 +6587,23 @@ function parseInlineToolCalls(text) {
|
|
|
5959
6587
|
return actions;
|
|
5960
6588
|
}
|
|
5961
6589
|
function getTokenCachePath() {
|
|
5962
|
-
const dir = process.env.VULTISIG_CONFIG_DIR ??
|
|
5963
|
-
return
|
|
6590
|
+
const dir = process.env.VULTISIG_CONFIG_DIR ?? join2(homedir2(), ".vultisig");
|
|
6591
|
+
return join2(dir, "agent-tokens.json");
|
|
5964
6592
|
}
|
|
5965
6593
|
function readTokenStore() {
|
|
5966
6594
|
try {
|
|
5967
|
-
const
|
|
5968
|
-
if (!existsSync(
|
|
5969
|
-
return JSON.parse(
|
|
6595
|
+
const path4 = getTokenCachePath();
|
|
6596
|
+
if (!existsSync(path4)) return {};
|
|
6597
|
+
return JSON.parse(readFileSync2(path4, "utf-8"));
|
|
5970
6598
|
} catch {
|
|
5971
6599
|
return {};
|
|
5972
6600
|
}
|
|
5973
6601
|
}
|
|
5974
6602
|
function writeTokenStore(store) {
|
|
5975
|
-
const
|
|
5976
|
-
const dir =
|
|
5977
|
-
if (!existsSync(dir))
|
|
5978
|
-
|
|
6603
|
+
const path4 = getTokenCachePath();
|
|
6604
|
+
const dir = join2(path4, "..");
|
|
6605
|
+
if (!existsSync(dir)) mkdirSync2(dir, { recursive: true });
|
|
6606
|
+
writeFileSync2(path4, JSON.stringify(store, null, 2), { mode: 384 });
|
|
5979
6607
|
}
|
|
5980
6608
|
function loadCachedToken(publicKey) {
|
|
5981
6609
|
const store = readTokenStore();
|
|
@@ -6353,6 +6981,68 @@ async function executeAgent(ctx2, options) {
|
|
|
6353
6981
|
}
|
|
6354
6982
|
}
|
|
6355
6983
|
}
|
|
6984
|
+
async function executeAgentAsk(ctx2, message, options) {
|
|
6985
|
+
setSilentMode(true);
|
|
6986
|
+
const originalConsoleLog = console.log;
|
|
6987
|
+
console.log = (...args) => {
|
|
6988
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
6989
|
+
};
|
|
6990
|
+
try {
|
|
6991
|
+
const vault = await ctx2.ensureActiveVault();
|
|
6992
|
+
const config = {
|
|
6993
|
+
backendUrl: options.backendUrl || process.env.VULTISIG_AGENT_URL || "http://localhost:9998",
|
|
6994
|
+
vaultName: vault.name,
|
|
6995
|
+
password: options.password,
|
|
6996
|
+
sessionId: options.session,
|
|
6997
|
+
verbose: options.verbose,
|
|
6998
|
+
askMode: true
|
|
6999
|
+
};
|
|
7000
|
+
const session = new AgentSession(vault, config);
|
|
7001
|
+
const ask = new AskInterface(session, !!config.verbose);
|
|
7002
|
+
const callbacks = ask.getCallbacks();
|
|
7003
|
+
await session.initialize(callbacks);
|
|
7004
|
+
const result = await ask.ask(message);
|
|
7005
|
+
if (options.json) {
|
|
7006
|
+
process.stdout.write(
|
|
7007
|
+
JSON.stringify({
|
|
7008
|
+
session_id: result.sessionId,
|
|
7009
|
+
response: result.response,
|
|
7010
|
+
tool_calls: result.toolCalls,
|
|
7011
|
+
transactions: result.transactions
|
|
7012
|
+
}) + "\n"
|
|
7013
|
+
);
|
|
7014
|
+
} else {
|
|
7015
|
+
process.stdout.write(`session:${result.sessionId}
|
|
7016
|
+
`);
|
|
7017
|
+
if (result.response) {
|
|
7018
|
+
process.stdout.write(`
|
|
7019
|
+
${result.response}
|
|
7020
|
+
`);
|
|
7021
|
+
}
|
|
7022
|
+
for (const tx of result.transactions) {
|
|
7023
|
+
process.stdout.write(`
|
|
7024
|
+
tx:${tx.chain}:${tx.hash}
|
|
7025
|
+
`);
|
|
7026
|
+
if (tx.explorerUrl) {
|
|
7027
|
+
process.stdout.write(`explorer:${tx.explorerUrl}
|
|
7028
|
+
`);
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
} catch (err) {
|
|
7033
|
+
if (options.json) {
|
|
7034
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + "\n");
|
|
7035
|
+
} else {
|
|
7036
|
+
process.stderr.write(`Error: ${err.message}
|
|
7037
|
+
`);
|
|
7038
|
+
}
|
|
7039
|
+
process.exit(1);
|
|
7040
|
+
} finally {
|
|
7041
|
+
console.log = originalConsoleLog;
|
|
7042
|
+
setSilentMode(false);
|
|
7043
|
+
}
|
|
7044
|
+
process.exit(0);
|
|
7045
|
+
}
|
|
6356
7046
|
async function executeAgentSessionsList(ctx2, options) {
|
|
6357
7047
|
const vault = await ctx2.ensureActiveVault();
|
|
6358
7048
|
const backendUrl = options.backendUrl || process.env.VULTISIG_AGENT_URL || "http://localhost:9998";
|
|
@@ -6429,8 +7119,8 @@ function formatDate(iso) {
|
|
|
6429
7119
|
|
|
6430
7120
|
// src/interactive/completer.ts
|
|
6431
7121
|
import { Chain as Chain10 } from "@vultisig/sdk";
|
|
6432
|
-
import
|
|
6433
|
-
import
|
|
7122
|
+
import fs3 from "fs";
|
|
7123
|
+
import path3 from "path";
|
|
6434
7124
|
var COMMANDS = [
|
|
6435
7125
|
// Vault management
|
|
6436
7126
|
"vaults",
|
|
@@ -6525,28 +7215,28 @@ function createCompleter(ctx2) {
|
|
|
6525
7215
|
}
|
|
6526
7216
|
function completeFilePath(partial, filterVult) {
|
|
6527
7217
|
try {
|
|
6528
|
-
const endsWithSeparator = partial.endsWith("/") || partial.endsWith(
|
|
7218
|
+
const endsWithSeparator = partial.endsWith("/") || partial.endsWith(path3.sep);
|
|
6529
7219
|
let dir;
|
|
6530
7220
|
let basename;
|
|
6531
7221
|
if (endsWithSeparator) {
|
|
6532
7222
|
dir = partial;
|
|
6533
7223
|
basename = "";
|
|
6534
7224
|
} else {
|
|
6535
|
-
dir =
|
|
6536
|
-
basename =
|
|
6537
|
-
if (
|
|
7225
|
+
dir = path3.dirname(partial);
|
|
7226
|
+
basename = path3.basename(partial);
|
|
7227
|
+
if (fs3.existsSync(partial) && fs3.statSync(partial).isDirectory()) {
|
|
6538
7228
|
dir = partial;
|
|
6539
7229
|
basename = "";
|
|
6540
7230
|
}
|
|
6541
7231
|
}
|
|
6542
|
-
const resolvedDir =
|
|
6543
|
-
if (!
|
|
7232
|
+
const resolvedDir = path3.resolve(dir);
|
|
7233
|
+
if (!fs3.existsSync(resolvedDir) || !fs3.statSync(resolvedDir).isDirectory()) {
|
|
6544
7234
|
return [[], partial];
|
|
6545
7235
|
}
|
|
6546
|
-
const files =
|
|
7236
|
+
const files = fs3.readdirSync(resolvedDir);
|
|
6547
7237
|
const matches = files.filter((file) => file.startsWith(basename)).map((file) => {
|
|
6548
|
-
const fullPath =
|
|
6549
|
-
const stats =
|
|
7238
|
+
const fullPath = path3.join(dir, file);
|
|
7239
|
+
const stats = fs3.statSync(path3.join(resolvedDir, file));
|
|
6550
7240
|
if (stats.isDirectory()) {
|
|
6551
7241
|
return fullPath + "/";
|
|
6552
7242
|
}
|
|
@@ -7825,19 +8515,19 @@ import chalk13 from "chalk";
|
|
|
7825
8515
|
|
|
7826
8516
|
// src/lib/version.ts
|
|
7827
8517
|
import chalk14 from "chalk";
|
|
7828
|
-
import { readFileSync as
|
|
7829
|
-
import { homedir as
|
|
7830
|
-
import { join as
|
|
8518
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
|
|
8519
|
+
import { homedir as homedir3 } from "os";
|
|
8520
|
+
import { join as join3 } from "path";
|
|
7831
8521
|
var cachedVersion = null;
|
|
7832
8522
|
function getVersion() {
|
|
7833
8523
|
if (cachedVersion) return cachedVersion;
|
|
7834
8524
|
if (true) {
|
|
7835
|
-
cachedVersion = "0.
|
|
8525
|
+
cachedVersion = "0.9.0";
|
|
7836
8526
|
return cachedVersion;
|
|
7837
8527
|
}
|
|
7838
8528
|
try {
|
|
7839
8529
|
const packagePath = new URL("../../package.json", import.meta.url);
|
|
7840
|
-
const pkg = JSON.parse(
|
|
8530
|
+
const pkg = JSON.parse(readFileSync3(packagePath, "utf-8"));
|
|
7841
8531
|
cachedVersion = pkg.version;
|
|
7842
8532
|
return cachedVersion;
|
|
7843
8533
|
} catch {
|
|
@@ -7845,13 +8535,13 @@ function getVersion() {
|
|
|
7845
8535
|
return cachedVersion;
|
|
7846
8536
|
}
|
|
7847
8537
|
}
|
|
7848
|
-
var CACHE_DIR =
|
|
7849
|
-
var VERSION_CACHE_FILE =
|
|
8538
|
+
var CACHE_DIR = join3(homedir3(), ".vultisig", "cache");
|
|
8539
|
+
var VERSION_CACHE_FILE = join3(CACHE_DIR, "version-check.json");
|
|
7850
8540
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7851
8541
|
function readVersionCache() {
|
|
7852
8542
|
try {
|
|
7853
8543
|
if (!existsSync2(VERSION_CACHE_FILE)) return null;
|
|
7854
|
-
const data =
|
|
8544
|
+
const data = readFileSync3(VERSION_CACHE_FILE, "utf-8");
|
|
7855
8545
|
return JSON.parse(data);
|
|
7856
8546
|
} catch {
|
|
7857
8547
|
return null;
|
|
@@ -7860,9 +8550,9 @@ function readVersionCache() {
|
|
|
7860
8550
|
function writeVersionCache(cache) {
|
|
7861
8551
|
try {
|
|
7862
8552
|
if (!existsSync2(CACHE_DIR)) {
|
|
7863
|
-
|
|
8553
|
+
mkdirSync3(CACHE_DIR, { recursive: true });
|
|
7864
8554
|
}
|
|
7865
|
-
|
|
8555
|
+
writeFileSync3(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
7866
8556
|
} catch {
|
|
7867
8557
|
}
|
|
7868
8558
|
}
|
|
@@ -7967,9 +8657,9 @@ function getUpdateCommand() {
|
|
|
7967
8657
|
}
|
|
7968
8658
|
|
|
7969
8659
|
// src/lib/completion.ts
|
|
7970
|
-
import { homedir as
|
|
7971
|
-
import { join as
|
|
7972
|
-
import { readFileSync as
|
|
8660
|
+
import { homedir as homedir4 } from "os";
|
|
8661
|
+
import { join as join4 } from "path";
|
|
8662
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
7973
8663
|
var tabtab = null;
|
|
7974
8664
|
async function getTabtab() {
|
|
7975
8665
|
if (!tabtab) {
|
|
@@ -8034,7 +8724,7 @@ var CHAINS = [
|
|
|
8034
8724
|
];
|
|
8035
8725
|
function getVaultNames() {
|
|
8036
8726
|
try {
|
|
8037
|
-
const vaultDir =
|
|
8727
|
+
const vaultDir = join4(homedir4(), ".vultisig", "vaults");
|
|
8038
8728
|
if (!existsSync3(vaultDir)) return [];
|
|
8039
8729
|
const { readdirSync } = __require("fs");
|
|
8040
8730
|
const files = readdirSync(vaultDir);
|
|
@@ -8042,7 +8732,7 @@ function getVaultNames() {
|
|
|
8042
8732
|
for (const file of files) {
|
|
8043
8733
|
if (file.startsWith("vault:") && file.endsWith(".json")) {
|
|
8044
8734
|
try {
|
|
8045
|
-
const content =
|
|
8735
|
+
const content = readFileSync4(join4(vaultDir, file), "utf-8");
|
|
8046
8736
|
const vault = JSON.parse(content);
|
|
8047
8737
|
if (vault.name) names.push(vault.name);
|
|
8048
8738
|
if (vault.id) names.push(vault.id);
|
|
@@ -8365,10 +9055,10 @@ createCmd.command("secure").description("Create a secure vault (multi-device MPC
|
|
|
8365
9055
|
});
|
|
8366
9056
|
})
|
|
8367
9057
|
);
|
|
8368
|
-
program.command("import <file>").description("Import vault from .vult file").action(
|
|
8369
|
-
withExit(async (file) => {
|
|
9058
|
+
program.command("import <file>").description("Import vault from .vult file").option("--password <password>", "Password to decrypt the vault file").action(
|
|
9059
|
+
withExit(async (file, options) => {
|
|
8370
9060
|
const context = await init(program.opts().vault);
|
|
8371
|
-
await executeImport(context, file);
|
|
9061
|
+
await executeImport(context, file, options.password);
|
|
8372
9062
|
})
|
|
8373
9063
|
);
|
|
8374
9064
|
var createFromSeedphraseCmd = program.command("create-from-seedphrase").description("Create vault from BIP39 seedphrase");
|
|
@@ -8485,7 +9175,7 @@ joinCmd.command("secure").description("Join a SecureVault creation session").opt
|
|
|
8485
9175
|
const context = await init(program.opts().vault);
|
|
8486
9176
|
let qrPayload = options.qr;
|
|
8487
9177
|
if (!qrPayload && options.qrFile) {
|
|
8488
|
-
qrPayload = (await
|
|
9178
|
+
qrPayload = (await fs4.readFile(options.qrFile, "utf-8")).trim();
|
|
8489
9179
|
}
|
|
8490
9180
|
if (!qrPayload) {
|
|
8491
9181
|
qrPayload = await promptQrPayload();
|
|
@@ -8635,10 +9325,10 @@ program.command("discount").description("Show your VULT discount tier for swap f
|
|
|
8635
9325
|
})
|
|
8636
9326
|
);
|
|
8637
9327
|
program.command("export [path]").description("Export vault to file").option("--password <password>", "Password to unlock the vault (for encrypted vaults)").option("--exportPassword <password>", "Password to encrypt the exported file (defaults to --password)").action(
|
|
8638
|
-
withExit(async (
|
|
9328
|
+
withExit(async (path4, options) => {
|
|
8639
9329
|
const context = await init(program.opts().vault, options.password);
|
|
8640
9330
|
await executeExport(context, {
|
|
8641
|
-
outputPath:
|
|
9331
|
+
outputPath: path4,
|
|
8642
9332
|
password: options.password,
|
|
8643
9333
|
exportPassword: options.exportPassword
|
|
8644
9334
|
});
|
|
@@ -8856,6 +9546,21 @@ var agentCmd = program.command("agent").description("AI-powered chat interface f
|
|
|
8856
9546
|
sessionId: options.sessionId
|
|
8857
9547
|
});
|
|
8858
9548
|
});
|
|
9549
|
+
agentCmd.command("ask <message>").description("Send a single message and get the response (for AI agent integration)").option("--session <id>", "Continue an existing conversation").option("--backend-url <url>", "Agent backend URL (default: http://localhost:9998)").option("--password <password>", "Vault password for signing operations").option("--verbose", "Show tool calls and debug info on stderr").option("--json", "Output structured JSON instead of text").action(
|
|
9550
|
+
async (message, options) => {
|
|
9551
|
+
const parentOpts = agentCmd.opts();
|
|
9552
|
+
const context = await init(
|
|
9553
|
+
program.opts().vault,
|
|
9554
|
+
options.password || parentOpts.password
|
|
9555
|
+
);
|
|
9556
|
+
await executeAgentAsk(context, message, {
|
|
9557
|
+
...options,
|
|
9558
|
+
backendUrl: options.backendUrl || parentOpts.backendUrl,
|
|
9559
|
+
password: options.password || parentOpts.password,
|
|
9560
|
+
verbose: options.verbose || parentOpts.verbose
|
|
9561
|
+
});
|
|
9562
|
+
}
|
|
9563
|
+
);
|
|
8859
9564
|
var sessionsCmd = agentCmd.command("sessions").description("Manage agent chat sessions");
|
|
8860
9565
|
sessionsCmd.command("list").description("List chat sessions for the current vault").option("--backend-url <url>", "Agent backend URL (default: http://localhost:9998)").option("--password <password>", "Vault password for authentication").action(
|
|
8861
9566
|
withExit(async (options) => {
|