@candledottv/cli 0.11.15 → 0.11.16
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/README.md +1 -1
- package/dist/index.js +2098 -1907
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -444,6 +444,9 @@ function writeUsageFailure(deps, message, json) {
|
|
|
444
444
|
deps.stderr.write(`${message}
|
|
445
445
|
`);
|
|
446
446
|
}
|
|
447
|
+
function portalDevicesUrl(apiUrl, portalOrigin) {
|
|
448
|
+
return `${portalDeviceUrl(apiUrl, portalOrigin)}?tab=keys#connected-devices`;
|
|
449
|
+
}
|
|
447
450
|
function portalDeviceUrl(apiUrl, portalOrigin) {
|
|
448
451
|
if (portalOrigin) {
|
|
449
452
|
try {
|
|
@@ -653,7 +656,14 @@ function profileTable(config, now) {
|
|
|
653
656
|
}
|
|
654
657
|
var PRE_PROFILE_FIELDS, API_KEY_PREFIXES, API_KEY_RANDOM_LENGTH = 43, INVALID_RPC_HOST = "invalid (fix with profile set)";
|
|
655
658
|
var init_profiles = __esm(() => {
|
|
656
|
-
PRE_PROFILE_FIELDS = [
|
|
659
|
+
PRE_PROFILE_FIELDS = [
|
|
660
|
+
"apiUrl",
|
|
661
|
+
"keyPrefix",
|
|
662
|
+
"deviceTokenPrefix",
|
|
663
|
+
"scopes",
|
|
664
|
+
"label",
|
|
665
|
+
"portalOrigin"
|
|
666
|
+
];
|
|
657
667
|
API_KEY_PREFIXES = ["cndl_live_", "cndl_test_"];
|
|
658
668
|
});
|
|
659
669
|
|
|
@@ -821,7 +831,7 @@ class EncryptedFileSecretStore {
|
|
|
821
831
|
}
|
|
822
832
|
async function promptHiddenSecret(promptText) {
|
|
823
833
|
if (!process.stdin.isTTY) {
|
|
824
|
-
throw new Error("No TTY available for
|
|
834
|
+
throw new Error("No TTY available for hidden input; run this in a terminal.");
|
|
825
835
|
}
|
|
826
836
|
return readHiddenLine(promptText, realPromptStreams());
|
|
827
837
|
}
|
|
@@ -26900,10 +26910,18 @@ async function assertNotOlderCopy(ctx, path, raw, accept) {
|
|
|
26900
26910
|
}
|
|
26901
26911
|
async function confirmLastSix(ctx, address, what) {
|
|
26902
26912
|
const expected = address.slice(-6);
|
|
26903
|
-
const
|
|
26904
|
-
|
|
26905
|
-
|
|
26913
|
+
const normalize3 = (value) => address.startsWith("0x") ? value.toLowerCase() : value;
|
|
26914
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
26915
|
+
const typed = (await ctx.deps.promptLine(`Type the last six characters of ${what} (${address}) to confirm: `)).trim();
|
|
26916
|
+
if (normalize3(typed) === normalize3(expected))
|
|
26917
|
+
return;
|
|
26918
|
+
const difference = Array.from({ length: Math.max(expected.length, typed.length) }, (_, i) => i).find((i) => normalize3(typed)[i] !== normalize3(expected)[i]) ?? 0;
|
|
26919
|
+
ctx.deps.stderr.write(`Expected ${expected}
|
|
26920
|
+
You typed ${typed}
|
|
26921
|
+
${" ".repeat(10 + difference)}^ character ${difference + 1} differs
|
|
26922
|
+
`);
|
|
26906
26923
|
}
|
|
26924
|
+
throw new VaultError("DESTINATION_NOT_CONFIRMED", "That is not the last six characters of that address; nothing was done.", { suggestion: "Nothing was signed. Run it again and type the last six characters exactly as shown." });
|
|
26907
26925
|
}
|
|
26908
26926
|
function writeVaultFailure(ctx, error) {
|
|
26909
26927
|
if (isUsageError(error))
|
|
@@ -30286,399 +30304,1233 @@ var init_wallet_keygen = __esm(() => {
|
|
|
30286
30304
|
init_esm();
|
|
30287
30305
|
});
|
|
30288
30306
|
|
|
30289
|
-
// src/
|
|
30290
|
-
|
|
30291
|
-
|
|
30307
|
+
// src/commands/swap.ts
|
|
30308
|
+
import { randomUUID } from "node:crypto";
|
|
30309
|
+
function tradingFailure(ctx, error, id) {
|
|
30310
|
+
if (error instanceof TradingUsage) {
|
|
30311
|
+
writeUsageFailure(ctx.deps, error.message, ctx.json);
|
|
30312
|
+
return 2;
|
|
30313
|
+
}
|
|
30314
|
+
writeLocalFailure(ctx.deps, {
|
|
30315
|
+
code: error instanceof TradingError ? error.code : "TRADING_FAILED",
|
|
30316
|
+
message: `${error instanceof Error ? describeRpcFailure(error) : "Trading failed."}${id ? ` Operation ${id}; use candle swap status ${id} before another attempt.` : ""}`,
|
|
30317
|
+
...error instanceof TradingError && error.suggestion ? { suggestion: error.suggestion } : {},
|
|
30318
|
+
...error instanceof TradingError && error.details ? { details: error.details } : {}
|
|
30319
|
+
}, ctx.json);
|
|
30320
|
+
return error instanceof TradingError ? error.exitCode : 1;
|
|
30292
30321
|
}
|
|
30293
|
-
function
|
|
30294
|
-
|
|
30295
|
-
|
|
30296
|
-
|
|
30297
|
-
|
|
30298
|
-
|
|
30299
|
-
|
|
30300
|
-
|
|
30301
|
-
|
|
30302
|
-
|
|
30303
|
-
|
|
30304
|
-
if (
|
|
30305
|
-
|
|
30306
|
-
|
|
30307
|
-
out.push(toChecksumAddress(token));
|
|
30322
|
+
function lazySolanaClient(ctx, flag) {
|
|
30323
|
+
let pending;
|
|
30324
|
+
return () => {
|
|
30325
|
+
pending ??= tradingSolanaClient(ctx, flag);
|
|
30326
|
+
return pending;
|
|
30327
|
+
};
|
|
30328
|
+
}
|
|
30329
|
+
async function tradingRead(ctx, client, read) {
|
|
30330
|
+
try {
|
|
30331
|
+
return await read();
|
|
30332
|
+
} catch (error) {
|
|
30333
|
+
if (isRateLimited(error))
|
|
30334
|
+
throw tradingRateLimited(ctx, client.endpoint.host, error);
|
|
30335
|
+
throw error;
|
|
30308
30336
|
}
|
|
30309
|
-
return out;
|
|
30310
30337
|
}
|
|
30311
|
-
function
|
|
30312
|
-
|
|
30338
|
+
function printTradingResult(ctx, result) {
|
|
30339
|
+
ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
|
|
30340
|
+
` : `${JSON.stringify(result, null, 2)}
|
|
30341
|
+
`);
|
|
30342
|
+
return 0;
|
|
30313
30343
|
}
|
|
30314
|
-
function
|
|
30315
|
-
const
|
|
30316
|
-
|
|
30317
|
-
|
|
30318
|
-
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30322
|
-
|
|
30323
|
-
|
|
30324
|
-
}
|
|
30344
|
+
function reportSettlement(ctx, settlement, out, id) {
|
|
30345
|
+
const parsed = settlementSchema.safeParse(settlement);
|
|
30346
|
+
const reading = parsed.success ? parsed.data : undefined;
|
|
30347
|
+
if (reading?.state === "settled" && reading.settledOutRaw !== undefined)
|
|
30348
|
+
ctx.deps.stderr.write(`settled ${decimalAmount(reading.settledOutRaw, out.decimals)} ${out.asset}
|
|
30349
|
+
`);
|
|
30350
|
+
else {
|
|
30351
|
+
const state = !reading ? "not measured" : reading.state === "settled" ? "settled, amount not measured" : reading.state;
|
|
30352
|
+
ctx.deps.stderr.write(`Settlement: ${state}. Re-check with GET /api/v1/agent/swap/jobs/${id} (candle swap status ${id}).
|
|
30353
|
+
`);
|
|
30354
|
+
}
|
|
30355
|
+
return settlement !== null && typeof settlement === "object" ? { settlement } : {};
|
|
30325
30356
|
}
|
|
30326
|
-
function
|
|
30327
|
-
|
|
30328
|
-
if (recorded === undefined && fromBlockFlag === undefined)
|
|
30329
|
-
return;
|
|
30330
|
-
if (recorded === undefined)
|
|
30331
|
-
return { block: fromBlockFlag, source: "--from-block" };
|
|
30332
|
-
if (fromBlockFlag === undefined)
|
|
30333
|
-
return { block: recorded, source: "record" };
|
|
30334
|
-
return { block: recorded < fromBlockFlag ? recorded : fromBlockFlag, source: "record and --from-block" };
|
|
30357
|
+
function validClientId(id) {
|
|
30358
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id);
|
|
30335
30359
|
}
|
|
30336
|
-
async function
|
|
30337
|
-
const
|
|
30338
|
-
|
|
30339
|
-
|
|
30340
|
-
|
|
30341
|
-
const topics = [ERC20_TRANSFER_TOPIC, null, addressTopic(wallet)];
|
|
30342
|
-
while (from <= toBlock) {
|
|
30343
|
-
const to = from + page - 1n > toBlock ? toBlock : from + page - 1n;
|
|
30360
|
+
async function lookupOperation(ctx, key, id, kind) {
|
|
30361
|
+
const local = await savedOperation(ctx, key, id);
|
|
30362
|
+
const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
|
|
30363
|
+
const found = [];
|
|
30364
|
+
for (const candidate of kinds) {
|
|
30344
30365
|
try {
|
|
30345
|
-
|
|
30346
|
-
|
|
30347
|
-
|
|
30348
|
-
|
|
30349
|
-
|
|
30366
|
+
found.push({
|
|
30367
|
+
...operationSchema.parse(await request(ctx, key, jobPath(candidate, id))),
|
|
30368
|
+
kind: candidate,
|
|
30369
|
+
clientTradeId: id
|
|
30370
|
+
});
|
|
30350
30371
|
} catch (error) {
|
|
30351
|
-
if (error instanceof
|
|
30352
|
-
|
|
30353
|
-
continue;
|
|
30354
|
-
}
|
|
30355
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
30356
|
-
return { failed: `eth_getLogs from block ${from} failed: ${reason}`, tokens: [...seen.values()], pages };
|
|
30372
|
+
if (!(error instanceof TradingError && error.code === "JOB_NOT_FOUND"))
|
|
30373
|
+
throw error;
|
|
30357
30374
|
}
|
|
30358
30375
|
}
|
|
30359
|
-
|
|
30376
|
+
if (found.length > 1)
|
|
30377
|
+
throw new TradingError("AMBIGUOUS_OPERATION", "This id exists on multiple rails; specify --kind trade, swap or launch.");
|
|
30378
|
+
return found[0] ? { ...found[0], ...local?.signature ? { signature: local.signature } : {} } : null;
|
|
30360
30379
|
}
|
|
30361
|
-
function
|
|
30362
|
-
const
|
|
30363
|
-
|
|
30364
|
-
|
|
30365
|
-
|
|
30366
|
-
} else if (record.absent) {
|
|
30367
|
-
lines.push(` sealed EVM record: absent at ${record.path} (not on this machine, or nothing recorded yet)`);
|
|
30368
|
-
} else {
|
|
30369
|
-
const damage = [
|
|
30370
|
-
...record.unreadableLines > 0 ? [`${record.unreadableLines} unreadable line(s) skipped`] : [],
|
|
30371
|
-
...record.partialTail ? ["a partial last line skipped"] : []
|
|
30372
|
-
];
|
|
30373
|
-
lines.push(` sealed EVM record: ${record.tokens.length} token(s), ${record.scanStarts.length} scan start(s)${damage.length > 0 ? `; ${damage.join(", ")}` : ""}`);
|
|
30374
|
-
}
|
|
30375
|
-
const server = sources.server;
|
|
30376
|
-
if (!server.asked)
|
|
30377
|
-
lines.push(` server traded-token list: not asked (${server.reason ?? "--emergency calls no Candle API"})`);
|
|
30378
|
-
else if (!server.answered)
|
|
30379
|
-
lines.push(` server traded-token list: FAILED (${server.reason ?? "no answer"})`);
|
|
30380
|
-
else
|
|
30381
|
-
lines.push(` server traded-token list: ${server.tokens.length} token(s)${server.truncated ? " (truncated)" : ""}`);
|
|
30382
|
-
lines.push(` --token: ${sources.flags.length}`);
|
|
30383
|
-
const logs = sources.logs;
|
|
30384
|
-
if (logs.ran && !logs.failed) {
|
|
30385
|
-
lines.push(` Transfer logs over ${host}: blocks ${logs.fromBlock} to ${logs.toBlock} (start from ${logs.startSource}), ${logs.tokens.length} contract(s)`);
|
|
30386
|
-
} else if (logs.failed) {
|
|
30387
|
-
lines.push(` Transfer logs over ${host}: FAILED (${logs.reason})`);
|
|
30388
|
-
} else {
|
|
30389
|
-
lines.push(` Transfer logs: NOT scanned (${logs.reason})`);
|
|
30380
|
+
async function swapStatus(args, ctx) {
|
|
30381
|
+
const parsed = parseArgs(args, { valueFlags: ["--kind"], booleanFlags: ["--wait"] });
|
|
30382
|
+
if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "") || parsed.values["--kind"] !== undefined && !["trade", "swap", "launch"].includes(parsed.values["--kind"])) {
|
|
30383
|
+
writeUsageFailure(ctx.deps, "Usage: candle swap status <id> [--kind trade|swap|launch] [--wait]", ctx.json);
|
|
30384
|
+
return 2;
|
|
30390
30385
|
}
|
|
30391
|
-
|
|
30392
|
-
|
|
30386
|
+
try {
|
|
30387
|
+
const key = await tradingKey(ctx);
|
|
30388
|
+
const id = parsed.positionals[0];
|
|
30389
|
+
const kind = parsed.values["--kind"];
|
|
30390
|
+
const facts = bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge);
|
|
30391
|
+
const found = await lookupOperation(ctx, key, id, kind);
|
|
30392
|
+
if (!found)
|
|
30393
|
+
throw new TradingError("JOB_NOT_FOUND", `No operation ${id} is visible to key ${apiKeyPrefix(key) ?? "this key"} on the ${kind ?? "trade, swap, launch"} rail(s). Nothing was resent.`, {
|
|
30394
|
+
suggestion: `Trade and launch jobs are readable by any key on this account; swap jobs only by the key that placed them. If another key or profile placed it, run: candle swap status ${id} --profile <that profile>`
|
|
30395
|
+
});
|
|
30396
|
+
const result = parsed.booleans.has("--wait") ? await waitForSettlement(ctx, key, id, found, facts) : { ...found, ...bridgeStatusField(ctx, found, facts) };
|
|
30397
|
+
return printTradingResult(ctx, result);
|
|
30398
|
+
} catch (error) {
|
|
30399
|
+
return tradingFailure(ctx, error);
|
|
30393
30400
|
}
|
|
30394
|
-
return lines;
|
|
30395
|
-
}
|
|
30396
|
-
var LOG_PAGE_BLOCKS = 10000n, LOG_PAGE_FLOOR = 250n;
|
|
30397
|
-
var init_evm_sweep = __esm(() => {
|
|
30398
|
-
init_evm_lite();
|
|
30399
|
-
});
|
|
30400
|
-
|
|
30401
|
-
// src/vault/evm-tee.ts
|
|
30402
|
-
function resolveHoodClient(ctx, flag) {
|
|
30403
|
-
const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
30404
|
-
if ("error" in resolved)
|
|
30405
|
-
return resolved;
|
|
30406
|
-
return {
|
|
30407
|
-
rpc: createEvmRpc(resolved.url, ctx.deps.fetch),
|
|
30408
|
-
url: resolved.url,
|
|
30409
|
-
host: rpcHostOf2(resolved.url),
|
|
30410
|
-
builtIn: resolved.builtIn
|
|
30411
|
-
};
|
|
30412
30401
|
}
|
|
30413
|
-
function
|
|
30414
|
-
|
|
30402
|
+
function bridgeFactsOf(raw) {
|
|
30403
|
+
const parsed = bridgeFactsSchema.safeParse(raw);
|
|
30404
|
+
return parsed.success ? parsed.data : undefined;
|
|
30415
30405
|
}
|
|
30416
|
-
|
|
30417
|
-
|
|
30418
|
-
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
30419
|
-
throw new VaultError("EVM_CHAIN_MISMATCH", `${client.host} answered chain id ${chainId}; Hood TEE wallets live on Hood, chain id ${HOOD_CHAIN_ID}, only.`, { suggestion: "Nothing was signed or written. Point --rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC." });
|
|
30420
|
-
}
|
|
30406
|
+
function isBridgeJob(job, facts) {
|
|
30407
|
+
return facts !== undefined || job.bridge !== null && typeof job.bridge === "object";
|
|
30421
30408
|
}
|
|
30422
|
-
|
|
30423
|
-
const
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30409
|
+
function bridgeStatusField(ctx, found, facts) {
|
|
30410
|
+
const job = found.job ?? {};
|
|
30411
|
+
if (found.kind !== "swap" || !isBridgeJob(job, facts))
|
|
30412
|
+
return {};
|
|
30413
|
+
const status = describeBridgeJob(job, facts);
|
|
30414
|
+
for (const line of status.lines)
|
|
30415
|
+
ctx.deps.stderr.write(`${line}
|
|
30416
|
+
`);
|
|
30417
|
+
const { lines: _lines, ...rest } = status;
|
|
30418
|
+
return { bridgeStatus: rest };
|
|
30429
30419
|
}
|
|
30430
|
-
function
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
|
|
30434
|
-
|
|
30435
|
-
|
|
30436
|
-
|
|
30437
|
-
|
|
30420
|
+
function waitFinished(current, facts, sent) {
|
|
30421
|
+
const job = current.job ?? {};
|
|
30422
|
+
if (current.kind === "swap" && isBridgeJob(job, facts)) {
|
|
30423
|
+
const status = describeBridgeJob(job, facts);
|
|
30424
|
+
if (sent && status.phase === "not_broadcast")
|
|
30425
|
+
return false;
|
|
30426
|
+
return bridgeStatusFinal(status);
|
|
30427
|
+
}
|
|
30428
|
+
const settlement = settlementSchema.safeParse(job.settlement);
|
|
30429
|
+
return !settlement.success || settlement.data.state !== "pending";
|
|
30438
30430
|
}
|
|
30439
|
-
async function
|
|
30440
|
-
const
|
|
30441
|
-
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
|
|
30445
|
-
const repaired = appendNotice(outcome, `the scan start for ${wallet}`);
|
|
30446
|
-
if (outcome.written) {
|
|
30447
|
-
if (repaired !== undefined)
|
|
30448
|
-
ctx.deps.stderr.write(`${repaired}
|
|
30449
|
-
`);
|
|
30450
|
-
return { recorded: true, block: block2.toString() };
|
|
30431
|
+
async function waitForSettlement(ctx, key, id, first, facts, sent = false) {
|
|
30432
|
+
const deadline = ctx.deps.now() + BRIDGE_WAIT_MS;
|
|
30433
|
+
let current = first;
|
|
30434
|
+
while (!waitFinished(current, facts, sent) && ctx.deps.now() < deadline) {
|
|
30435
|
+
await ctx.deps.sleep(BRIDGE_WAIT_POLL_MS);
|
|
30436
|
+
current = { ...current, ...operationSchema.parse(await request(ctx, key, jobPath(current.kind, id))) };
|
|
30451
30437
|
}
|
|
30452
|
-
|
|
30453
|
-
|
|
30438
|
+
const field = bridgeStatusField(ctx, current, facts);
|
|
30439
|
+
const finished = waitFinished(current, facts, sent);
|
|
30440
|
+
if (!finished)
|
|
30441
|
+
ctx.deps.stderr.write(`Still not final after ten minutes. Re-check with candle swap status ${id} --wait.
|
|
30454
30442
|
`);
|
|
30455
|
-
return {
|
|
30443
|
+
return { ...current, ...field, waited: { final: finished } };
|
|
30456
30444
|
}
|
|
30457
|
-
async function
|
|
30458
|
-
|
|
30459
|
-
|
|
30460
|
-
|
|
30461
|
-
|
|
30462
|
-
return { appended: false, notice: error instanceof Error ? error.message : String(error) };
|
|
30463
|
-
}
|
|
30445
|
+
async function decimalsFor(ctx, asset, client) {
|
|
30446
|
+
if (BASES[asset])
|
|
30447
|
+
return BASES[asset].decimals;
|
|
30448
|
+
const reader = await client();
|
|
30449
|
+
let result;
|
|
30464
30450
|
try {
|
|
30465
|
-
|
|
30466
|
-
if (!outcome.written)
|
|
30467
|
-
return { appended: false, notice: outcome.detail };
|
|
30468
|
-
const repaired = appendNotice(outcome, `token ${entry.token}`);
|
|
30469
|
-
return { appended: true, ...repaired !== undefined ? { notice: repaired } : {} };
|
|
30451
|
+
result = await tradingRead(ctx, reader, () => reader.rpc.getTokenSupply(asset));
|
|
30470
30452
|
} catch (error) {
|
|
30471
|
-
|
|
30453
|
+
if (error instanceof TradingError)
|
|
30454
|
+
throw error;
|
|
30455
|
+
throw new TradingError("RPC_FAILED", "Solana getTokenSupply failed; no automatic re-send was made.");
|
|
30472
30456
|
}
|
|
30457
|
+
const decimals = result.decimals;
|
|
30458
|
+
if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 18)
|
|
30459
|
+
throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
|
|
30460
|
+
return decimals;
|
|
30473
30461
|
}
|
|
30474
|
-
async function
|
|
30475
|
-
if (entry.linkedWalletId === undefined) {
|
|
30476
|
-
return {
|
|
30477
|
-
lock: {
|
|
30478
|
-
state: "unknown",
|
|
30479
|
-
reason: "this wallet has no linked wallet id recorded",
|
|
30480
|
-
suggestion: `Nothing was signed. This wallet has no server row, so its lock cannot be read. candle tee sweep ${entry.address} --emergency moves the funds and exits 3; it does not record sweptAt or retire the entry.`
|
|
30481
|
-
}
|
|
30482
|
-
};
|
|
30483
|
-
}
|
|
30484
|
-
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
30485
|
-
if (apiKey === undefined)
|
|
30486
|
-
return { lock: { state: "unknown", reason: "no API key for this profile" } };
|
|
30462
|
+
async function assertDeferredExecuteSupported(ctx, key, id) {
|
|
30487
30463
|
try {
|
|
30488
|
-
|
|
30489
|
-
auth: "key",
|
|
30490
|
-
credentials: { apiKey },
|
|
30491
|
-
apiUrl: ctx.apiUrl,
|
|
30492
|
-
fetch: ctx.deps.fetch,
|
|
30493
|
-
env: ctx.deps.env
|
|
30494
|
-
});
|
|
30495
|
-
if (!result.ok)
|
|
30496
|
-
return { lock: { state: "unknown", reason: result.message ?? `HTTP ${result.status}` } };
|
|
30497
|
-
const body = result.body ?? {};
|
|
30498
|
-
const reported = typeof body.address === "string" ? body.address : undefined;
|
|
30499
|
-
if (reported === undefined || !sameAddress(reported, entry.address)) {
|
|
30500
|
-
return {
|
|
30501
|
-
lock: {
|
|
30502
|
-
state: "unknown",
|
|
30503
|
-
reason: reported === undefined ? "the response named no wallet address" : `the response is for ${reported}, not this wallet`
|
|
30504
|
-
}
|
|
30505
|
-
};
|
|
30506
|
-
}
|
|
30507
|
-
const tradedTokens = Array.isArray(body.tradedTokens) ? body.tradedTokens.filter((token) => typeof token === "string" && /^0x[0-9a-fA-F]{40}$/.test(token)) : undefined;
|
|
30508
|
-
const bridges = walletBridgesOf(result.body);
|
|
30509
|
-
const extras = {
|
|
30510
|
-
...tradedTokens !== undefined ? { tradedTokens } : {},
|
|
30511
|
-
...body.tradedTokensTruncated === true ? { tradedTokensTruncated: true } : {},
|
|
30512
|
-
...bridges !== undefined ? { bridges } : {}
|
|
30513
|
-
};
|
|
30514
|
-
if (body.activeOperation === null)
|
|
30515
|
-
return { lock: { state: "free" }, ...extras };
|
|
30516
|
-
const op = body.activeOperation;
|
|
30517
|
-
if (op === undefined || typeof op.operationId !== "string") {
|
|
30518
|
-
return { lock: { state: "unknown", reason: "the response carried no activeOperation field" }, ...extras };
|
|
30519
|
-
}
|
|
30520
|
-
return {
|
|
30521
|
-
lock: {
|
|
30522
|
-
state: "held",
|
|
30523
|
-
operationId: op.operationId,
|
|
30524
|
-
kind: typeof op.kind === "string" ? op.kind : "operation",
|
|
30525
|
-
expiresAt: typeof op.expiresAt === "number" ? op.expiresAt : 0
|
|
30526
|
-
},
|
|
30527
|
-
...extras
|
|
30528
|
-
};
|
|
30464
|
+
await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
|
|
30529
30465
|
} catch (error) {
|
|
30530
|
-
|
|
30466
|
+
if (error instanceof TradingError && error.code === "JOB_NOT_FOUND")
|
|
30467
|
+
return;
|
|
30468
|
+
throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet trade back for confirmation, so the quote could not be shown before the money moved. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that has it.");
|
|
30531
30469
|
}
|
|
30470
|
+
throw new TradingError("INVALID_RESPONSE", "The execute route answered for a trade that does not exist.");
|
|
30532
30471
|
}
|
|
30533
|
-
function
|
|
30534
|
-
|
|
30535
|
-
|
|
30536
|
-
|
|
30537
|
-
return `candle swap status ${operationId} --kind ${kind}`;
|
|
30538
|
-
return `the ${kind} operation ${operationId}`;
|
|
30539
|
-
}
|
|
30540
|
-
function assertWalletLockFree(entry, lock, now) {
|
|
30541
|
-
if (lock.state === "free")
|
|
30542
|
-
return;
|
|
30543
|
-
if (lock.state === "held") {
|
|
30544
|
-
const left = Math.max(0, Math.ceil((lock.expiresAt - now) / 60000));
|
|
30545
|
-
throw new VaultError("WALLET_BUSY", `${entry.label || entry.address} has a sequenced ${lock.kind} in flight (operation ${lock.operationId}); signing locally now could use the nonce one of its legs holds.`, {
|
|
30546
|
-
suggestion: `Nothing was signed. Wait for operation ${lock.operationId} to finish (${busyStatusHint(lock.kind, lock.operationId)}); the lock expires within ${left} minute(s) at most. If funds must move now, candle tee sweep ${entry.address} --emergency compares the chain's nonces instead.`,
|
|
30547
|
-
details: { operationId: lock.operationId }
|
|
30548
|
-
});
|
|
30549
|
-
}
|
|
30550
|
-
throw new VaultError("WALLET_LOCK_UNKNOWN", `Could not read whether ${entry.label || entry.address} has a sequenced operation in flight (${lock.reason}), so nothing is signed at its nonce.`, {
|
|
30551
|
-
suggestion: lock.suggestion ?? `Nothing was signed. Restore the API key or connectivity and run it again. If the server is unreachable and funds must move: candle tee sweep ${entry.address} --emergency`
|
|
30472
|
+
async function swap(args, ctx) {
|
|
30473
|
+
const parsed = parseArgs(args, {
|
|
30474
|
+
valueFlags: ["--amount", "--percent", "--wallet", "--to", "--client-trade-id", "--slippage-bps", "--rpc-url"],
|
|
30475
|
+
booleanFlags: ["--yes", "--wait"]
|
|
30552
30476
|
});
|
|
30553
|
-
|
|
30554
|
-
|
|
30555
|
-
|
|
30556
|
-
init_deps();
|
|
30557
|
-
init_evm_lite();
|
|
30558
|
-
init_errors();
|
|
30559
|
-
init_evm_record();
|
|
30560
|
-
init_promote_support();
|
|
30561
|
-
init_store();
|
|
30562
|
-
});
|
|
30563
|
-
|
|
30564
|
-
// src/vault/evm-transfer.ts
|
|
30565
|
-
function refuse2(code, message, opts = {}) {
|
|
30566
|
-
return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
|
|
30567
|
-
}
|
|
30568
|
-
function checkEvmDestination(to, from) {
|
|
30569
|
-
if (!to.startsWith("0x")) {
|
|
30570
|
-
throw refuse2("TRANSFER_CHAIN_MISMATCH", `${to} is not an EVM address, and ${from.label || from.address} is an EVM key.`, { suggestion: "Nothing was signed. An EVM key sends to a 0x address; a Solana key sends to a Solana address." });
|
|
30571
|
-
}
|
|
30572
|
-
const checked = checkEvmAddress(to);
|
|
30573
|
-
if (!checked.ok) {
|
|
30574
|
-
throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
|
|
30575
|
-
}
|
|
30576
|
-
if (sameEvmAddress(checked.address, from.address)) {
|
|
30577
|
-
throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
|
|
30477
|
+
if ("error" in parsed) {
|
|
30478
|
+
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
30479
|
+
return 2;
|
|
30578
30480
|
}
|
|
30579
|
-
|
|
30580
|
-
}
|
|
30581
|
-
|
|
30582
|
-
if (!
|
|
30583
|
-
|
|
30584
|
-
|
|
30585
|
-
}
|
|
30586
|
-
async function resolveEvmAsset(rpc, asset, chainId) {
|
|
30587
|
-
const upper = asset.toUpperCase();
|
|
30588
|
-
if (upper === "ETH")
|
|
30589
|
-
return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
|
|
30590
|
-
let token;
|
|
30591
|
-
if (upper === "USDG") {
|
|
30592
|
-
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
30593
|
-
return {
|
|
30594
|
-
usage: `USDG is named on Hood (chain id ${HOOD_CHAIN_ID}) only; this RPC answered chain id ${chainId}. Name the token by its contract address.`
|
|
30595
|
-
};
|
|
30596
|
-
}
|
|
30597
|
-
token = toChecksumAddress(HOOD_USDG_ADDRESS);
|
|
30598
|
-
} else {
|
|
30599
|
-
const checked = checkEvmAddress(asset);
|
|
30600
|
-
if (!checked.ok)
|
|
30601
|
-
return {
|
|
30602
|
-
usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
|
|
30603
|
-
};
|
|
30604
|
-
token = checked.address;
|
|
30481
|
+
const flags = parsed.values;
|
|
30482
|
+
const id = flags["--client-trade-id"] ?? `swap-${randomUUID()}`;
|
|
30483
|
+
const slippage = Number(flags["--slippage-bps"] ?? "50");
|
|
30484
|
+
if (parsed.positionals.length !== 2 || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
|
|
30485
|
+
writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> [--wallet <tee-or-embedded>] [--to <tee>] [--wait] [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]. Solana: SOL, USDC, CNDL or a mint. Hood: ETH, USDG or a 0x token. SOL or USDC to ETH or USDG, or back, is a bridge between two TEE wallets.", ctx.json);
|
|
30486
|
+
return 2;
|
|
30605
30487
|
}
|
|
30606
|
-
let decimals;
|
|
30607
30488
|
try {
|
|
30608
|
-
|
|
30609
|
-
|
|
30610
|
-
|
|
30611
|
-
|
|
30612
|
-
|
|
30613
|
-
|
|
30614
|
-
|
|
30615
|
-
|
|
30616
|
-
|
|
30617
|
-
|
|
30618
|
-
|
|
30619
|
-
|
|
30620
|
-
|
|
30621
|
-
|
|
30622
|
-
|
|
30623
|
-
|
|
30624
|
-
|
|
30625
|
-
|
|
30626
|
-
|
|
30627
|
-
|
|
30628
|
-
|
|
30629
|
-
|
|
30630
|
-
|
|
30631
|
-
|
|
30632
|
-
|
|
30633
|
-
|
|
30634
|
-
|
|
30635
|
-
|
|
30636
|
-
|
|
30637
|
-
|
|
30638
|
-
|
|
30639
|
-
|
|
30640
|
-
|
|
30641
|
-
|
|
30642
|
-
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
|
|
30646
|
-
|
|
30647
|
-
|
|
30648
|
-
|
|
30649
|
-
|
|
30650
|
-
|
|
30651
|
-
|
|
30652
|
-
|
|
30653
|
-
|
|
30654
|
-
|
|
30655
|
-
|
|
30656
|
-
|
|
30657
|
-
const
|
|
30658
|
-
|
|
30659
|
-
|
|
30660
|
-
|
|
30661
|
-
|
|
30662
|
-
|
|
30663
|
-
|
|
30664
|
-
|
|
30665
|
-
|
|
30666
|
-
|
|
30667
|
-
|
|
30668
|
-
|
|
30669
|
-
|
|
30670
|
-
|
|
30671
|
-
|
|
30672
|
-
|
|
30673
|
-
|
|
30674
|
-
|
|
30675
|
-
|
|
30676
|
-
|
|
30677
|
-
|
|
30678
|
-
|
|
30679
|
-
|
|
30680
|
-
|
|
30681
|
-
|
|
30489
|
+
const fromAsset = classifyAsset(parsed.positionals[0]);
|
|
30490
|
+
const toAsset = classifyAsset(parsed.positionals[1]);
|
|
30491
|
+
const bridge = bridgePair(fromAsset, toAsset);
|
|
30492
|
+
if (bridge)
|
|
30493
|
+
return await bridgeSwap(ctx, {
|
|
30494
|
+
flags,
|
|
30495
|
+
yes: parsed.booleans.has("--yes"),
|
|
30496
|
+
wait: parsed.booleans.has("--wait"),
|
|
30497
|
+
id,
|
|
30498
|
+
slippage,
|
|
30499
|
+
...bridge
|
|
30500
|
+
});
|
|
30501
|
+
if (flags["--to"] !== undefined || parsed.booleans.has("--wait"))
|
|
30502
|
+
throw new TradingUsage("--to and --wait are for a bridge: SOL or USDC to ETH or USDG, or back.");
|
|
30503
|
+
const chain2 = pairChain(fromAsset, toAsset);
|
|
30504
|
+
const walletFlag = flags["--wallet"];
|
|
30505
|
+
const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
|
|
30506
|
+
if (named !== undefined && named !== chain2)
|
|
30507
|
+
throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, chain2);
|
|
30508
|
+
if (chain2 === "hood")
|
|
30509
|
+
return await hoodSwap(ctx, {
|
|
30510
|
+
flags,
|
|
30511
|
+
yes: parsed.booleans.has("--yes"),
|
|
30512
|
+
id,
|
|
30513
|
+
slippage,
|
|
30514
|
+
from: fromAsset,
|
|
30515
|
+
to: toAsset
|
|
30516
|
+
});
|
|
30517
|
+
const from = fromAsset.asset;
|
|
30518
|
+
const to = toAsset.asset;
|
|
30519
|
+
if (from === to)
|
|
30520
|
+
throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
|
|
30521
|
+
const fromBase = baseAsset(from);
|
|
30522
|
+
const toBase = baseAsset(to);
|
|
30523
|
+
if (!fromBase && !toBase)
|
|
30524
|
+
throw new TradingError("PAIR_UNSUPPORTED", "A token trade must have SOL, USDC or CNDL on one side; token-to-token routing is unavailable.");
|
|
30525
|
+
if (flags["--amount"])
|
|
30526
|
+
rawAmount(flags["--amount"], 18);
|
|
30527
|
+
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
30528
|
+
if (percent !== undefined && percent > 100000000n)
|
|
30529
|
+
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
30530
|
+
const kind = fromBase && toBase ? "swap" : "trade";
|
|
30531
|
+
const key = await tradingKey(ctx);
|
|
30532
|
+
const prior = await lookupOperation(ctx, key, id, kind);
|
|
30533
|
+
if (prior)
|
|
30534
|
+
return printTradingResult(ctx, prior);
|
|
30535
|
+
const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
|
|
30536
|
+
if (payerWallet.kind === "embedded" && kind === "swap")
|
|
30537
|
+
throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
|
|
30538
|
+
const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
|
|
30539
|
+
const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
|
|
30540
|
+
const decimals = await decimalsFor(ctx, from, solana);
|
|
30541
|
+
const outDecimals = await decimalsFor(ctx, to, solana);
|
|
30542
|
+
let amountRaw;
|
|
30543
|
+
if (percent !== undefined) {
|
|
30544
|
+
const reader = await solana();
|
|
30545
|
+
let balance;
|
|
30546
|
+
if (from === "SOL")
|
|
30547
|
+
balance = await tradingRead(ctx, reader, () => reader.rpc.getBalance(wallet.address));
|
|
30548
|
+
else {
|
|
30549
|
+
const mint = BASES[from]?.mint ?? from;
|
|
30550
|
+
const accounts = (await tradingRead(ctx, reader, () => Promise.all([
|
|
30551
|
+
reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_PROGRAM_ID),
|
|
30552
|
+
reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_2022_PROGRAM_ID)
|
|
30553
|
+
]))).flat();
|
|
30554
|
+
balance = accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
|
|
30555
|
+
}
|
|
30556
|
+
amountRaw = (balance * percent / 100000000n).toString();
|
|
30557
|
+
if (amountRaw === "0")
|
|
30558
|
+
throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
|
|
30559
|
+
} else
|
|
30560
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
30561
|
+
if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
|
|
30562
|
+
throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
|
|
30563
|
+
if (payerWallet.kind === "embedded")
|
|
30564
|
+
await assertDeferredExecuteSupported(ctx, key, id);
|
|
30565
|
+
if (!await claimOperation(ctx, key, id, kind))
|
|
30566
|
+
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
30567
|
+
ctx.deps.stderr.write(`Operation: ${id}
|
|
30568
|
+
`);
|
|
30569
|
+
const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
|
|
30570
|
+
const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
|
|
30571
|
+
clientTradeId: id,
|
|
30572
|
+
from,
|
|
30573
|
+
to,
|
|
30574
|
+
amountRaw,
|
|
30575
|
+
maxSlippageBps: slippage,
|
|
30576
|
+
payer
|
|
30577
|
+
}) : await request(ctx, key, "/api/v1/trade/agent/build", {
|
|
30578
|
+
clientTradeId: id,
|
|
30579
|
+
chain: "solana",
|
|
30580
|
+
mint: fromBase ? to : from,
|
|
30581
|
+
side: fromBase ? "buy" : "sell",
|
|
30582
|
+
quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
|
|
30583
|
+
amountRaw,
|
|
30584
|
+
maxSlippageBps: slippage,
|
|
30585
|
+
payer,
|
|
30586
|
+
...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
|
|
30587
|
+
});
|
|
30588
|
+
if (built.job || built.status === "executed")
|
|
30589
|
+
return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
|
|
30590
|
+
const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
|
|
30591
|
+
if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
|
|
30592
|
+
throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
|
|
30593
|
+
if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
|
|
30594
|
+
throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
|
|
30595
|
+
const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
|
|
30596
|
+
if (!artifacts)
|
|
30597
|
+
throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
|
|
30598
|
+
if (kind === "trade" && artifacts.quoteAsset !== (fromBase ?? toBase)?.toLowerCase())
|
|
30599
|
+
throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not the requested pair. Nothing was signed.`);
|
|
30600
|
+
if (data?.status !== "built" || typeof data.minOutRaw !== "string" || !/^\d+$/.test(data.minOutRaw) || !data.fee || !Number.isFinite(data.fee.bps))
|
|
30601
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete quote; nothing was signed.");
|
|
30602
|
+
const minimumRaw = !fromBase && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
|
|
30603
|
+
const quote = {
|
|
30604
|
+
intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from} to ${to}`,
|
|
30605
|
+
wallet: wallet.address,
|
|
30606
|
+
venue: artifacts.quoteSource ?? artifacts.venue,
|
|
30607
|
+
priceImpactPct: artifacts.priceImpactPct ?? null,
|
|
30608
|
+
fee: data.fee,
|
|
30609
|
+
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to}`,
|
|
30610
|
+
minOutRaw: data.minOutRaw,
|
|
30611
|
+
minimumReceivedRaw: minimumRaw,
|
|
30612
|
+
tokenRisks: artifacts.tokenRisks ?? []
|
|
30613
|
+
};
|
|
30614
|
+
if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
|
|
30615
|
+
return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind, quote });
|
|
30616
|
+
if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
|
|
30617
|
+
throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
|
|
30618
|
+
if (payerWallet.kind === "embedded") {
|
|
30619
|
+
const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
|
|
30620
|
+
return printTradingResult(ctx, {
|
|
30621
|
+
...executed,
|
|
30622
|
+
clientTradeId: id,
|
|
30623
|
+
kind,
|
|
30624
|
+
quote,
|
|
30625
|
+
wallet: safeText(payerWallet.address)
|
|
30626
|
+
});
|
|
30627
|
+
}
|
|
30628
|
+
const transaction = kind === "swap" ? data.transactionsBase64?.[0] : artifacts.transactionBase64;
|
|
30629
|
+
if (kind === "swap" && data.transactionsBase64?.length !== 1)
|
|
30630
|
+
throw new TradingError("INVALID_RESPONSE", "A TEE swap must contain exactly one same-chain transaction.");
|
|
30631
|
+
if (!transaction)
|
|
30632
|
+
throw new TradingError("INVALID_RESPONSE", "Missing transaction.");
|
|
30633
|
+
const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
|
|
30634
|
+
const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
|
|
30635
|
+
clientTradeId: id,
|
|
30636
|
+
swapId: data.swapId,
|
|
30637
|
+
signedTransactionsBase64: [signed]
|
|
30638
|
+
}) : await request(ctx, key, "/api/v1/trade/agent/submit", { clientTradeId: id, signedTransactions: [signed] });
|
|
30639
|
+
const settled = kind === "swap" ? reportSettlement(ctx, result.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
|
|
30640
|
+
return printTradingResult(ctx, {
|
|
30641
|
+
...result,
|
|
30642
|
+
clientTradeId: id,
|
|
30643
|
+
kind,
|
|
30644
|
+
quote,
|
|
30645
|
+
wallet: safeText(wallet.address),
|
|
30646
|
+
...settled
|
|
30647
|
+
});
|
|
30648
|
+
} catch (error) {
|
|
30649
|
+
return tradingFailure(ctx, error, id);
|
|
30650
|
+
}
|
|
30651
|
+
}
|
|
30652
|
+
function lazyEvmRpc(ctx, flag) {
|
|
30653
|
+
let rpc;
|
|
30654
|
+
return () => {
|
|
30655
|
+
if (rpc)
|
|
30656
|
+
return rpc;
|
|
30657
|
+
const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
30658
|
+
if ("error" in resolved)
|
|
30659
|
+
throw new TradingUsage(resolved.error);
|
|
30660
|
+
ctx.deps.stderr.write(`Reading from ${rpcHostOf2(resolved.url)} (Hood RPC; reads only, nothing is sent there)
|
|
30661
|
+
`);
|
|
30662
|
+
rpc = createEvmRpc(resolved.url, ctx.deps.fetch);
|
|
30663
|
+
return rpc;
|
|
30664
|
+
};
|
|
30665
|
+
}
|
|
30666
|
+
async function evmRead(what, read) {
|
|
30667
|
+
try {
|
|
30668
|
+
return await read();
|
|
30669
|
+
} catch (error) {
|
|
30670
|
+
if (error instanceof TradingError || error instanceof TradingUsage)
|
|
30671
|
+
throw error;
|
|
30672
|
+
throw new TradingError("RPC_FAILED", `Reading ${what} over the Hood RPC failed; nothing was built or signed.`);
|
|
30673
|
+
}
|
|
30674
|
+
}
|
|
30675
|
+
async function hoodDecimals(asset, rpc) {
|
|
30676
|
+
const base = asset.base ? HOOD_BASES[asset.base] : undefined;
|
|
30677
|
+
if (base)
|
|
30678
|
+
return base.decimals;
|
|
30679
|
+
const decimals = await evmRead(`${asset.asset} decimals()`, () => rpc().erc20Decimals(asset.asset));
|
|
30680
|
+
if (decimals > 36)
|
|
30681
|
+
throw new TradingError("INVALID_RESPONSE", "The token's decimals() is out of range.");
|
|
30682
|
+
return decimals;
|
|
30683
|
+
}
|
|
30684
|
+
async function recordTradedToken(ctx, wallet, token) {
|
|
30685
|
+
const skipped = (reason) => `Notice: the sealed EVM record was not updated for ${token} (${reason}). The leg landed. A later sweep still finds this token with --token ${token}, or with --from-block.`;
|
|
30686
|
+
const append = ctx.deps.appendEvmRecord;
|
|
30687
|
+
let notice;
|
|
30688
|
+
if (!append)
|
|
30689
|
+
notice = skipped("this CLI build has no sealed EVM record writer");
|
|
30690
|
+
else {
|
|
30691
|
+
try {
|
|
30692
|
+
const outcome = await append(ctx, { kind: "token", wallet: toChecksumAddress(wallet), token });
|
|
30693
|
+
notice = outcome.appended ? outcome.notice : skipped(outcome.notice);
|
|
30694
|
+
} catch (error) {
|
|
30695
|
+
notice = skipped(error instanceof Error ? error.message : "the append failed");
|
|
30696
|
+
}
|
|
30697
|
+
}
|
|
30698
|
+
if (notice)
|
|
30699
|
+
ctx.deps.stderr.write(`${safeText(notice)}
|
|
30700
|
+
`);
|
|
30701
|
+
return notice;
|
|
30702
|
+
}
|
|
30703
|
+
function describeHoodLegs(first, hasFee, primary = "trade") {
|
|
30704
|
+
const kinds = plannedLegKinds(first.legKind, first.plannedLegCount, hasFee, primary);
|
|
30705
|
+
const names = {
|
|
30706
|
+
approval: "approve",
|
|
30707
|
+
permit2Approval: "Permit2 approve",
|
|
30708
|
+
trade: "trade",
|
|
30709
|
+
feeTransfer: "fee",
|
|
30710
|
+
bridgeDeposit: "bridge deposit"
|
|
30711
|
+
};
|
|
30712
|
+
return kinds ? kinds.map((kind) => names[kind] ?? kind) : [`${first.plannedLegCount} legs, starting with ${names[first.legKind] ?? first.legKind}`];
|
|
30713
|
+
}
|
|
30714
|
+
async function hoodSwap(ctx, args) {
|
|
30715
|
+
const { flags, id, from, to } = args;
|
|
30716
|
+
if (from.asset === to.asset)
|
|
30717
|
+
throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
|
|
30718
|
+
if (!from.base && !to.base)
|
|
30719
|
+
throw new TradingError("PAIR_UNSUPPORTED", "A Hood token trade must have ETH or USDG on one side; token-to-token routing is unavailable.");
|
|
30720
|
+
if (flags["--amount"])
|
|
30721
|
+
rawAmount(flags["--amount"], 18);
|
|
30722
|
+
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
30723
|
+
if (percent !== undefined && percent > 100000000n)
|
|
30724
|
+
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
30725
|
+
const kind = from.base && to.base ? "swap" : "trade";
|
|
30726
|
+
const key = await tradingKey(ctx);
|
|
30727
|
+
const prior = await lookupOperation(ctx, key, id, kind);
|
|
30728
|
+
if (prior)
|
|
30729
|
+
return printTradingResult(ctx, prior);
|
|
30730
|
+
const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
|
|
30731
|
+
if (payer.kind === "embedded" && kind === "swap")
|
|
30732
|
+
throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
|
|
30733
|
+
const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
|
|
30734
|
+
const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
|
|
30735
|
+
const decimals = await hoodDecimals(from, rpc);
|
|
30736
|
+
const outDecimals = await hoodDecimals(to, rpc);
|
|
30737
|
+
let amountRaw;
|
|
30738
|
+
if (percent !== undefined) {
|
|
30739
|
+
const balance = from.asset === "ETH" ? await evmRead("the ETH balance", () => rpc().getBalance(payerAddress)) : await evmRead(`the ${from.asset} balance`, () => rpc().erc20BalanceOf(from.asset === "USDG" ? HOOD_USDG_ADDRESS : from.asset, payerAddress));
|
|
30740
|
+
amountRaw = (balance * percent / 100000000n).toString();
|
|
30741
|
+
if (amountRaw === "0")
|
|
30742
|
+
throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
|
|
30743
|
+
} else
|
|
30744
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
30745
|
+
if (payer.kind === "embedded")
|
|
30746
|
+
await assertDeferredExecuteSupported(ctx, key, id);
|
|
30747
|
+
if (!await claimOperation(ctx, key, id, kind))
|
|
30748
|
+
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
30749
|
+
ctx.deps.stderr.write(`Operation: ${id}
|
|
30750
|
+
`);
|
|
30751
|
+
const payerBody = payer.kind === "tee" ? { type: "linked", linkedWalletId: payer.wallet.id } : { type: "main" };
|
|
30752
|
+
const base = from.base ?? to.base;
|
|
30753
|
+
const token = from.base ? to.asset : from.asset;
|
|
30754
|
+
const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
|
|
30755
|
+
clientTradeId: id,
|
|
30756
|
+
from: from.asset,
|
|
30757
|
+
to: to.asset,
|
|
30758
|
+
amountRaw,
|
|
30759
|
+
maxSlippageBps: args.slippage,
|
|
30760
|
+
payer: payerBody
|
|
30761
|
+
}) : await request(ctx, key, "/api/v1/trade/agent/build", {
|
|
30762
|
+
clientTradeId: id,
|
|
30763
|
+
chain: "hood",
|
|
30764
|
+
mint: token,
|
|
30765
|
+
side: from.base ? "buy" : "sell",
|
|
30766
|
+
quoteAsset: base.toLowerCase(),
|
|
30767
|
+
amountRaw,
|
|
30768
|
+
maxSlippageBps: args.slippage,
|
|
30769
|
+
payer: payerBody,
|
|
30770
|
+
...payer.kind === "embedded" ? { deferExecution: true } : {}
|
|
30771
|
+
});
|
|
30772
|
+
if (built.job || built.status === "executed")
|
|
30773
|
+
return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
|
|
30774
|
+
const body = kind === "swap" ? built.payload : built;
|
|
30775
|
+
const data = swapBuildSchema.parse(body);
|
|
30776
|
+
const echoed = kind === "swap" ? body?.recipient : body?.walletAddress;
|
|
30777
|
+
if (body?.chain !== "hood" || typeof echoed !== "string" || echoed.toLowerCase() !== payerAddress.toLowerCase())
|
|
30778
|
+
throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
|
|
30779
|
+
const artifacts = kind === "swap" ? { ...data, quoteAsset: undefined, quoteSource: undefined } : data.artifacts;
|
|
30780
|
+
if (!artifacts)
|
|
30781
|
+
throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
|
|
30782
|
+
if (kind === "trade" && artifacts.quoteAsset !== base.toLowerCase())
|
|
30783
|
+
throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not ${base}. Nothing was signed.`);
|
|
30784
|
+
let sequenced;
|
|
30785
|
+
if (payer.kind === "tee") {
|
|
30786
|
+
const parsed = sequencedSchema.safeParse(body);
|
|
30787
|
+
if (!parsed.success)
|
|
30788
|
+
throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet trades one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
|
|
30789
|
+
sequenced = parsed.data;
|
|
30790
|
+
}
|
|
30791
|
+
const minimumRaw = !from.base && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
|
|
30792
|
+
const quote = {
|
|
30793
|
+
intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from.asset} to ${to.asset} on Hood`,
|
|
30794
|
+
wallet: payerAddress,
|
|
30795
|
+
venue: artifacts.quoteSource ?? artifacts.venue,
|
|
30796
|
+
priceImpactPct: artifacts.priceImpactPct ?? null,
|
|
30797
|
+
fee: data.fee,
|
|
30798
|
+
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to.asset}`,
|
|
30799
|
+
minOutRaw: data.minOutRaw,
|
|
30800
|
+
minimumReceivedRaw: minimumRaw,
|
|
30801
|
+
tokenRisks: artifacts.tokenRisks ?? []
|
|
30802
|
+
};
|
|
30803
|
+
if (sequenced) {
|
|
30804
|
+
const leg = sequenced.nextLeg;
|
|
30805
|
+
const maxFee = BigInt(leg.maxFeePerGas);
|
|
30806
|
+
const reserve = sweepReserveFloor(maxFee, kind === "trade" ? [token] : []);
|
|
30807
|
+
quote.legs = describeHoodLegs(sequenced, BigInt(data.fee.feeRaw) > 0n);
|
|
30808
|
+
quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
|
|
30809
|
+
quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a trade never spends it`;
|
|
30810
|
+
quote.operationId = sequenced.operationId;
|
|
30811
|
+
}
|
|
30812
|
+
if (!await confirmQuote(ctx, quote, args.yes)) {
|
|
30813
|
+
if (sequenced)
|
|
30814
|
+
ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade from it is refused as WALLET_BUSY until then.
|
|
30815
|
+
`);
|
|
30816
|
+
return printTradingResult(ctx, {
|
|
30817
|
+
success: true,
|
|
30818
|
+
status: "cancelled",
|
|
30819
|
+
clientTradeId: id,
|
|
30820
|
+
kind,
|
|
30821
|
+
quote,
|
|
30822
|
+
...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
|
|
30823
|
+
});
|
|
30824
|
+
}
|
|
30825
|
+
if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
|
|
30826
|
+
throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
|
|
30827
|
+
if (payer.kind === "embedded") {
|
|
30828
|
+
const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
|
|
30829
|
+
return printTradingResult(ctx, { ...executed, clientTradeId: id, kind, quote, wallet: safeText(payerAddress) });
|
|
30830
|
+
}
|
|
30831
|
+
const wallet = payer.wallet;
|
|
30832
|
+
if (wallet.chain !== "evm" || !sequenced)
|
|
30833
|
+
throw chainMismatch(`TEE wallet ${wallet.id}`, wallet.chain === "evm" ? "hood" : "solana", "hood");
|
|
30834
|
+
const recorded = kind === "trade" ? token : HOOD_USDG_ADDRESS;
|
|
30835
|
+
const notices = [];
|
|
30836
|
+
const run = await runSequencedLegs(ctx, key, {
|
|
30837
|
+
wallet,
|
|
30838
|
+
first: sequenced,
|
|
30839
|
+
submitPath: kind === "swap" ? "/api/v1/agent/swap/submit" : "/api/v1/trade/agent/submit",
|
|
30840
|
+
submitFields: kind === "swap" ? { clientTradeId: id, swapId: data.swapId } : { clientTradeId: id },
|
|
30841
|
+
unwrap: (answer) => kind === "swap" ? answer.payload ?? {} : answer,
|
|
30842
|
+
clientId: id,
|
|
30843
|
+
kind,
|
|
30844
|
+
allowedLegs: HOOD_TRADE_LEGS,
|
|
30845
|
+
onLanded: async (_leg) => {
|
|
30846
|
+
const notice = await recordTradedToken(ctx, wallet.address, toChecksumAddress(recorded));
|
|
30847
|
+
if (notice)
|
|
30848
|
+
notices.push(notice);
|
|
30849
|
+
}
|
|
30850
|
+
});
|
|
30851
|
+
const settled = kind === "swap" ? reportSettlement(ctx, run.final.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
|
|
30852
|
+
return printTradingResult(ctx, {
|
|
30853
|
+
...run.final,
|
|
30854
|
+
...settled,
|
|
30855
|
+
clientTradeId: id,
|
|
30856
|
+
kind,
|
|
30857
|
+
chain: "hood",
|
|
30858
|
+
quote,
|
|
30859
|
+
wallet: safeText(wallet.address),
|
|
30860
|
+
operationId: sequenced.operationId,
|
|
30861
|
+
landedLegs: run.landed,
|
|
30862
|
+
evmRecord: { token: toChecksumAddress(recorded), notices }
|
|
30863
|
+
});
|
|
30864
|
+
}
|
|
30865
|
+
async function bridgeOriginBalance(ctx, from, address, solana, evm) {
|
|
30866
|
+
if (from === "ETH")
|
|
30867
|
+
return evmRead("the ETH balance", () => evm().getBalance(address));
|
|
30868
|
+
if (from === "USDG")
|
|
30869
|
+
return evmRead("the USDG balance", () => evm().erc20BalanceOf(HOOD_USDG_ADDRESS, address));
|
|
30870
|
+
const reader = await solana();
|
|
30871
|
+
if (from === "SOL")
|
|
30872
|
+
return tradingRead(ctx, reader, () => reader.rpc.getBalance(address));
|
|
30873
|
+
const mint = BASES.USDC?.mint;
|
|
30874
|
+
const accounts = (await tradingRead(ctx, reader, () => Promise.all([
|
|
30875
|
+
reader.rpc.getTokenAccountsByOwner(address, TOKEN_PROGRAM_ID),
|
|
30876
|
+
reader.rpc.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID)
|
|
30877
|
+
]))).flat();
|
|
30878
|
+
return accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
|
|
30879
|
+
}
|
|
30880
|
+
async function bridgeSwap(ctx, args) {
|
|
30881
|
+
const { flags, id, from, to } = args;
|
|
30882
|
+
const origin = BRIDGE_ASSETS[from].chain;
|
|
30883
|
+
const destination = BRIDGE_ASSETS[to].chain;
|
|
30884
|
+
const walletFlag = flags["--wallet"];
|
|
30885
|
+
const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
|
|
30886
|
+
if (named !== undefined && named !== origin)
|
|
30887
|
+
throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, origin);
|
|
30888
|
+
if (flags["--amount"])
|
|
30889
|
+
rawAmount(flags["--amount"], 18);
|
|
30890
|
+
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
30891
|
+
if (percent !== undefined && percent > 100000000n)
|
|
30892
|
+
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
30893
|
+
const key = await tradingKey(ctx);
|
|
30894
|
+
const prior = await lookupOperation(ctx, key, id, "swap");
|
|
30895
|
+
if (prior)
|
|
30896
|
+
return printTradingResult(ctx, {
|
|
30897
|
+
...prior,
|
|
30898
|
+
...bridgeStatusField(ctx, prior, bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge))
|
|
30899
|
+
});
|
|
30900
|
+
const payer = await tradingPayer(ctx, key, walletFlag, "swap:write", origin);
|
|
30901
|
+
if (payer.kind !== "tee")
|
|
30902
|
+
throw new TradingError("CHAIN_MISMATCH", `Only a TEE wallet bridges, and the embedded wallet ${safeText(payer.address)} is not one. Name a ${chainName(origin)} TEE wallet with --wallet. Nothing was built.`);
|
|
30903
|
+
const wallet = payer.wallet;
|
|
30904
|
+
const { rows } = await listTradingWallets(ctx, key, "swap:write");
|
|
30905
|
+
const onDestination = rows.filter((row) => rowChain(row) === destination);
|
|
30906
|
+
const toFlag = flags["--to"];
|
|
30907
|
+
let toWalletId;
|
|
30908
|
+
let candidates = onDestination.filter((row) => row.active);
|
|
30909
|
+
if (toFlag !== undefined) {
|
|
30910
|
+
const matches = rows.filter((row) => matchesName(row, toFlag));
|
|
30911
|
+
const match = matches[0];
|
|
30912
|
+
if (matches.length > 1)
|
|
30913
|
+
throw new TradingError("TEE_WALLET_REQUIRED", `"${safeText(toFlag)}" matches ${matches.length} TEE wallets on this key: ${matches.map(describeWallet).join("; ")}. Name one by id or address.`);
|
|
30914
|
+
if (!match || rowChain(match) !== destination)
|
|
30915
|
+
throw new TradingError("BRIDGE_DESTINATION_MISSING", `--to ${safeText(toFlag)} is not a ${chainName(destination)} TEE wallet on this key. A bridge lands only in this key's own TEE wallet on ${chainName(destination)}${onDestination.length > 0 ? `: ${onDestination.map(describeWallet).join("; ")}` : ""}. Nothing was built.`);
|
|
30916
|
+
if (!match.active)
|
|
30917
|
+
throw new TradingError("TEE_WALLET_INACTIVE", `The destination ${describeWallet(match)} is not a verified-active TEE wallet. Nothing was built.`);
|
|
30918
|
+
toWalletId = match.id;
|
|
30919
|
+
candidates = [match];
|
|
30920
|
+
} else if (candidates.length !== 1) {
|
|
30921
|
+
throw new TradingError("BRIDGE_DESTINATION_MISSING", candidates.length === 0 ? `This key has no active ${chainName(destination)} TEE wallet to bridge into. Promote one onto this key (candle vault promote), then bridge. Nothing was built.` : `This key has more than one ${chainName(destination)} TEE wallet; name the destination with --to: ${candidates.map(describeWallet).join("; ")}. Nothing was built.`);
|
|
30922
|
+
}
|
|
30923
|
+
const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
|
|
30924
|
+
const evm = lazyEvmRpc(ctx, flags["--rpc-url"]);
|
|
30925
|
+
const decimals = BRIDGE_ASSETS[from].decimals;
|
|
30926
|
+
let amountRaw;
|
|
30927
|
+
if (percent !== undefined) {
|
|
30928
|
+
const balance = await bridgeOriginBalance(ctx, from, wallet.address, solana, evm);
|
|
30929
|
+
amountRaw = (balance * percent / 100000000n).toString();
|
|
30930
|
+
if (amountRaw === "0")
|
|
30931
|
+
throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
|
|
30932
|
+
} else
|
|
30933
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
30934
|
+
if (!await claimOperation(ctx, key, id, "swap"))
|
|
30935
|
+
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
30936
|
+
ctx.deps.stderr.write(`Operation: ${id}
|
|
30937
|
+
`);
|
|
30938
|
+
const built = await request(ctx, key, "/api/v1/agent/swap/build", {
|
|
30939
|
+
clientTradeId: id,
|
|
30940
|
+
from,
|
|
30941
|
+
to,
|
|
30942
|
+
amountRaw,
|
|
30943
|
+
maxSlippageBps: args.slippage,
|
|
30944
|
+
payer: { type: "linked", linkedWalletId: wallet.id },
|
|
30945
|
+
...toWalletId !== undefined ? { toWalletId } : {}
|
|
30946
|
+
});
|
|
30947
|
+
if (built.job || built.status === "executed")
|
|
30948
|
+
return printTradingResult(ctx, { ...built, clientTradeId: id, kind: "swap" });
|
|
30949
|
+
const body = built.payload ?? {};
|
|
30950
|
+
const parsedBuild = bridgeBuildSchema.safeParse(body);
|
|
30951
|
+
if (!parsedBuild.success)
|
|
30952
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete bridge quote; nothing was signed.");
|
|
30953
|
+
const data = parsedBuild.data;
|
|
30954
|
+
if (data.venue !== "relay")
|
|
30955
|
+
throw new TradingError("INVALID_RESPONSE", `A bridge goes through Relay, not ${safeText(data.venue)}; nothing was signed.`);
|
|
30956
|
+
if (data.fee.bps !== 0 || data.fee.feeRaw !== "0")
|
|
30957
|
+
throw new TradingError("INVALID_RESPONSE", "A bridge carries no Candle fee, and this quote has one; nothing was signed.");
|
|
30958
|
+
const recipient = candidates.find((row) => destination === "hood" ? sameEvmAddress(row.address, data.recipient) : row.address === data.recipient);
|
|
30959
|
+
if (!recipient)
|
|
30960
|
+
throw new TradingError("INVALID_RESPONSE", `Candle named ${safeText(data.recipient)} as the destination, which is not ${toWalletId !== undefined ? "the TEE wallet --to named" : `this key's ${chainName(destination)} TEE wallet`}; nothing was signed.`);
|
|
30961
|
+
const facts = {
|
|
30962
|
+
from,
|
|
30963
|
+
to,
|
|
30964
|
+
recipient: recipient.address,
|
|
30965
|
+
...data.statusChecks[0] !== undefined ? { statusCheck: data.statusChecks[0] } : {}
|
|
30966
|
+
};
|
|
30967
|
+
await saveOperationBridge(ctx, key, id, { ...facts });
|
|
30968
|
+
const refused = (problem) => new TradingError("RELAY_STEP_REFUSED", `Relay's deposit did not pass this machine's check (${problem}); nothing was signed.`);
|
|
30969
|
+
let sequenced;
|
|
30970
|
+
let transaction;
|
|
30971
|
+
const hoodOrigin = from === "ETH" || from === "USDG" ? from : undefined;
|
|
30972
|
+
if (hoodOrigin) {
|
|
30973
|
+
const parsedLeg = sequencedSchema.safeParse(body);
|
|
30974
|
+
if (!parsedLeg.success)
|
|
30975
|
+
throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet bridges one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
|
|
30976
|
+
sequenced = parsedLeg.data;
|
|
30977
|
+
if (typeof data.walletAddress !== "string" || !sameEvmAddress(data.walletAddress, wallet.address))
|
|
30978
|
+
throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
|
|
30979
|
+
const plan = plannedLegKinds(sequenced.legKind, sequenced.plannedLegCount, false, "bridgeDeposit");
|
|
30980
|
+
if (!bridgePlanAdmitted(hoodOrigin, plan))
|
|
30981
|
+
throw refused(`the plan is ${sequenced.plannedLegCount} leg(s) starting with ${sequenced.legKind}`);
|
|
30982
|
+
const problem = relayHoodLegProblem(sequenced.legKind, sequenced.nextLeg, {
|
|
30983
|
+
origin: hoodOrigin,
|
|
30984
|
+
payer: wallet.address,
|
|
30985
|
+
amountRaw
|
|
30986
|
+
});
|
|
30987
|
+
if (problem)
|
|
30988
|
+
throw refused(problem);
|
|
30989
|
+
} else {
|
|
30990
|
+
if (data.transactionsBase64?.length !== 1)
|
|
30991
|
+
throw refused(`Candle returned ${data.transactionsBase64?.length ?? 0} transactions, not one deposit`);
|
|
30992
|
+
transaction = data.transactionsBase64[0];
|
|
30993
|
+
const reader = await solana();
|
|
30994
|
+
const problem = await tradingRead(ctx, reader, () => relaySolanaDepositProblem(transaction, wallet.address, reader.rpc));
|
|
30995
|
+
if (problem)
|
|
30996
|
+
throw refused(problem);
|
|
30997
|
+
}
|
|
30998
|
+
const outDecimals = BRIDGE_ASSETS[to].decimals;
|
|
30999
|
+
const minimumRaw = (BigInt(data.expectedOutRaw) * BigInt(1e4 - args.slippage) / 10000n).toString();
|
|
31000
|
+
const quote = {
|
|
31001
|
+
intent: `Bridge ${decimalAmount(amountRaw, decimals)} ${from} on ${chainName(origin)} to ${to} on ${chainName(destination)}`,
|
|
31002
|
+
wallet: wallet.address,
|
|
31003
|
+
venue: "relay",
|
|
31004
|
+
priceImpactPct: null,
|
|
31005
|
+
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to} (Relay's estimate ${decimalAmount(data.expectedOutRaw, outDecimals)} ${to}, less the ${args.slippage} bps slippage bound)`,
|
|
31006
|
+
minimumReceivedRaw: minimumRaw,
|
|
31007
|
+
expectedOutRaw: data.expectedOutRaw,
|
|
31008
|
+
destination: `${recipient.label ? `${recipient.label} ` : ""}${recipient.address} (${chainName(destination)} TEE wallet on this key)`,
|
|
31009
|
+
candleFee: "none",
|
|
31010
|
+
relayFees: data.venueCostUsd !== undefined ? `about $${data.venueCostUsd} as Relay reports it` : "not reported",
|
|
31011
|
+
estimatedTime: data.venueTimeEstimateSec !== undefined ? `about ${data.venueTimeEstimateSec}s as Relay reports it` : "not reported",
|
|
31012
|
+
tokenRisks: []
|
|
31013
|
+
};
|
|
31014
|
+
if (sequenced) {
|
|
31015
|
+
const leg = sequenced.nextLeg;
|
|
31016
|
+
const maxFee = BigInt(leg.maxFeePerGas);
|
|
31017
|
+
const reserve = sweepReserveFloor(maxFee, []);
|
|
31018
|
+
quote.legs = describeHoodLegs(sequenced, false, "bridgeDeposit");
|
|
31019
|
+
quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
|
|
31020
|
+
quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a bridge never spends it`;
|
|
31021
|
+
quote.operationId = sequenced.operationId;
|
|
31022
|
+
}
|
|
31023
|
+
if (!await confirmQuote(ctx, quote, args.yes)) {
|
|
31024
|
+
if (sequenced)
|
|
31025
|
+
ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade or sweep from it waits until then.
|
|
31026
|
+
`);
|
|
31027
|
+
return printTradingResult(ctx, {
|
|
31028
|
+
success: true,
|
|
31029
|
+
status: "cancelled",
|
|
31030
|
+
clientTradeId: id,
|
|
31031
|
+
kind: "swap",
|
|
31032
|
+
quote,
|
|
31033
|
+
...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
|
|
31034
|
+
});
|
|
31035
|
+
}
|
|
31036
|
+
if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
|
|
31037
|
+
throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
|
|
31038
|
+
let result;
|
|
31039
|
+
let depositHash;
|
|
31040
|
+
if (sequenced && hoodOrigin) {
|
|
31041
|
+
const run = await runSequencedLegs(ctx, key, {
|
|
31042
|
+
wallet,
|
|
31043
|
+
first: sequenced,
|
|
31044
|
+
submitPath: "/api/v1/agent/swap/submit",
|
|
31045
|
+
submitFields: { clientTradeId: id, swapId: data.swapId },
|
|
31046
|
+
unwrap: (answer) => answer.payload ?? {},
|
|
31047
|
+
clientId: id,
|
|
31048
|
+
kind: "swap",
|
|
31049
|
+
primaryLeg: "bridgeDeposit",
|
|
31050
|
+
allowedLegs: bridgeLegKinds(hoodOrigin),
|
|
31051
|
+
checkLeg: (kind, leg) => relayHoodLegProblem(kind, leg, { origin: hoodOrigin, payer: wallet.address, amountRaw }) === null,
|
|
31052
|
+
onLanded: async () => {}
|
|
31053
|
+
});
|
|
31054
|
+
depositHash = run.landed.find((leg) => leg.kind === "bridgeDeposit")?.hash;
|
|
31055
|
+
result = {
|
|
31056
|
+
...run.final,
|
|
31057
|
+
chain: "hood",
|
|
31058
|
+
operationId: sequenced.operationId,
|
|
31059
|
+
landedLegs: run.landed
|
|
31060
|
+
};
|
|
31061
|
+
} else {
|
|
31062
|
+
const signed = await relaySign(ctx, key, wallet, transaction);
|
|
31063
|
+
const submitted = await request(ctx, key, "/api/v1/agent/swap/submit", {
|
|
31064
|
+
clientTradeId: id,
|
|
31065
|
+
swapId: data.swapId,
|
|
31066
|
+
signedTransactionsBase64: [signed]
|
|
31067
|
+
});
|
|
31068
|
+
const payload = submitted.payload ?? {};
|
|
31069
|
+
const hashes = Array.isArray(payload.hashes) ? payload.hashes.filter((h) => typeof h === "string") : [];
|
|
31070
|
+
depositHash = hashes[0];
|
|
31071
|
+
result = submitted;
|
|
31072
|
+
}
|
|
31073
|
+
ctx.deps.stderr.write(`Deposit ${depositHash ? safeText(depositHash) : "sent"}: filling.${args.wait ? "" : ` Check it with candle swap status ${id} (add --wait to follow it).`}
|
|
31074
|
+
`);
|
|
31075
|
+
const receipt = {
|
|
31076
|
+
...result,
|
|
31077
|
+
clientTradeId: id,
|
|
31078
|
+
kind: "swap",
|
|
31079
|
+
bridge: { from, to, destination: recipient.address, ...depositHash ? { depositHash } : {} },
|
|
31080
|
+
quote,
|
|
31081
|
+
wallet: safeText(wallet.address)
|
|
31082
|
+
};
|
|
31083
|
+
if (!args.wait)
|
|
31084
|
+
return printTradingResult(ctx, receipt);
|
|
31085
|
+
try {
|
|
31086
|
+
const found = await lookupOperation(ctx, key, id, "swap");
|
|
31087
|
+
if (!found)
|
|
31088
|
+
return printTradingResult(ctx, { ...receipt, waited: { final: false } });
|
|
31089
|
+
const waited = await waitForSettlement(ctx, key, id, found, facts, true);
|
|
31090
|
+
return printTradingResult(ctx, {
|
|
31091
|
+
...receipt,
|
|
31092
|
+
job: waited.job,
|
|
31093
|
+
bridgeStatus: waited.bridgeStatus,
|
|
31094
|
+
waited: waited.waited
|
|
31095
|
+
});
|
|
31096
|
+
} catch (error) {
|
|
31097
|
+
const reason = safeText(error instanceof Error ? describeRpcFailure(error) : String(error));
|
|
31098
|
+
ctx.deps.stderr.write(`Could not follow the bridge (${reason}). The deposit was sent; do not send it again. Re-check with candle swap status ${id} --wait.
|
|
31099
|
+
`);
|
|
31100
|
+
return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
|
|
31101
|
+
}
|
|
31102
|
+
}
|
|
31103
|
+
var settlementSchema, bridgeFactsSchema, HOOD_TRADE_LEGS, bridgeBuildSchema;
|
|
31104
|
+
var init_swap = __esm(() => {
|
|
31105
|
+
init_zod();
|
|
31106
|
+
init_args();
|
|
31107
|
+
init_bridge();
|
|
31108
|
+
init_evm_lite();
|
|
31109
|
+
init_profiles();
|
|
31110
|
+
init_render();
|
|
31111
|
+
init_solana_endpoint();
|
|
31112
|
+
init_solana_lite();
|
|
31113
|
+
init_trading();
|
|
31114
|
+
settlementSchema = exports_external.object({
|
|
31115
|
+
state: exports_external.enum(["settled", "pending", "failed", "uncertain"]),
|
|
31116
|
+
settledOutRaw: exports_external.string().regex(/^\d+$/).optional()
|
|
31117
|
+
});
|
|
31118
|
+
bridgeFactsSchema = exports_external.object({
|
|
31119
|
+
from: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
|
|
31120
|
+
to: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
|
|
31121
|
+
recipient: exports_external.string(),
|
|
31122
|
+
statusCheck: exports_external.string().optional()
|
|
31123
|
+
});
|
|
31124
|
+
HOOD_TRADE_LEGS = ["approval", "permit2Approval", "trade", "feeTransfer"];
|
|
31125
|
+
bridgeBuildSchema = exports_external.object({
|
|
31126
|
+
status: exports_external.literal("built"),
|
|
31127
|
+
swapId: exports_external.string().min(1),
|
|
31128
|
+
venue: exports_external.string(),
|
|
31129
|
+
fee: exports_external.object({ bps: exports_external.number(), feeRaw: exports_external.string() }).passthrough(),
|
|
31130
|
+
expectedOutRaw: exports_external.string().regex(/^\d+$/),
|
|
31131
|
+
expiresAt: exports_external.number().finite(),
|
|
31132
|
+
recipient: exports_external.string().min(1),
|
|
31133
|
+
statusChecks: exports_external.array(exports_external.string()).default([]),
|
|
31134
|
+
transactionsBase64: exports_external.array(exports_external.string()).optional(),
|
|
31135
|
+
walletAddress: exports_external.string().optional(),
|
|
31136
|
+
venueCostUsd: exports_external.number().finite().optional(),
|
|
31137
|
+
venueTimeEstimateSec: exports_external.number().finite().optional()
|
|
31138
|
+
}).passthrough();
|
|
31139
|
+
});
|
|
31140
|
+
|
|
31141
|
+
// src/vault/evm-sweep.ts
|
|
31142
|
+
function sameToken(a, b) {
|
|
31143
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
31144
|
+
}
|
|
31145
|
+
function mergedTokens(sources) {
|
|
31146
|
+
const out = [];
|
|
31147
|
+
const seen = new Set;
|
|
31148
|
+
for (const token of [
|
|
31149
|
+
...sources.always,
|
|
31150
|
+
...sources.flags,
|
|
31151
|
+
...sources.record.tokens,
|
|
31152
|
+
...sources.server.tokens,
|
|
31153
|
+
...sources.logs.tokens
|
|
31154
|
+
]) {
|
|
31155
|
+
const key = token.toLowerCase();
|
|
31156
|
+
if (seen.has(key))
|
|
31157
|
+
continue;
|
|
31158
|
+
seen.add(key);
|
|
31159
|
+
out.push(toChecksumAddress(token));
|
|
31160
|
+
}
|
|
31161
|
+
return out;
|
|
31162
|
+
}
|
|
31163
|
+
function alwaysTokens() {
|
|
31164
|
+
return [toChecksumAddress(HOOD_USDG_ADDRESS), HOOD_WETH_ADDRESS];
|
|
31165
|
+
}
|
|
31166
|
+
function recordSource(read, wallet) {
|
|
31167
|
+
const mine = read.entries.filter((entry) => sameToken(entry.wallet, wallet));
|
|
31168
|
+
return {
|
|
31169
|
+
tokens: mine.flatMap((entry) => entry.kind === "token" ? [entry.token] : []),
|
|
31170
|
+
scanStarts: mine.flatMap((entry) => entry.kind === "scanStart" ? [BigInt(entry.block)] : []),
|
|
31171
|
+
absent: read.absent,
|
|
31172
|
+
noKey: read.noKey,
|
|
31173
|
+
unreadableLines: read.unreadableLines,
|
|
31174
|
+
partialTail: read.partialTail,
|
|
31175
|
+
path: read.path
|
|
31176
|
+
};
|
|
31177
|
+
}
|
|
31178
|
+
function startBlock(scanStarts, fromBlockFlag) {
|
|
31179
|
+
const recorded = scanStarts.length > 0 ? scanStarts.reduce((a, b) => a < b ? a : b) : undefined;
|
|
31180
|
+
if (recorded === undefined && fromBlockFlag === undefined)
|
|
31181
|
+
return;
|
|
31182
|
+
if (recorded === undefined)
|
|
31183
|
+
return { block: fromBlockFlag, source: "--from-block" };
|
|
31184
|
+
if (fromBlockFlag === undefined)
|
|
31185
|
+
return { block: recorded, source: "record" };
|
|
31186
|
+
return { block: recorded < fromBlockFlag ? recorded : fromBlockFlag, source: "record and --from-block" };
|
|
31187
|
+
}
|
|
31188
|
+
async function discoverTransferLogs(rpc, wallet, fromBlock, toBlock, opts = {}) {
|
|
31189
|
+
const seen = new Map;
|
|
31190
|
+
let page = opts.pageBlocks ?? LOG_PAGE_BLOCKS;
|
|
31191
|
+
let pages = 0;
|
|
31192
|
+
let from = fromBlock;
|
|
31193
|
+
const topics = [ERC20_TRANSFER_TOPIC, null, addressTopic(wallet)];
|
|
31194
|
+
while (from <= toBlock) {
|
|
31195
|
+
const to = from + page - 1n > toBlock ? toBlock : from + page - 1n;
|
|
31196
|
+
try {
|
|
31197
|
+
const logs = await rpc.getLogs({ fromBlock: from, toBlock: to, topics });
|
|
31198
|
+
pages += 1;
|
|
31199
|
+
for (const log of logs)
|
|
31200
|
+
seen.set(log.address.toLowerCase(), log.address);
|
|
31201
|
+
from = to + 1n;
|
|
31202
|
+
} catch (error) {
|
|
31203
|
+
if (error instanceof EvmRpcError && error.kind === "rpc" && page > LOG_PAGE_FLOOR) {
|
|
31204
|
+
page = page / 2n < LOG_PAGE_FLOOR ? LOG_PAGE_FLOOR : page / 2n;
|
|
31205
|
+
continue;
|
|
31206
|
+
}
|
|
31207
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
31208
|
+
return { failed: `eth_getLogs from block ${from} failed: ${reason}`, tokens: [...seen.values()], pages };
|
|
31209
|
+
}
|
|
31210
|
+
}
|
|
31211
|
+
return { tokens: [...seen.values()], pages };
|
|
31212
|
+
}
|
|
31213
|
+
function sourceLines(sources, host) {
|
|
31214
|
+
const lines = ["Token sources:", " USDG and WETH: always read"];
|
|
31215
|
+
const record = sources.record;
|
|
31216
|
+
if (record.noKey && record.absent) {
|
|
31217
|
+
lines.push(` sealed EVM record: none (this vault has no record key)`);
|
|
31218
|
+
} else if (record.absent) {
|
|
31219
|
+
lines.push(` sealed EVM record: absent at ${record.path} (not on this machine, or nothing recorded yet)`);
|
|
31220
|
+
} else {
|
|
31221
|
+
const damage = [
|
|
31222
|
+
...record.unreadableLines > 0 ? [`${record.unreadableLines} unreadable line(s) skipped`] : [],
|
|
31223
|
+
...record.partialTail ? ["a partial last line skipped"] : []
|
|
31224
|
+
];
|
|
31225
|
+
lines.push(` sealed EVM record: ${record.tokens.length} token(s), ${record.scanStarts.length} scan start(s)${damage.length > 0 ? `; ${damage.join(", ")}` : ""}`);
|
|
31226
|
+
}
|
|
31227
|
+
const server = sources.server;
|
|
31228
|
+
if (!server.asked)
|
|
31229
|
+
lines.push(` server traded-token list: not asked (${server.reason ?? "--emergency calls no Candle API"})`);
|
|
31230
|
+
else if (!server.answered)
|
|
31231
|
+
lines.push(` server traded-token list: FAILED (${server.reason ?? "no answer"})`);
|
|
31232
|
+
else
|
|
31233
|
+
lines.push(` server traded-token list: ${server.tokens.length} token(s)${server.truncated ? " (truncated)" : ""}`);
|
|
31234
|
+
lines.push(` --token: ${sources.flags.length}`);
|
|
31235
|
+
const logs = sources.logs;
|
|
31236
|
+
if (logs.ran && !logs.failed) {
|
|
31237
|
+
lines.push(` Transfer logs over ${host}: blocks ${logs.fromBlock} to ${logs.toBlock} (start from ${logs.startSource}), ${logs.tokens.length} contract(s)`);
|
|
31238
|
+
} else if (logs.failed) {
|
|
31239
|
+
lines.push(` Transfer logs over ${host}: FAILED (${logs.reason})`);
|
|
31240
|
+
} else {
|
|
31241
|
+
lines.push(` Transfer logs: NOT scanned (${logs.reason})`);
|
|
31242
|
+
}
|
|
31243
|
+
if (logs.ran && logs.fromBlock !== undefined) {
|
|
31244
|
+
lines.push(` A token received before block ${logs.fromBlock}, or from a contract that emits no Transfer log, still needs --token <0x...>.`);
|
|
31245
|
+
}
|
|
31246
|
+
return lines;
|
|
31247
|
+
}
|
|
31248
|
+
var LOG_PAGE_BLOCKS = 10000n, LOG_PAGE_FLOOR = 250n;
|
|
31249
|
+
var init_evm_sweep = __esm(() => {
|
|
31250
|
+
init_evm_lite();
|
|
31251
|
+
});
|
|
31252
|
+
|
|
31253
|
+
// src/vault/evm-tee.ts
|
|
31254
|
+
function resolveHoodClient(ctx, flag) {
|
|
31255
|
+
const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
31256
|
+
if ("error" in resolved)
|
|
31257
|
+
return resolved;
|
|
31258
|
+
return {
|
|
31259
|
+
rpc: createEvmRpc(resolved.url, ctx.deps.fetch),
|
|
31260
|
+
url: resolved.url,
|
|
31261
|
+
host: rpcHostOf2(resolved.url),
|
|
31262
|
+
builtIn: resolved.builtIn
|
|
31263
|
+
};
|
|
31264
|
+
}
|
|
31265
|
+
function hoodHostLine(client, what) {
|
|
31266
|
+
return `Reading ${what} from ${client.host}${client.builtIn ? " (the built-in Hood RPC)" : ""}.`;
|
|
31267
|
+
}
|
|
31268
|
+
async function assertHoodChain(client) {
|
|
31269
|
+
const chainId = await client.rpc.chainId();
|
|
31270
|
+
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
31271
|
+
throw new VaultError("EVM_CHAIN_MISMATCH", `${client.host} answered chain id ${chainId}; Hood TEE wallets live on Hood, chain id ${HOOD_CHAIN_ID}, only.`, { suggestion: "Nothing was signed or written. Point --rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC." });
|
|
31272
|
+
}
|
|
31273
|
+
}
|
|
31274
|
+
async function readEvmHoldings(rpc, address) {
|
|
31275
|
+
const [eth, usdg, weth] = await Promise.all([
|
|
31276
|
+
rpc.getBalance(address),
|
|
31277
|
+
rpc.erc20BalanceOf(toChecksumAddress(HOOD_USDG_ADDRESS), address),
|
|
31278
|
+
rpc.erc20BalanceOf(HOOD_WETH_ADDRESS, address)
|
|
31279
|
+
]);
|
|
31280
|
+
return { eth, usdg, weth };
|
|
31281
|
+
}
|
|
31282
|
+
function holdingsLines(address, holdings, observedAt) {
|
|
31283
|
+
return [
|
|
31284
|
+
`Holdings at ${address} on Hood (observed ${observedAt}):`,
|
|
31285
|
+
` ETH ${formatUnits(holdings.eth, NATIVE_DECIMALS)} (${holdings.eth} wei)`,
|
|
31286
|
+
` USDG ${formatUnits(holdings.usdg, HOOD_USDG_DECIMALS)} (${holdings.usdg} raw)`,
|
|
31287
|
+
` WETH ${formatUnits(holdings.weth, NATIVE_DECIMALS)} (${holdings.weth} raw)`,
|
|
31288
|
+
" Other tokens are not listed: an EVM RPC has no token-account listing."
|
|
31289
|
+
];
|
|
31290
|
+
}
|
|
31291
|
+
async function appendScanStart(ctx, vaultPath, wallet, block2) {
|
|
31292
|
+
const outcome = await appendEvmRecordEntry({
|
|
31293
|
+
vaultPath,
|
|
31294
|
+
entry: { kind: "scanStart", wallet, block: Number(block2) },
|
|
31295
|
+
clock: ctx.deps
|
|
31296
|
+
});
|
|
31297
|
+
const repaired = appendNotice(outcome, `the scan start for ${wallet}`);
|
|
31298
|
+
if (outcome.written) {
|
|
31299
|
+
if (repaired !== undefined)
|
|
31300
|
+
ctx.deps.stderr.write(`${repaired}
|
|
31301
|
+
`);
|
|
31302
|
+
return { recorded: true, block: block2.toString() };
|
|
31303
|
+
}
|
|
31304
|
+
ctx.deps.stderr.write(`The promote height, Hood block ${block2}, was not written to the sealed EVM record at ${evmRecordPath(vaultPath)} (${outcome.detail}).
|
|
31305
|
+
` + `Keep it: a later sweep of ${wallet} finds traded tokens from it with: candle tee sweep ${wallet} --from-block ${block2}
|
|
31306
|
+
`);
|
|
31307
|
+
return { recorded: false, block: block2.toString() };
|
|
31308
|
+
}
|
|
31309
|
+
async function appendEvmRecordForTrade(ctx, entry) {
|
|
31310
|
+
let vaultPath;
|
|
31311
|
+
try {
|
|
31312
|
+
vaultPath = defaultVaultPath(ctx.deps.env, ctx.deps.homedir());
|
|
31313
|
+
} catch (error) {
|
|
31314
|
+
return { appended: false, notice: error instanceof Error ? error.message : String(error) };
|
|
31315
|
+
}
|
|
31316
|
+
try {
|
|
31317
|
+
const outcome = await appendEvmRecordEntry({ vaultPath, entry, clock: ctx.deps });
|
|
31318
|
+
if (!outcome.written)
|
|
31319
|
+
return { appended: false, notice: outcome.detail };
|
|
31320
|
+
const repaired = appendNotice(outcome, `token ${entry.token}`);
|
|
31321
|
+
return { appended: true, ...repaired !== undefined ? { notice: repaired } : {} };
|
|
31322
|
+
} catch (error) {
|
|
31323
|
+
return { appended: false, notice: error instanceof Error ? error.message : String(error) };
|
|
31324
|
+
}
|
|
31325
|
+
}
|
|
31326
|
+
async function readHoodTeeServer(ctx, entry) {
|
|
31327
|
+
if (entry.linkedWalletId === undefined) {
|
|
31328
|
+
return {
|
|
31329
|
+
lock: {
|
|
31330
|
+
state: "unknown",
|
|
31331
|
+
reason: "this wallet has no linked wallet id recorded",
|
|
31332
|
+
suggestion: `Nothing was signed. This wallet has no server row, so its lock cannot be read. candle tee sweep ${entry.address} --emergency moves the funds and exits 3; it does not record sweptAt or retire the entry.`
|
|
31333
|
+
}
|
|
31334
|
+
};
|
|
31335
|
+
}
|
|
31336
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
31337
|
+
if (apiKey === undefined)
|
|
31338
|
+
return { lock: { state: "unknown", reason: "no API key for this profile" } };
|
|
31339
|
+
try {
|
|
31340
|
+
const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/hood-tee`, {
|
|
31341
|
+
auth: "key",
|
|
31342
|
+
credentials: { apiKey },
|
|
31343
|
+
apiUrl: ctx.apiUrl,
|
|
31344
|
+
fetch: ctx.deps.fetch,
|
|
31345
|
+
env: ctx.deps.env
|
|
31346
|
+
});
|
|
31347
|
+
if (!result.ok)
|
|
31348
|
+
return { lock: { state: "unknown", reason: result.message ?? `HTTP ${result.status}` } };
|
|
31349
|
+
const body = result.body ?? {};
|
|
31350
|
+
const reported = typeof body.address === "string" ? body.address : undefined;
|
|
31351
|
+
if (reported === undefined || !sameAddress(reported, entry.address)) {
|
|
31352
|
+
return {
|
|
31353
|
+
lock: {
|
|
31354
|
+
state: "unknown",
|
|
31355
|
+
reason: reported === undefined ? "the response named no wallet address" : `the response is for ${reported}, not this wallet`
|
|
31356
|
+
}
|
|
31357
|
+
};
|
|
31358
|
+
}
|
|
31359
|
+
const tradedTokens = Array.isArray(body.tradedTokens) ? body.tradedTokens.filter((token) => typeof token === "string" && /^0x[0-9a-fA-F]{40}$/.test(token)) : undefined;
|
|
31360
|
+
const bridges = walletBridgesOf(result.body);
|
|
31361
|
+
const extras = {
|
|
31362
|
+
...tradedTokens !== undefined ? { tradedTokens } : {},
|
|
31363
|
+
...body.tradedTokensTruncated === true ? { tradedTokensTruncated: true } : {},
|
|
31364
|
+
...bridges !== undefined ? { bridges } : {}
|
|
31365
|
+
};
|
|
31366
|
+
if (body.activeOperation === null)
|
|
31367
|
+
return { lock: { state: "free" }, ...extras };
|
|
31368
|
+
const op = body.activeOperation;
|
|
31369
|
+
if (op === undefined || typeof op.operationId !== "string") {
|
|
31370
|
+
return { lock: { state: "unknown", reason: "the response carried no activeOperation field" }, ...extras };
|
|
31371
|
+
}
|
|
31372
|
+
return {
|
|
31373
|
+
lock: {
|
|
31374
|
+
state: "held",
|
|
31375
|
+
operationId: op.operationId,
|
|
31376
|
+
kind: typeof op.kind === "string" ? op.kind : "operation",
|
|
31377
|
+
expiresAt: typeof op.expiresAt === "number" ? op.expiresAt : 0
|
|
31378
|
+
},
|
|
31379
|
+
...extras
|
|
31380
|
+
};
|
|
31381
|
+
} catch (error) {
|
|
31382
|
+
return { lock: { state: "unknown", reason: error instanceof Error ? error.message : String(error) } };
|
|
31383
|
+
}
|
|
31384
|
+
}
|
|
31385
|
+
function busyStatusHint(kind, operationId) {
|
|
31386
|
+
if (kind === "swap")
|
|
31387
|
+
return `candle swap status ${operationId}`;
|
|
31388
|
+
if (kind === "trade" || kind === "launch")
|
|
31389
|
+
return `candle swap status ${operationId} --kind ${kind}`;
|
|
31390
|
+
return `the ${kind} operation ${operationId}`;
|
|
31391
|
+
}
|
|
31392
|
+
function assertWalletLockFree(entry, lock, now) {
|
|
31393
|
+
if (lock.state === "free")
|
|
31394
|
+
return;
|
|
31395
|
+
if (lock.state === "held") {
|
|
31396
|
+
const left = Math.max(0, Math.ceil((lock.expiresAt - now) / 60000));
|
|
31397
|
+
throw new VaultError("WALLET_BUSY", `${entry.label || entry.address} has a sequenced ${lock.kind} in flight (operation ${lock.operationId}); signing locally now could use the nonce one of its legs holds.`, {
|
|
31398
|
+
suggestion: `Nothing was signed. Wait for operation ${lock.operationId} to finish (${busyStatusHint(lock.kind, lock.operationId)}); the lock expires within ${left} minute(s) at most. If funds must move now, candle tee sweep ${entry.address} --emergency compares the chain's nonces instead.`,
|
|
31399
|
+
details: { operationId: lock.operationId }
|
|
31400
|
+
});
|
|
31401
|
+
}
|
|
31402
|
+
throw new VaultError("WALLET_LOCK_UNKNOWN", `Could not read whether ${entry.label || entry.address} has a sequenced operation in flight (${lock.reason}), so nothing is signed at its nonce.`, {
|
|
31403
|
+
suggestion: lock.suggestion ?? `Nothing was signed. Restore the API key or connectivity and run it again. If the server is unreachable and funds must move: candle tee sweep ${entry.address} --emergency`
|
|
31404
|
+
});
|
|
31405
|
+
}
|
|
31406
|
+
var init_evm_tee = __esm(() => {
|
|
31407
|
+
init_bridge();
|
|
31408
|
+
init_deps();
|
|
31409
|
+
init_evm_lite();
|
|
31410
|
+
init_errors();
|
|
31411
|
+
init_evm_record();
|
|
31412
|
+
init_promote_support();
|
|
31413
|
+
init_store();
|
|
31414
|
+
});
|
|
31415
|
+
|
|
31416
|
+
// src/vault/evm-transfer.ts
|
|
31417
|
+
function refuse2(code, message, opts = {}) {
|
|
31418
|
+
return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
|
|
31419
|
+
}
|
|
31420
|
+
function checkEvmDestination(to, from) {
|
|
31421
|
+
if (!to.startsWith("0x")) {
|
|
31422
|
+
throw refuse2("TRANSFER_CHAIN_MISMATCH", `${to} is not an EVM address, and ${from.label || from.address} is an EVM key.`, { suggestion: "Nothing was signed. An EVM key sends to a 0x address; a Solana key sends to a Solana address." });
|
|
31423
|
+
}
|
|
31424
|
+
const checked = checkEvmAddress(to);
|
|
31425
|
+
if (!checked.ok) {
|
|
31426
|
+
throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
|
|
31427
|
+
}
|
|
31428
|
+
if (sameEvmAddress(checked.address, from.address)) {
|
|
31429
|
+
throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
|
|
31430
|
+
}
|
|
31431
|
+
return checked.address;
|
|
31432
|
+
}
|
|
31433
|
+
function assertSolanaDestination(to, from) {
|
|
31434
|
+
if (!looksLikeEvmAddress(to))
|
|
31435
|
+
return;
|
|
31436
|
+
throw refuse2("TRANSFER_CHAIN_MISMATCH", `${to} is an EVM address, and ${from.label || from.address} is a Solana key.`, { suggestion: "Nothing was signed. A Solana key sends to a Solana address; an EVM key sends to a 0x address." });
|
|
31437
|
+
}
|
|
31438
|
+
async function resolveEvmAsset(rpc, asset, chainId) {
|
|
31439
|
+
const upper = asset.toUpperCase();
|
|
31440
|
+
if (upper === "ETH")
|
|
31441
|
+
return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
|
|
31442
|
+
let token;
|
|
31443
|
+
if (upper === "USDG") {
|
|
31444
|
+
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
31445
|
+
return {
|
|
31446
|
+
usage: `USDG is named on Hood (chain id ${HOOD_CHAIN_ID}) only; this RPC answered chain id ${chainId}. Name the token by its contract address.`
|
|
31447
|
+
};
|
|
31448
|
+
}
|
|
31449
|
+
token = toChecksumAddress(HOOD_USDG_ADDRESS);
|
|
31450
|
+
} else {
|
|
31451
|
+
const checked = checkEvmAddress(asset);
|
|
31452
|
+
if (!checked.ok)
|
|
31453
|
+
return {
|
|
31454
|
+
usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
|
|
31455
|
+
};
|
|
31456
|
+
token = checked.address;
|
|
31457
|
+
}
|
|
31458
|
+
let decimals;
|
|
31459
|
+
try {
|
|
31460
|
+
decimals = await rpc.erc20Decimals(token);
|
|
31461
|
+
} catch (error) {
|
|
31462
|
+
throw refuse2("EVM_TOKEN_UNREADABLE", `${token} did not answer decimals(): ${error instanceof Error ? error.message : String(error)}`, { suggestion: "Nothing was signed. Check the contract address, and that this RPC serves the chain it lives on." });
|
|
31463
|
+
}
|
|
31464
|
+
const symbol = upper === "USDG" ? "USDG" : await rpc.erc20Symbol(token) ?? `${token.slice(0, 10)}…`;
|
|
31465
|
+
return { kind: "erc20", symbol, decimals, token };
|
|
31466
|
+
}
|
|
31467
|
+
function nativeName(hood, chainId) {
|
|
31468
|
+
return hood ? "ETH" : `ETH on chain ${chainId}`;
|
|
31469
|
+
}
|
|
31470
|
+
function evmDisplayLines(plan) {
|
|
31471
|
+
const { tx, asset, hood, chainId } = plan;
|
|
31472
|
+
const native = nativeName(hood, chainId);
|
|
31473
|
+
const lines = [
|
|
31474
|
+
`chain ${chainId}${hood ? " (Hood)" : ""}`,
|
|
31475
|
+
`from ${plan.from.label} ${plan.from.address}`
|
|
31476
|
+
];
|
|
31477
|
+
if (asset.kind === "native") {
|
|
31478
|
+
lines.push(`to ${tx.to}`);
|
|
31479
|
+
lines.push(`amount ${plan.amount} ${native} = ${plan.amountRaw} wei`);
|
|
31480
|
+
} else {
|
|
31481
|
+
lines.push(`to ${tx.to} (the ${asset.symbol} contract, ${asset.decimals} dp)`);
|
|
31482
|
+
lines.push(`recipient ${plan.recipient} (decoded from transfer(address,uint256))`);
|
|
31483
|
+
lines.push(`amount ${plan.amount} ${asset.symbol} = ${plan.amountRaw} raw`);
|
|
31484
|
+
}
|
|
31485
|
+
lines.push(`gas limit ${tx.gas}`);
|
|
31486
|
+
lines.push(`max fee ${tx.maxFeePerGas} wei/gas (priority ${tx.maxPriorityFeePerGas} wei/gas)`);
|
|
31487
|
+
lines.push(`fee cap ${formatUnits(plan.feeCap, NATIVE_DECIMALS)} ${native} (gas × max fee)`);
|
|
31488
|
+
lines.push(`nonce ${tx.nonce}`);
|
|
31489
|
+
return lines;
|
|
31490
|
+
}
|
|
31491
|
+
async function planEvmTransfer(rpc, input) {
|
|
31492
|
+
const to = checkEvmDestination(input.to, input.from);
|
|
31493
|
+
const chainId = await rpc.chainId();
|
|
31494
|
+
if (input.builtIn && chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
31495
|
+
throw refuse2("EVM_CHAIN_MISMATCH", `The built-in Hood RPC answered chain id ${chainId}, not ${HOOD_CHAIN_ID}.`, {
|
|
31496
|
+
suggestion: "Nothing was signed. Pass --rpc-url for another chain; the built-in endpoint is Hood's only."
|
|
31497
|
+
});
|
|
31498
|
+
}
|
|
31499
|
+
const hood = chainId === BigInt(HOOD_CHAIN_ID);
|
|
31500
|
+
const asset = await resolveEvmAsset(rpc, input.asset, chainId);
|
|
31501
|
+
if ("usage" in asset)
|
|
31502
|
+
return asset;
|
|
31503
|
+
const max = input.amount.toLowerCase() === "max";
|
|
31504
|
+
if (asset.kind === "erc20" && asset.token !== undefined && sameEvmAddress(to, asset.token)) {
|
|
31505
|
+
throw refuse2("EVM_RECIPIENT_IS_TOKEN", `${to} is the ${asset.symbol} contract itself; sending it its own tokens is a loss.`);
|
|
31506
|
+
}
|
|
31507
|
+
let amountRaw = 0n;
|
|
31508
|
+
if (!max) {
|
|
31509
|
+
const parsed = parseUnits(input.amount, asset.decimals);
|
|
31510
|
+
if (!parsed.ok && parsed.reason === "precision") {
|
|
31511
|
+
throw refuse2("EVM_AMOUNT_PRECISION", `${input.amount} has more decimal places than ${asset.symbol}'s ${asset.decimals}.`);
|
|
31512
|
+
}
|
|
31513
|
+
if (!parsed.ok)
|
|
31514
|
+
return { usage: `--amount must be a positive decimal or max; ${input.amount} is neither.` };
|
|
31515
|
+
amountRaw = parsed.raw;
|
|
31516
|
+
}
|
|
31517
|
+
const nonce = await rpc.getTransactionCount(input.from.address, "pending");
|
|
31518
|
+
const fees = await quoteFees(rpc);
|
|
31519
|
+
const nativeBalance = await rpc.getBalance(input.from.address);
|
|
31520
|
+
if (asset.kind === "erc20" && asset.token !== undefined) {
|
|
31521
|
+
if (max) {
|
|
31522
|
+
amountRaw = await rpc.erc20BalanceOf(asset.token, input.from.address);
|
|
31523
|
+
if (amountRaw === 0n) {
|
|
31524
|
+
throw new VaultError("VAULT_INDEX_INVALID", `${input.from.label} holds no ${asset.symbol}; there is nothing to send.`);
|
|
31525
|
+
}
|
|
31526
|
+
}
|
|
31527
|
+
const draft2 = buildErc20Transfer({
|
|
31528
|
+
chainId,
|
|
31529
|
+
nonce,
|
|
31530
|
+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
|
|
31531
|
+
maxFeePerGas: fees.maxFeePerGas,
|
|
31532
|
+
gas: 0n,
|
|
31533
|
+
token: asset.token,
|
|
30682
31534
|
recipient: to,
|
|
30683
31535
|
amount: amountRaw
|
|
30684
31536
|
});
|
|
@@ -31128,6 +31980,8 @@ async function resolveKeyMaterial(keyFile, chain2, ctx) {
|
|
|
31128
31980
|
return { ok: false, message: `Could not read --key-file: ${error instanceof Error ? error.message : error}` };
|
|
31129
31981
|
}
|
|
31130
31982
|
}
|
|
31983
|
+
if (!ctx.deps.isTTY.stdin)
|
|
31984
|
+
return { ok: false, message: "No TTY available for private key input; pass --key-file instead" };
|
|
31131
31985
|
try {
|
|
31132
31986
|
const promptText = chain2 === "solana" ? "Solana private key (base58 or id.json contents; input hidden): " : "EVM private key (hex; input hidden): ";
|
|
31133
31987
|
const entered = (await ctx.deps.promptSecret(promptText)).trim();
|
|
@@ -32244,6 +33098,26 @@ var init_tee_evm = __esm(() => {
|
|
|
32244
33098
|
init_wallets();
|
|
32245
33099
|
});
|
|
32246
33100
|
|
|
33101
|
+
// src/commands/tee-status-wallet.ts
|
|
33102
|
+
async function serverTeeWallet(ctx, address, chain2) {
|
|
33103
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
33104
|
+
const suggestion = "Pass --verify to read it from this machine's vault, or select the profile holding its key.";
|
|
33105
|
+
if (!apiKey)
|
|
33106
|
+
throw new TradingError("TEE_WALLET_NOT_ON_KEY", `${address}: no API key available.`, { suggestion });
|
|
33107
|
+
const { rows } = await listTradingWallets(ctx, apiKey);
|
|
33108
|
+
const row = rows.find((candidate) => candidate.chain === chain2 && (chain2 === "evm" ? sameEvmAddress(candidate.address, address) : candidate.address === address));
|
|
33109
|
+
if (!row)
|
|
33110
|
+
throw new TradingError("TEE_WALLET_NOT_ON_KEY", `${address} is not a TEE wallet bound to key ${apiKeyPrefix(apiKey) ?? "this key"}.`, { suggestion });
|
|
33111
|
+
return { row, apiKey };
|
|
33112
|
+
}
|
|
33113
|
+
var UNVERIFIED_KEY_LINE = "Key not verified on this machine. Pass --verify to open the vault and check it.";
|
|
33114
|
+
var init_tee_status_wallet = __esm(() => {
|
|
33115
|
+
init_deps();
|
|
33116
|
+
init_evm_lite();
|
|
33117
|
+
init_profiles();
|
|
33118
|
+
init_trading();
|
|
33119
|
+
});
|
|
33120
|
+
|
|
32247
33121
|
// src/commands/tee-status-evm.ts
|
|
32248
33122
|
async function readVaultEntry(ctx, address) {
|
|
32249
33123
|
const resolved = vaultPathFor(ctx, { values: {}, booleans: new Set, positionals: [] });
|
|
@@ -32282,9 +33156,10 @@ async function teeStatusEvm(ctx, parsed, address) {
|
|
|
32282
33156
|
writeUsageFailure(deps, rpcUrl.error, json);
|
|
32283
33157
|
return 2;
|
|
32284
33158
|
}
|
|
33159
|
+
const verify = parsed.booleans.has("--verify");
|
|
32285
33160
|
let vault;
|
|
32286
33161
|
try {
|
|
32287
|
-
vault = await readVaultEntry(ctx, checked.address);
|
|
33162
|
+
vault = verify ? await readVaultEntry(ctx, checked.address) : null;
|
|
32288
33163
|
} catch (error) {
|
|
32289
33164
|
if (error instanceof TradingError && error.code === "USAGE") {
|
|
32290
33165
|
writeUsageFailure(deps, error.message, json);
|
|
@@ -32296,11 +33171,21 @@ async function teeStatusEvm(ctx, parsed, address) {
|
|
|
32296
33171
|
}
|
|
32297
33172
|
throw error;
|
|
32298
33173
|
}
|
|
33174
|
+
let serverWallet = null;
|
|
33175
|
+
if (!verify) {
|
|
33176
|
+
try {
|
|
33177
|
+
serverWallet = await serverTeeWallet(ctx, address, "evm");
|
|
33178
|
+
} catch (error) {
|
|
33179
|
+
return tradingFailure(ctx, error);
|
|
33180
|
+
}
|
|
33181
|
+
}
|
|
32299
33182
|
const report = {
|
|
32300
33183
|
address: checked.address,
|
|
32301
33184
|
chain: "hood",
|
|
32302
33185
|
label: vault?.label ?? null,
|
|
32303
33186
|
source: vault ? "vault" : "server",
|
|
33187
|
+
verified: verify,
|
|
33188
|
+
...!verify ? { localState: null, retainedSweepReceipts: null, pendingSweepTransactions: null, vaultLifecycle: null } : {},
|
|
32304
33189
|
linkedWalletId: vault?.linkedWalletId ?? null,
|
|
32305
33190
|
vaultDestination: vault?.vaultDestination ?? null,
|
|
32306
33191
|
observedAt: new Date(deps.now()).toISOString()
|
|
@@ -32310,7 +33195,7 @@ async function teeStatusEvm(ctx, parsed, address) {
|
|
|
32310
33195
|
report.server = { error: "no API key available; server state not read" };
|
|
32311
33196
|
else {
|
|
32312
33197
|
try {
|
|
32313
|
-
const
|
|
33198
|
+
const rows = serverWallet ? [serverWallet.row] : (await listTradingWallets(ctx, apiKey)).rows;
|
|
32314
33199
|
const row = rows.find((candidate) => candidate.chain === "evm" && sameEvmAddress(candidate.address, address));
|
|
32315
33200
|
if (row) {
|
|
32316
33201
|
report.linkedWalletId ??= row.id;
|
|
@@ -32377,6 +33262,9 @@ async function teeStatusEvm(ctx, parsed, address) {
|
|
|
32377
33262
|
deps.stdout.write(` chain Hood (4663)
|
|
32378
33263
|
`);
|
|
32379
33264
|
deps.stdout.write(` source ${report.source}
|
|
33265
|
+
`);
|
|
33266
|
+
if (!verify)
|
|
33267
|
+
deps.stdout.write(`${UNVERIFIED_KEY_LINE}
|
|
32380
33268
|
`);
|
|
32381
33269
|
if (vault?.vaultDestination)
|
|
32382
33270
|
deps.stdout.write(` vault ${vault.vaultDestination}
|
|
@@ -32417,6 +33305,8 @@ var init_tee_status_evm = __esm(() => {
|
|
|
32417
33305
|
init_trading();
|
|
32418
33306
|
init_errors();
|
|
32419
33307
|
init_store();
|
|
33308
|
+
init_swap();
|
|
33309
|
+
init_tee_status_wallet();
|
|
32420
33310
|
init_vault_support();
|
|
32421
33311
|
});
|
|
32422
33312
|
|
|
@@ -33082,43 +33972,59 @@ async function readLifecycle(ctx, apiKey, linkedWalletId) {
|
|
|
33082
33972
|
}
|
|
33083
33973
|
async function teeStatus(args, ctx) {
|
|
33084
33974
|
const { deps, json } = ctx;
|
|
33085
|
-
|
|
33086
|
-
|
|
33087
|
-
|
|
33975
|
+
const parsed = parseArgs(args, {
|
|
33976
|
+
valueFlags: ["--rpc-url", "--keystore"],
|
|
33977
|
+
pathFlags: ["--keystore"],
|
|
33978
|
+
booleanFlags: ["--verify"]
|
|
33979
|
+
});
|
|
33088
33980
|
if ("error" in parsed)
|
|
33089
33981
|
return usage2(ctx, parsed.error);
|
|
33982
|
+
const verify = parsed.booleans.has("--verify");
|
|
33983
|
+
if (verify && !refuseEnvPassphrase2(ctx))
|
|
33984
|
+
return 1;
|
|
33090
33985
|
const [address, extra] = parsed.positionals;
|
|
33091
33986
|
if (!address || extra !== undefined)
|
|
33092
|
-
return usage2(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
|
|
33987
|
+
return usage2(ctx, "Usage: candle tee status <address> [--rpc-url <url>] [--verify]");
|
|
33093
33988
|
if (/^0x/i.test(address))
|
|
33094
33989
|
return await teeStatusEvm(ctx, parsed, address);
|
|
33095
33990
|
const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
|
|
33096
33991
|
if ("error" in solana)
|
|
33097
33992
|
return usage2(ctx, solana.error);
|
|
33098
|
-
|
|
33099
|
-
|
|
33100
|
-
|
|
33993
|
+
let local = null;
|
|
33994
|
+
let entry;
|
|
33995
|
+
if (verify) {
|
|
33996
|
+
const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
|
|
33997
|
+
if (!resolved.ok)
|
|
33998
|
+
return resolved.code;
|
|
33999
|
+
local = resolved.resolved;
|
|
34000
|
+
entry = local.source === "vault" ? local.legacyView : local.entry;
|
|
34001
|
+
} else {
|
|
34002
|
+
try {
|
|
34003
|
+
const { row } = await serverTeeWallet(ctx, address, "solana");
|
|
34004
|
+
entry = { address, label: row.label ?? "", linkedWalletId: row.id };
|
|
34005
|
+
} catch (error) {
|
|
34006
|
+
return tradingFailure(ctx, error);
|
|
34007
|
+
}
|
|
34008
|
+
}
|
|
33101
34009
|
try {
|
|
33102
|
-
if (
|
|
33103
|
-
const reconciled = await maybeReconcileVaultTee(ctx,
|
|
34010
|
+
if (local?.source === "vault") {
|
|
34011
|
+
const reconciled = await maybeReconcileVaultTee(ctx, local);
|
|
33104
34012
|
if (reconciled.code !== null)
|
|
33105
34013
|
return reconciled.code;
|
|
33106
34014
|
}
|
|
33107
|
-
const entry = resolved.resolved.source === "vault" ? resolved.resolved.legacyView : resolved.resolved.entry;
|
|
33108
34015
|
const report = {
|
|
33109
34016
|
address,
|
|
33110
34017
|
label: entry.label,
|
|
33111
|
-
source:
|
|
34018
|
+
source: local?.source ?? "server",
|
|
34019
|
+
verified: verify,
|
|
33112
34020
|
linkedWalletId: entry.linkedWalletId ?? null,
|
|
33113
|
-
vaultDestination: entry.tee?.vaultDestination ?? null,
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
34021
|
+
vaultDestination: verify ? entry.tee?.vaultDestination ?? null : null,
|
|
34022
|
+
vaultLifecycle: local?.source === "vault" ? local.entry.tee?.lifecycle ?? null : null,
|
|
34023
|
+
localState: !verify ? null : entry.tee?.sweptAt ? "swept" : entry.tee?.stopRequestedAt ? "stop-requested" : entry.linkedWalletId ? "enabled" : "local-only",
|
|
34024
|
+
retainedSweepReceipts: verify ? entry.tee?.sweepReceipts?.length ?? 0 : null,
|
|
34025
|
+
pendingSweepTransactions: verify ? entry.tee?.sweepPending?.length ?? 0 : null,
|
|
33117
34026
|
observedAt: new Date(deps.now()).toISOString()
|
|
33118
34027
|
};
|
|
33119
|
-
if (resolved.resolved.source === "vault") {
|
|
33120
|
-
report.vaultLifecycle = resolved.resolved.entry.tee?.lifecycle ?? null;
|
|
33121
|
-
}
|
|
33122
34028
|
if (entry.linkedWalletId) {
|
|
33123
34029
|
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
33124
34030
|
if (apiKey) {
|
|
@@ -33164,13 +34070,17 @@ async function teeStatus(args, ctx) {
|
|
|
33164
34070
|
}
|
|
33165
34071
|
deps.stdout.write(`${address} ${entry.label}
|
|
33166
34072
|
`);
|
|
33167
|
-
if (
|
|
34073
|
+
if (local?.source === "vault") {
|
|
33168
34074
|
deps.stdout.write(` source vault (Phase 1 store is read-only for this address)
|
|
33169
34075
|
`);
|
|
33170
|
-
deps.stdout.write(` vault life ${
|
|
34076
|
+
deps.stdout.write(` vault life ${local.entry.tee?.lifecycle ?? "?"}
|
|
33171
34077
|
`);
|
|
33172
34078
|
}
|
|
33173
|
-
|
|
34079
|
+
if (!verify)
|
|
34080
|
+
deps.stdout.write(`${UNVERIFIED_KEY_LINE}
|
|
34081
|
+
`);
|
|
34082
|
+
else
|
|
34083
|
+
deps.stdout.write(` local state ${report.localState}
|
|
33174
34084
|
`);
|
|
33175
34085
|
if (entry.tee?.vaultDestination)
|
|
33176
34086
|
deps.stdout.write(` vault ${entry.tee.vaultDestination}
|
|
@@ -33206,7 +34116,8 @@ async function teeStatus(args, ctx) {
|
|
|
33206
34116
|
`);
|
|
33207
34117
|
return 0;
|
|
33208
34118
|
} finally {
|
|
33209
|
-
|
|
34119
|
+
if (local)
|
|
34120
|
+
releaseResolvedTee(local);
|
|
33210
34121
|
}
|
|
33211
34122
|
}
|
|
33212
34123
|
async function teeDisable(args, ctx) {
|
|
@@ -34233,8 +35144,10 @@ var init_tee = __esm(() => {
|
|
|
34233
35144
|
init_wallet_import_flow();
|
|
34234
35145
|
init_wallet_keygen();
|
|
34235
35146
|
init_wallet_keystore();
|
|
35147
|
+
init_swap();
|
|
34236
35148
|
init_tee_evm();
|
|
34237
35149
|
init_tee_status_evm();
|
|
35150
|
+
init_tee_status_wallet();
|
|
34238
35151
|
init_wallets();
|
|
34239
35152
|
StoreChangedError = class StoreChangedError extends Error {
|
|
34240
35153
|
};
|
|
@@ -34400,7 +35313,7 @@ function missingSignerFailure(missing) {
|
|
|
34400
35313
|
message: `The target key has a signer, so the owner of ${missing.length === 1 ? "this wallet" : "these wallets"} must change with the binding, and this machine does not hold the current owner. Nothing changed.
|
|
34401
35314
|
${missing.map((m) => ` ${m.walletId}: on ${m.where}`).join(`
|
|
34402
35315
|
`)}`,
|
|
34403
|
-
suggestion: "Run the same rebind on the machine named for each wallet
|
|
35316
|
+
suggestion: "Run the same rebind on the machine named for each wallet. If that is a trading machine with no login, sign in there on a temporary profile for the move: https://docs.candle.tv/developers/cli-headless#moving-wallets-onto-a-key-that-has-a-signer"
|
|
34404
35317
|
};
|
|
34405
35318
|
}
|
|
34406
35319
|
function rebindFailureDetails(result, context) {
|
|
@@ -34750,7 +35663,7 @@ function effectiveEmbeddedWallet(value) {
|
|
|
34750
35663
|
return value === "denied" ? "denied" : "allowed";
|
|
34751
35664
|
}
|
|
34752
35665
|
function embeddedWalletLine(permission, keyPrefix) {
|
|
34753
|
-
return permission === "allowed" ? "Embedded wallet: allowed. This key may
|
|
35666
|
+
return permission === "allowed" ? "Embedded wallet: allowed. This key may use the account's embedded wallet, with Candle signing, for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write." : `Embedded wallet: denied. This key cannot spend the account's embedded wallet; the owner can allow it with: candle keys update ${keyPrefix} --embedded-wallet allow`;
|
|
34754
35667
|
}
|
|
34755
35668
|
function keyName(keyPrefix, label) {
|
|
34756
35669
|
const cleaned = labelCell(label ?? undefined);
|
|
@@ -34791,7 +35704,7 @@ async function confirmCreateAllow(ctx) {
|
|
|
34791
35704
|
writeLocalFailure(deps, ALLOW_REQUIRES_TTY, json);
|
|
34792
35705
|
return 1;
|
|
34793
35706
|
}
|
|
34794
|
-
deps.stderr.write(`This key will be allowed to
|
|
35707
|
+
deps.stderr.write(`This key will be allowed to use the account's embedded wallet, with Candle signing, for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write.
|
|
34795
35708
|
` + `A key that only trades its own TEE wallets does not need it.
|
|
34796
35709
|
`);
|
|
34797
35710
|
const answer = await deps.promptLine("Create the key with the embedded wallet allowed? [y/N] ");
|
|
@@ -34890,7 +35803,7 @@ async function keysUpdate(args, ctx) {
|
|
|
34890
35803
|
`API ${apiUrl} (${candleEnvironment(apiUrl) ?? "not a Candle host"})`,
|
|
34891
35804
|
"",
|
|
34892
35805
|
`Embedded wallet ${shown.from} -> ${shown.to} (${shown.direction})`,
|
|
34893
|
-
widen ? "It can
|
|
35806
|
+
widen ? "It can use the account's embedded wallet for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write." : "It stops spending the account's embedded wallet from its next request.",
|
|
34894
35807
|
"",
|
|
34895
35808
|
""
|
|
34896
35809
|
].join(`
|
|
@@ -39338,8 +40251,8 @@ var require_checkpoint = __commonJS((exports) => {
|
|
|
39338
40251
|
const split2 = envelope.indexOf(CHECKPOINT_SEPARATOR);
|
|
39339
40252
|
const header = envelope.slice(0, split2 + 1);
|
|
39340
40253
|
const data = envelope.slice(split2 + CHECKPOINT_SEPARATOR.length);
|
|
39341
|
-
const
|
|
39342
|
-
const signatures = Array.from(
|
|
40254
|
+
const matches2 = data.matchAll(SIGNATURE_REGEX);
|
|
40255
|
+
const signatures = Array.from(matches2, (match) => {
|
|
39343
40256
|
const [, name2, signature] = match;
|
|
39344
40257
|
const sigBytes = Buffer.from(signature, "base64");
|
|
39345
40258
|
if (sigBytes.length < 5) {
|
|
@@ -43413,11 +44326,11 @@ var require_validate2 = __commonJS((exports) => {
|
|
|
43413
44326
|
jsonPointer = $data;
|
|
43414
44327
|
data = names_1.default.rootData;
|
|
43415
44328
|
} else {
|
|
43416
|
-
const
|
|
43417
|
-
if (!
|
|
44329
|
+
const matches3 = RELATIVE_JSON_POINTER.exec($data);
|
|
44330
|
+
if (!matches3)
|
|
43418
44331
|
throw new Error(`Invalid JSON-pointer: ${$data}`);
|
|
43419
|
-
const up = +
|
|
43420
|
-
jsonPointer =
|
|
44332
|
+
const up = +matches3[1];
|
|
44333
|
+
jsonPointer = matches3[2];
|
|
43421
44334
|
if (jsonPointer === "#") {
|
|
43422
44335
|
if (up >= dataLevel)
|
|
43423
44336
|
throw new Error(errorMsg("property/index", up));
|
|
@@ -44094,11 +45007,11 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
44094
45007
|
urnComponent.error = "URN can not be parsed";
|
|
44095
45008
|
return urnComponent;
|
|
44096
45009
|
}
|
|
44097
|
-
const
|
|
44098
|
-
if (
|
|
45010
|
+
const matches3 = urnComponent.path.match(URN_REG);
|
|
45011
|
+
if (matches3) {
|
|
44099
45012
|
const scheme = options.scheme || urnComponent.scheme || "urn";
|
|
44100
|
-
urnComponent.nid =
|
|
44101
|
-
urnComponent.nss =
|
|
45013
|
+
urnComponent.nid = matches3[1].toLowerCase();
|
|
45014
|
+
urnComponent.nss = matches3[2];
|
|
44102
45015
|
const urnScheme = `${scheme}:${options.nid || urnComponent.nid}`;
|
|
44103
45016
|
const schemeHandler = getSchemeHandler(urnScheme);
|
|
44104
45017
|
urnComponent.path = undefined;
|
|
@@ -44344,8 +45257,8 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
44344
45257
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
44345
45258
|
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
44346
45259
|
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
44347
|
-
function getParseError(parsed,
|
|
44348
|
-
if (
|
|
45260
|
+
function getParseError(parsed, matches3) {
|
|
45261
|
+
if (matches3[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
|
|
44349
45262
|
return 'URI path must start with "/" when authority is present.';
|
|
44350
45263
|
}
|
|
44351
45264
|
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
@@ -44392,19 +45305,19 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
44392
45305
|
}
|
|
44393
45306
|
}
|
|
44394
45307
|
}
|
|
44395
|
-
const
|
|
44396
|
-
if (
|
|
44397
|
-
parsed.scheme =
|
|
44398
|
-
parsed.userinfo =
|
|
44399
|
-
parsed.host =
|
|
44400
|
-
parsed.port = parseInt(
|
|
44401
|
-
parsed.path =
|
|
44402
|
-
parsed.query =
|
|
44403
|
-
parsed.fragment =
|
|
45308
|
+
const matches3 = uri.match(URI_PARSE);
|
|
45309
|
+
if (matches3) {
|
|
45310
|
+
parsed.scheme = matches3[1];
|
|
45311
|
+
parsed.userinfo = matches3[3];
|
|
45312
|
+
parsed.host = matches3[4];
|
|
45313
|
+
parsed.port = parseInt(matches3[5], 10);
|
|
45314
|
+
parsed.path = matches3[6] || "";
|
|
45315
|
+
parsed.query = matches3[7];
|
|
45316
|
+
parsed.fragment = matches3[8];
|
|
44404
45317
|
if (isNaN(parsed.port)) {
|
|
44405
|
-
parsed.port =
|
|
45318
|
+
parsed.port = matches3[5];
|
|
44406
45319
|
}
|
|
44407
|
-
const parseError = getParseError(parsed,
|
|
45320
|
+
const parseError = getParseError(parsed, matches3);
|
|
44408
45321
|
if (parseError !== undefined) {
|
|
44409
45322
|
parsed.error = parsed.error || parseError;
|
|
44410
45323
|
malformedAuthorityOrPort = true;
|
|
@@ -47063,12 +47976,12 @@ var require_formats = __commonJS((exports) => {
|
|
|
47063
47976
|
var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
47064
47977
|
var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
47065
47978
|
function date(str) {
|
|
47066
|
-
const
|
|
47067
|
-
if (!
|
|
47979
|
+
const matches3 = DATE.exec(str);
|
|
47980
|
+
if (!matches3)
|
|
47068
47981
|
return false;
|
|
47069
|
-
const year = +
|
|
47070
|
-
const month = +
|
|
47071
|
-
const day = +
|
|
47982
|
+
const year = +matches3[1];
|
|
47983
|
+
const month = +matches3[2];
|
|
47984
|
+
const day = +matches3[3];
|
|
47072
47985
|
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]);
|
|
47073
47986
|
}
|
|
47074
47987
|
function compareDate(d1, d2) {
|
|
@@ -47083,16 +47996,16 @@ var require_formats = __commonJS((exports) => {
|
|
|
47083
47996
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
47084
47997
|
function getTime(strictTimeZone) {
|
|
47085
47998
|
return function time(str) {
|
|
47086
|
-
const
|
|
47087
|
-
if (!
|
|
47999
|
+
const matches3 = TIME.exec(str);
|
|
48000
|
+
if (!matches3)
|
|
47088
48001
|
return false;
|
|
47089
|
-
const hr = +
|
|
47090
|
-
const min = +
|
|
47091
|
-
const sec = +
|
|
47092
|
-
const tz =
|
|
47093
|
-
const tzSign =
|
|
47094
|
-
const tzH = +(
|
|
47095
|
-
const tzM = +(
|
|
48002
|
+
const hr = +matches3[1];
|
|
48003
|
+
const min = +matches3[2];
|
|
48004
|
+
const sec = +matches3[3];
|
|
48005
|
+
const tz = matches3[4];
|
|
48006
|
+
const tzSign = matches3[5] === "-" ? -1 : 1;
|
|
48007
|
+
const tzH = +(matches3[6] || 0);
|
|
48008
|
+
const tzM = +(matches3[7] || 0);
|
|
47096
48009
|
if (tzH > 23 || tzM > 59 || strictTimeZone && !tz)
|
|
47097
48010
|
return false;
|
|
47098
48011
|
if (hr <= 23 && min <= 59 && sec < 60)
|
|
@@ -50732,10 +51645,10 @@ async function walletFor(cfg, fetch2, name2, scope) {
|
|
|
50732
51645
|
throw new Refusal(fail3("SCOPE_MISSING", `The key needs ${scope}.`));
|
|
50733
51646
|
const rows = Array.isArray(listed.page) ? listed.page : [];
|
|
50734
51647
|
const evm = rows.filter((row) => row.chain === "evm");
|
|
50735
|
-
const
|
|
50736
|
-
if (
|
|
51648
|
+
const matches3 = name2 === undefined ? evm : evm.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
|
|
51649
|
+
if (matches3.length !== 1)
|
|
50737
51650
|
throw new Refusal(fail3("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it." : `Name exactly one EVM TEE wallet: ${evm.map((row) => row.label ?? row.id).join(", ")}`));
|
|
50738
|
-
return { row:
|
|
51651
|
+
return { row: matches3[0], appId: typeof listed.privyAppId === "string" ? listed.privyAppId : "" };
|
|
50739
51652
|
}
|
|
50740
51653
|
async function builderPin2(cfg, fetch2, env) {
|
|
50741
51654
|
const pinned = env.CANDLE_HYPERLIQUID_BUILDER?.trim() || CANDLE_HYPERLIQUID_BUILDER_ADDRESS2;
|
|
@@ -50869,10 +51782,10 @@ function rawUnits(amount, decimals) {
|
|
|
50869
51782
|
}
|
|
50870
51783
|
function pickWallet(rows, chain2, name2, what) {
|
|
50871
51784
|
const onChain = rows.filter((row) => row.chain === chain2);
|
|
50872
|
-
const
|
|
50873
|
-
if (
|
|
51785
|
+
const matches3 = name2 === undefined ? onChain : onChain.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
|
|
51786
|
+
if (matches3.length !== 1)
|
|
50874
51787
|
throw new Refusal(fail3("TEE_WALLET_REQUIRED", onChain.length === 0 ? `This key has no ${what} bound to it.` : `Name exactly one ${what}: ${onChain.map((row) => row.label ?? row.id).join(", ")}`));
|
|
50875
|
-
return
|
|
51788
|
+
return matches3[0];
|
|
50876
51789
|
}
|
|
50877
51790
|
function depositProblem(build, expect) {
|
|
50878
51791
|
const destination = build.destination ?? {};
|
|
@@ -51467,7 +52380,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
51467
52380
|
}, async (args) => callAndRelay("candle_get_profile_trades", args, cfg));
|
|
51468
52381
|
register("candle_get_portfolio", {
|
|
51469
52382
|
title: "Read what the account holds, on Solana and Hood",
|
|
51470
|
-
description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
|
|
52383
|
+
description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. `walletsComplete` is false only " + "when the wallet list itself was cut off, and is absent on a server that predates it: then " + "`complete: false` may be a cut-off list or a failed read. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
|
|
51471
52384
|
inputSchema: {}
|
|
51472
52385
|
}, async () => callAndRelay("candle_get_portfolio", {}, cfg));
|
|
51473
52386
|
register("candle_resolve_token", {
|
|
@@ -51857,7 +52770,7 @@ init_render();
|
|
|
51857
52770
|
init_secret_store();
|
|
51858
52771
|
|
|
51859
52772
|
// src/version.ts
|
|
51860
|
-
var CLI_VERSION = "0.11.
|
|
52773
|
+
var CLI_VERSION = "0.11.16";
|
|
51861
52774
|
|
|
51862
52775
|
// src/commands/auth.ts
|
|
51863
52776
|
init_keys_embedded_wallet();
|
|
@@ -52084,7 +52997,7 @@ async function authLogout(args, ctx) {
|
|
|
52084
52997
|
const keepKey = parsed.booleans.has("--keep-key");
|
|
52085
52998
|
await printIdentity(ctx);
|
|
52086
52999
|
const config = await deps.readConfig();
|
|
52087
|
-
const { keyPrefix, portalOrigin } = effectiveProfileFields(config, ctx.profile);
|
|
53000
|
+
const { keyPrefix, portalOrigin, deviceTokenPrefix, label } = effectiveProfileFields(config, ctx.profile);
|
|
52088
53001
|
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
52089
53002
|
let revokedKey;
|
|
52090
53003
|
if (!keepKey && deviceToken && keyPrefix) {
|
|
@@ -52120,12 +53033,18 @@ async function authLogout(args, ctx) {
|
|
|
52120
53033
|
} else {
|
|
52121
53034
|
await deps.store.delete(SECRET_REFS.deviceToken);
|
|
52122
53035
|
await deps.store.delete(SECRET_REFS.apiKey);
|
|
52123
|
-
|
|
53036
|
+
const patch = Object.fromEntries(PRE_PROFILE_FIELDS.map((field) => [field, undefined]));
|
|
53037
|
+
const remaining = { ...config, ...patch };
|
|
53038
|
+
if (Object.values(remaining).some((value) => value !== undefined))
|
|
53039
|
+
await deps.writeConfig(patch);
|
|
53040
|
+
else
|
|
53041
|
+
await deps.clearConfig();
|
|
52124
53042
|
}
|
|
52125
|
-
const portalUrl =
|
|
53043
|
+
const portalUrl = portalDevicesUrl(apiUrl, portalOrigin);
|
|
53044
|
+
const device = deviceTokenPrefix ? { prefix: deviceTokenPrefix, label: label ?? null } : null;
|
|
52126
53045
|
const liveEnvOverrides = credentialEnvOverrides(deps.env);
|
|
52127
53046
|
if (json) {
|
|
52128
|
-
deps.stdout.write(`${JSON.stringify({ success: true, revokedKey: revokedKey ?? null, portalUrl, envOverrides: liveEnvOverrides })}
|
|
53047
|
+
deps.stdout.write(`${JSON.stringify({ success: true, revokedKey: revokedKey ?? null, portalUrl, device, envOverrides: liveEnvOverrides })}
|
|
52129
53048
|
`);
|
|
52130
53049
|
return 0;
|
|
52131
53050
|
}
|
|
@@ -52137,7 +53056,11 @@ async function authLogout(args, ctx) {
|
|
|
52137
53056
|
}
|
|
52138
53057
|
deps.stdout.write(`The device token itself is session-only to revoke -- that is intentional (a stolen token cannot read device metadata or revoke a sibling device). Sign in to the portal to revoke it there.
|
|
52139
53058
|
`);
|
|
52140
|
-
|
|
53059
|
+
if (device)
|
|
53060
|
+
deps.stdout.write(`Device ${device.prefix}${device.label ? ` (${device.label})` : ""} is still live on the server. Revoke it under Connected devices: ${portalUrl}
|
|
53061
|
+
`);
|
|
53062
|
+
else
|
|
53063
|
+
deps.stdout.write(`Connected devices: ${portalUrl}
|
|
52141
53064
|
`);
|
|
52142
53065
|
return 0;
|
|
52143
53066
|
}
|
|
@@ -52543,9 +53466,14 @@ var HELP = {
|
|
|
52543
53466
|
portfolio: {
|
|
52544
53467
|
group: "Account",
|
|
52545
53468
|
summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
|
|
52546
|
-
description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade.",
|
|
52547
|
-
usage: ["candle portfolio [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
|
|
52548
|
-
rows: [
|
|
53469
|
+
description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade. With a filter, only the wallets whose label or address contains it are read and shown, the subtotals and total are for those wallets, and the first line says how many matched. When Candle listed only part of the account's wallets, the result stays partial even if every wallet shown was read, and no match says so.",
|
|
53470
|
+
usage: ["candle portfolio [<filter>] [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
|
|
53471
|
+
rows: [
|
|
53472
|
+
{
|
|
53473
|
+
invocation: "<filter>",
|
|
53474
|
+
description: "Keep the wallets whose label or address contains this text, in every group. Vault keys it leaves out are never sent to your RPC"
|
|
53475
|
+
}
|
|
53476
|
+
],
|
|
52549
53477
|
flags: [
|
|
52550
53478
|
{ invocation: "--chain solana|hood", description: "Show one chain, and read nothing on the other" },
|
|
52551
53479
|
{
|
|
@@ -52561,6 +53489,7 @@ var HELP = {
|
|
|
52561
53489
|
examples: [
|
|
52562
53490
|
"candle portfolio",
|
|
52563
53491
|
"candle portfolio --chain hood",
|
|
53492
|
+
"candle portfolio p-",
|
|
52564
53493
|
"candle portfolio --rpc-url https://your-rpc.example --evm-rpc-url https://your-hood-rpc.example --json"
|
|
52565
53494
|
],
|
|
52566
53495
|
env: [...ENV_API, ...ENV_LOCAL_SIGNING]
|
|
@@ -52859,8 +53788,8 @@ var HELP = {
|
|
|
52859
53788
|
description: "Print the funding instruction for your vault to sign"
|
|
52860
53789
|
},
|
|
52861
53790
|
{
|
|
52862
|
-
invocation: "status <address> [--rpc-url <url>]",
|
|
52863
|
-
description: "Server lifecycle state and on-chain balances;
|
|
53791
|
+
invocation: "status <address> [--rpc-url <url>] [--verify]",
|
|
53792
|
+
description: "Server lifecycle state and on-chain balances without unlocking; --verify opens the vault and checks the key. On Hood, ETH, USDG, the gas reserve and gas: low"
|
|
52864
53793
|
},
|
|
52865
53794
|
{
|
|
52866
53795
|
invocation: "disable <address>",
|
|
@@ -52877,7 +53806,7 @@ var HELP = {
|
|
|
52877
53806
|
{ invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" },
|
|
52878
53807
|
{
|
|
52879
53808
|
invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
|
|
52880
|
-
description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an
|
|
53809
|
+
description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
|
|
52881
53810
|
}
|
|
52882
53811
|
],
|
|
52883
53812
|
flags: [KEYSTORE_FLAG],
|
|
@@ -53943,7 +54872,7 @@ init_tee_rebind();
|
|
|
53943
54872
|
var USAGE_TEE_SIGNER = "Usage: candle tee signer new --key <prefix|label> [--out <pem>] [--force] [--json]";
|
|
53944
54873
|
var USAGE_KEYS_SIGNER = "Usage: candle keys signer approve <code> --key <prefix|label> [--reject] | candle keys signer move <prefix|label> [--to-key <prefix|label>] [--json]";
|
|
53945
54874
|
var SIGNER_POLL_MS = 5000;
|
|
53946
|
-
var DEVICE_TOKEN_BESIDE_SIGNER_LINE = "Warning: this machine holds a device token as well as a key signer. A machine with both can approve its own signer request. Do not
|
|
54875
|
+
var DEVICE_TOKEN_BESIDE_SIGNER_LINE = "Warning: this machine holds a device token as well as a key signer. A machine with both can approve its own signer request. Do not leave the trading machine logged in. If you signed in for a move, run candle auth logout --profile <that profile> here, and approve from the owner's machine.";
|
|
53947
54876
|
var unsupported = {
|
|
53948
54877
|
code: "KEY_SIGNER_UNSUPPORTED",
|
|
53949
54878
|
message: "This Candle API does not serve key signers yet; nothing changed.",
|
|
@@ -54026,6 +54955,7 @@ async function teeSignerNew(args, ctx) {
|
|
|
54026
54955
|
await writeOut(ctx, out, recorded);
|
|
54027
54956
|
return reportApproved(ctx, view, recorded, { resumed: true, requested: false });
|
|
54028
54957
|
}
|
|
54958
|
+
let replaces = null;
|
|
54029
54959
|
let resumed = entry !== undefined;
|
|
54030
54960
|
if (entry === undefined) {
|
|
54031
54961
|
const all = allSignerWallets(view);
|
|
@@ -54040,6 +54970,11 @@ async function teeSignerNew(args, ctx) {
|
|
|
54040
54970
|
}, json);
|
|
54041
54971
|
return 1;
|
|
54042
54972
|
}
|
|
54973
|
+
if (active !== null) {
|
|
54974
|
+
replaces = { fingerprint: active.fingerprint, wallets: view.wallets.onSigner.length };
|
|
54975
|
+
deps.stderr.write(`Key ${keyPrefix} already has active signer ${replaces.fingerprint}. Approving this request replaces it. ${replaces.wallets > 0 ? `Its ${plural(replaces.wallets, "wallet")} keep trading only from the machine that holds it until candle keys signer move ${keyPrefix} runs there.` : "It owns no wallets, so nothing has to move."}
|
|
54976
|
+
`);
|
|
54977
|
+
}
|
|
54043
54978
|
const pair = generatePair();
|
|
54044
54979
|
const spkiSha256 = spkiSha256Of(pair.publicKeyDer);
|
|
54045
54980
|
const ref = keySignerRef(keyPrefix, spkiSha256);
|
|
@@ -54069,7 +55004,8 @@ async function teeSignerNew(args, ctx) {
|
|
|
54069
55004
|
const reread = await readSigner(ctx, "self", { apiKey });
|
|
54070
55005
|
return reportApproved(ctx, reread.ok ? reread.body : view, recorded, {
|
|
54071
55006
|
resumed,
|
|
54072
|
-
requested: true
|
|
55007
|
+
requested: true,
|
|
55008
|
+
replaces
|
|
54073
55009
|
});
|
|
54074
55010
|
}
|
|
54075
55011
|
}
|
|
@@ -54080,6 +55016,9 @@ async function teeSignerNew(args, ctx) {
|
|
|
54080
55016
|
return 1;
|
|
54081
55017
|
}
|
|
54082
55018
|
const body = requested.body;
|
|
55019
|
+
if (body.supersededPending)
|
|
55020
|
+
deps.stderr.write(`An earlier pending request for this key was cancelled.
|
|
55021
|
+
`);
|
|
54083
55022
|
if (body.spkiSha256 !== entry.spkiSha256) {
|
|
54084
55023
|
writeLocalFailure(deps, {
|
|
54085
55024
|
code: "KEY_SIGNER_CHANGED",
|
|
@@ -54109,7 +55048,7 @@ async function teeSignerNew(args, ctx) {
|
|
|
54109
55048
|
if (now !== null && now.spkiSha256 === entry.spkiSha256) {
|
|
54110
55049
|
const recorded = { ...entry, signerQuorumId: now.signerQuorumId };
|
|
54111
55050
|
await saveKeySignerEntry(deps, recorded);
|
|
54112
|
-
return reportApproved(ctx, view, recorded, { resumed, requested: true });
|
|
55051
|
+
return reportApproved(ctx, view, recorded, { resumed, requested: true, replaces });
|
|
54113
55052
|
}
|
|
54114
55053
|
if (view.pending === null || view.pending.spkiSha256 !== entry.spkiSha256) {
|
|
54115
55054
|
writeLocalFailure(deps, {
|
|
@@ -54153,6 +55092,7 @@ function reportApproved(ctx, view, entry, how) {
|
|
|
54153
55092
|
signerQuorumId: entry.signerQuorumId,
|
|
54154
55093
|
resumed: how.resumed,
|
|
54155
55094
|
requested: how.requested,
|
|
55095
|
+
replaces: how.replaces ?? null,
|
|
54156
55096
|
wallets: {
|
|
54157
55097
|
onSigner: onSigner.map((w) => w.id),
|
|
54158
55098
|
legacy: legacy.map((w) => w.id),
|
|
@@ -54261,6 +55201,7 @@ async function keysSignerApprove(args, ctx) {
|
|
|
54261
55201
|
"",
|
|
54262
55202
|
"Requested from a machine using this key. Compare the fingerprint with what that machine printed.",
|
|
54263
55203
|
`Wallets on this key: ${all.length}.`,
|
|
55204
|
+
...active !== null ? [`Approving replaces signer ${active.fingerprint} as this key's active signer.`] : [],
|
|
54264
55205
|
...active !== null && onActive > 0 ? [
|
|
54265
55206
|
`${plural(onActive, "wallet")} ${onActive === 1 ? "is" : "are"} owned by signer ${active.fingerprint}. They keep trading from the machine that holds that signer until you move them.`
|
|
54266
55207
|
] : [],
|
|
@@ -58254,1491 +59195,662 @@ async function externalList(args, ctx) {
|
|
|
58254
59195
|
return 0;
|
|
58255
59196
|
}
|
|
58256
59197
|
for (const row of rows) {
|
|
58257
|
-
deps.stdout.write(`${row.address}
|
|
58258
|
-
`);
|
|
58259
|
-
deps.stdout.write(` label ${row.label || "(none)"}
|
|
58260
|
-
`);
|
|
58261
|
-
if (row.derivation)
|
|
58262
|
-
deps.stdout.write(` derivation ${row.derivation}
|
|
58263
|
-
`);
|
|
58264
|
-
if (row.exposureUnknown)
|
|
58265
|
-
deps.stdout.write(` history unknown (restored from the phrase)
|
|
58266
|
-
`);
|
|
58267
|
-
}
|
|
58268
|
-
deps.stdout.write(`External wallets are never delegated to Privy and never registered with Candle. Balances are read over your own RPC; Candle is not in their transactions.
|
|
58269
|
-
`);
|
|
58270
|
-
return 0;
|
|
58271
|
-
});
|
|
58272
|
-
}
|
|
58273
|
-
async function externalSweep(args, ctx) {
|
|
58274
|
-
const parsed = parseArgs(args, {
|
|
58275
|
-
valueFlags: ["--to", "--rpc-url", "--keystore"],
|
|
58276
|
-
booleanFlags: ["--accept-older-copy"],
|
|
58277
|
-
pathFlags: ["--keystore"]
|
|
58278
|
-
});
|
|
58279
|
-
if ("error" in parsed)
|
|
58280
|
-
return usage(ctx, parsed.error);
|
|
58281
|
-
const [source, extra] = parsed.positionals;
|
|
58282
|
-
if (!source || extra !== undefined) {
|
|
58283
|
-
return usage(ctx, "Usage: candle external sweep <external> --to <vault> [--rpc-url <url>]");
|
|
58284
|
-
}
|
|
58285
|
-
const to = parsed.values["--to"];
|
|
58286
|
-
if (!to)
|
|
58287
|
-
return usage(ctx, "--to <vault> is required: the vault receive key everything is sent to (never inferred).");
|
|
58288
|
-
const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
|
|
58289
|
-
if ("error" in solana)
|
|
58290
|
-
return usage(ctx, solana.error);
|
|
58291
|
-
if (!refuseEnvPassphrase(ctx))
|
|
58292
|
-
return 1;
|
|
58293
|
-
if (!requireTty(ctx, "external sweep"))
|
|
58294
|
-
return 1;
|
|
58295
|
-
const { deps } = ctx;
|
|
58296
|
-
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
58297
|
-
if ("error" in resolvedVault)
|
|
58298
|
-
return usage(ctx, resolvedVault.error);
|
|
58299
|
-
const path = resolvedVault.path;
|
|
58300
|
-
return runVaultCommand(ctx, async ({ hold }) => {
|
|
58301
|
-
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
58302
|
-
const opened = await unlockInteractively(ctx, path, raw, {
|
|
58303
|
-
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
58304
|
-
});
|
|
58305
|
-
const vault = hold(opened.vault);
|
|
58306
|
-
assertNotEvmEntry(vault.index, source, "external sweep");
|
|
58307
|
-
assertNotEvmEntry(vault.index, to, "external sweep");
|
|
58308
|
-
const sourceEntry = requireExternalEntry(vault.index, source);
|
|
58309
|
-
const destination = findVaultRoleEntry(vault.index, to);
|
|
58310
|
-
if (destination === undefined || destination.role !== "vault") {
|
|
58311
|
-
const other = vault.index.entries.find((candidate) => candidate.label === to || candidate.address === to);
|
|
58312
|
-
throw new VaultError("VAULT_INDEX_INVALID", other === undefined ? `No vault key matches --to ${to}.` : `--to ${to} is ${describeRole(other)}; a sweep goes to a role:vault receive key only.`, { suggestion: "Name a vault key by label or address: candle vault status --unlock", exitCode: 2 });
|
|
58313
|
-
}
|
|
58314
|
-
if (destination.address === sourceEntry.address) {
|
|
58315
|
-
return usage(ctx, "The source and the destination are the same address.");
|
|
58316
|
-
}
|
|
58317
|
-
if (!ctx.json) {
|
|
58318
|
-
deps.stdout.write(`Sweep everything from external wallet:
|
|
58319
|
-
`);
|
|
58320
|
-
deps.stdout.write(` source ${sourceEntry.label} ${sourceEntry.address}
|
|
58321
|
-
`);
|
|
58322
|
-
deps.stdout.write(`to vault receive key:
|
|
58323
|
-
`);
|
|
58324
|
-
deps.stdout.write(` destination ${destination.label} ${destination.address}
|
|
58325
|
-
`);
|
|
58326
|
-
deps.stdout.write(`SOL, classic SPL and Token-2022 balances move, signed locally with the external key.
|
|
58327
|
-
`);
|
|
58328
|
-
}
|
|
58329
|
-
await confirmLastSix(ctx, destination.address, "the vault destination");
|
|
58330
|
-
await opened.confirm(`sweep ${sourceEntry.label} to ${destination.address}`);
|
|
58331
|
-
const secret = await decryptKey(vault, sourceEntry.id);
|
|
58332
|
-
try {
|
|
58333
|
-
const outcome = await sweepEverythingTo({
|
|
58334
|
-
rpc: solana.rpc,
|
|
58335
|
-
ctx,
|
|
58336
|
-
secret64: secret,
|
|
58337
|
-
owner: sourceEntry.address,
|
|
58338
|
-
destination: destination.address,
|
|
58339
|
-
say: ctx.json ? () => {} : (line) => deps.stdout.write(`${line}
|
|
58340
|
-
`)
|
|
58341
|
-
});
|
|
58342
|
-
const complete = outcome.leftovers.length === 0;
|
|
58343
|
-
if (ctx.json) {
|
|
58344
|
-
writeJson(deps, {
|
|
58345
|
-
ok: complete,
|
|
58346
|
-
source: sourceEntry.address,
|
|
58347
|
-
sourceLabel: sourceEntry.label,
|
|
58348
|
-
destination: destination.address,
|
|
58349
|
-
receipts: outcome.receipts,
|
|
58350
|
-
leftovers: outcome.leftovers
|
|
58351
|
-
});
|
|
58352
|
-
return complete ? 0 : 3;
|
|
58353
|
-
}
|
|
58354
|
-
if (outcome.receipts.length === 0 && outcome.leftovers.length === 0) {
|
|
58355
|
-
deps.stdout.write(`Nothing to sweep: no balances found.
|
|
58356
|
-
`);
|
|
58357
|
-
return 0;
|
|
58358
|
-
}
|
|
58359
|
-
if (complete) {
|
|
58360
|
-
deps.stdout.write(`Swept: ${outcome.receipts.length} transaction(s) finalized.
|
|
58361
|
-
`);
|
|
58362
|
-
return 0;
|
|
58363
|
-
}
|
|
58364
|
-
deps.stdout.write(`Sweep incomplete: ${outcome.leftovers.length} leftover(s).
|
|
58365
|
-
`);
|
|
58366
|
-
for (const l of outcome.leftovers) {
|
|
58367
|
-
deps.stdout.write(` - ${l.kind}${l.mint ? ` ${l.mint}` : ""}${l.amountRaw ? ` (${l.amountRaw} raw)` : ""}: ${l.detail}
|
|
58368
|
-
`);
|
|
58369
|
-
}
|
|
58370
|
-
deps.stdout.write(`Re-run this sweep once the leftovers are resolved.
|
|
58371
|
-
`);
|
|
58372
|
-
return 3;
|
|
58373
|
-
} finally {
|
|
58374
|
-
wipe(secret);
|
|
58375
|
-
}
|
|
58376
|
-
});
|
|
58377
|
-
}
|
|
58378
|
-
|
|
58379
|
-
// src/commands/help.ts
|
|
58380
|
-
async function help(args, ctx) {
|
|
58381
|
-
const word2 = args[0];
|
|
58382
|
-
if (word2 === undefined) {
|
|
58383
|
-
ctx.deps.stdout.write(renderTopLevel());
|
|
58384
|
-
return 0;
|
|
58385
|
-
}
|
|
58386
|
-
const topic = renderTopic(word2);
|
|
58387
|
-
if (topic === undefined) {
|
|
58388
|
-
ctx.deps.stderr.write(`Unknown command: ${word2}
|
|
58389
|
-
`);
|
|
58390
|
-
ctx.deps.stderr.write(renderTopLevel());
|
|
58391
|
-
return 1;
|
|
58392
|
-
}
|
|
58393
|
-
ctx.deps.stdout.write(topic);
|
|
58394
|
-
return 0;
|
|
58395
|
-
}
|
|
58396
|
-
|
|
58397
|
-
// src/index.ts
|
|
58398
|
-
init_keys();
|
|
58399
|
-
|
|
58400
|
-
// src/commands/keys-access.ts
|
|
58401
|
-
init_agent_key_access();
|
|
58402
|
-
init_args();
|
|
58403
|
-
init_deps();
|
|
58404
|
-
init_profiles();
|
|
58405
|
-
init_render();
|
|
58406
|
-
init_promote_support();
|
|
58407
|
-
init_keys();
|
|
58408
|
-
init_tee_rebind();
|
|
58409
|
-
var KEYS_PATH4 = "/api/v1/agent/keys";
|
|
58410
|
-
var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|read-write|read-write-transfer> [--yes] [--json]
|
|
58411
|
-
` + " candle keys access <prefix|label> --history [--json]";
|
|
58412
|
-
var SELF = "self";
|
|
58413
|
-
var CLI_SPELLING = Object.fromEntries(Object.entries(ACCESS_LEVELS).map(([spelling, preset]) => [preset, spelling]));
|
|
58414
|
-
var NO_API_KEY2 = {
|
|
58415
|
-
code: "API_KEY_REQUIRED",
|
|
58416
|
-
message: "candle keys access self changes the profile's own API key, and this profile holds none.",
|
|
58417
|
-
suggestion: "Select the profile holding the key with --profile, or name the key by prefix to use the device token."
|
|
58418
|
-
};
|
|
58419
|
-
var REQUIRES_TTY = {
|
|
58420
|
-
code: "KEY_ACCESS_REQUIRES_TTY",
|
|
58421
|
-
message: "Widening a key needs a terminal: the key prefix is typed back, and nothing else supplies it.",
|
|
58422
|
-
suggestion: "Run it in an interactive shell; there is no flag and no environment variable to widen a key."
|
|
58423
|
-
};
|
|
58424
|
-
function levelName(side) {
|
|
58425
|
-
const preset = side.access ?? presetForScopes(side.scopes);
|
|
58426
|
-
if (preset)
|
|
58427
|
-
return AGENT_KEY_PRESET_LABELS[preset];
|
|
58428
|
-
return `custom (${accessCell(side.scopes)})`;
|
|
58429
|
-
}
|
|
58430
|
-
function keyName2(keyPrefix, label) {
|
|
58431
|
-
const cleaned = labelCell(label ?? undefined);
|
|
58432
|
-
return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
|
|
58433
|
-
}
|
|
58434
|
-
function listWords(words) {
|
|
58435
|
-
return words.length <= 1 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
|
|
58436
|
-
}
|
|
58437
|
-
function linkedTransferLine(ready) {
|
|
58438
|
-
const assets = Object.entries(ready.assets);
|
|
58439
|
-
const missing = assets.filter(([, ok]) => !ok).map(([asset]) => asset.toUpperCase());
|
|
58440
|
-
const readyAssets = assets.filter(([, ok]) => ok).map(([asset]) => asset.toUpperCase());
|
|
58441
|
-
const needs = [];
|
|
58442
|
-
if (!ready.txLimit)
|
|
58443
|
-
needs.push("a USD transaction limit");
|
|
58444
|
-
if (missing.length > 0) {
|
|
58445
|
-
needs.push(`${missing.map((asset) => `a ${asset}`).join(" and ")} spend cap`);
|
|
58446
|
-
}
|
|
58447
|
-
if (needs.length === 0)
|
|
58448
|
-
return null;
|
|
58449
|
-
const readyPart = readyAssets.length > 0 ? `; ${listWords(readyAssets)} ${readyAssets.length === 1 ? "is" : "are"} ready` : "";
|
|
58450
|
-
return [
|
|
58451
|
-
`Warning: to send to linked wallets it also needs ${needs.join(" and ")}${readyPart}.`,
|
|
58452
|
-
" Set them in the web key manager: Agents, Keys, this key, Spend limits."
|
|
58453
|
-
].join(`
|
|
58454
|
-
`);
|
|
58455
|
-
}
|
|
58456
|
-
var LOCALLY_RENDERED = new Set(["MOVES_FUNDS", "VAULT_TRANSFER_READY", "LINKED_TRANSFER_NEEDS_LIMITS"]);
|
|
58457
|
-
function accessScreen(shown, identity) {
|
|
58458
|
-
const environment = candleEnvironment(identity.apiUrl) ?? "not a Candle host";
|
|
58459
|
-
const label = labelCell(shown.label ?? undefined);
|
|
58460
|
-
const lines = [
|
|
58461
|
-
`Key ${shown.keyPrefix}${label ? ` ${label}` : ""}`,
|
|
58462
|
-
`Candle account ${identity.account}`,
|
|
58463
|
-
`API ${identity.apiUrl} (${environment})`,
|
|
58464
|
-
"",
|
|
58465
|
-
`Access ${levelName(shown.from)} -> ${levelName(shown.to)} (${shown.direction})`,
|
|
58466
|
-
`Adds ${shown.added.length > 0 ? formatScopesForSummary(shown.added) : "nothing"}`,
|
|
58467
|
-
`Removes ${shown.removed.length > 0 ? shown.removed.join(", ") : "nothing"}`,
|
|
58468
|
-
""
|
|
58469
|
-
];
|
|
58470
|
-
const bound = shown.effects.boundTeeWallets;
|
|
58471
|
-
if (bound && bound.count > 0) {
|
|
58472
|
-
const names = bound.sample.map((w) => labelCell(w.label ?? undefined) || shortAddress(w.address));
|
|
58473
|
-
const more = bound.count > names.length ? `, and ${bound.count - names.length} more` : "";
|
|
58474
|
-
lines.push(`TEE wallets on this key: ${bound.count} (${names.join(", ")}${more})`);
|
|
58475
|
-
if (shown.direction === "widen" && shown.added.includes("transfer:bound")) {
|
|
58476
|
-
lines.push("It can move these wallets' funds to their vaults as soon as you confirm.");
|
|
58477
|
-
}
|
|
58478
|
-
}
|
|
58479
|
-
const ready = shown.effects.linkedTransferReady;
|
|
58480
|
-
if (shown.direction === "widen" && ready) {
|
|
58481
|
-
const line = linkedTransferLine(ready);
|
|
58482
|
-
if (line)
|
|
58483
|
-
lines.push(line);
|
|
58484
|
-
}
|
|
58485
|
-
for (const warning of shown.warnings) {
|
|
58486
|
-
if (!LOCALLY_RENDERED.has(warning.code))
|
|
58487
|
-
lines.push(`Warning: ${warning.message}`);
|
|
58488
|
-
}
|
|
58489
|
-
if (lines[lines.length - 1] !== "")
|
|
58490
|
-
lines.push("");
|
|
58491
|
-
return lines.join(`
|
|
58492
|
-
`);
|
|
58493
|
-
}
|
|
58494
|
-
function accessFailureDetails(result, context) {
|
|
58495
|
-
let code = result.code;
|
|
58496
|
-
let message = result.message;
|
|
58497
|
-
let suggestion;
|
|
58498
|
-
if (result.status === 404 && result.code === undefined) {
|
|
58499
|
-
code = "KEY_ACCESS_UNSUPPORTED";
|
|
58500
|
-
message = context.history ? "This Candle API has no key access history yet." : "This Candle API cannot change a key's access yet; nothing changed.";
|
|
58501
|
-
} else if (result.code === "LOOSEN_REQUIRES_SESSION" && context.self) {
|
|
58502
|
-
const level = context.preset ? CLI_SPELLING[context.preset] : "<level>";
|
|
58503
|
-
suggestion = `Widen it from a signed-in session or with the device token: candle keys access <prefix> --access ${level}`;
|
|
58504
|
-
} else if (result.code === "KEY_ACCESS_STALE") {
|
|
58505
|
-
suggestion = "Run the command again; the preview will show the key's current access.";
|
|
58506
|
-
} else if (result.code === "KEY_ACCESS_PARTNER_KEY") {
|
|
58507
|
-
suggestion = "Partner keys are managed by Candle; ask your Candle contact to change one.";
|
|
58508
|
-
} else if (result.status === 404 && result.code === "VALIDATION_FAILED") {
|
|
58509
|
-
suggestion = "Run: candle keys list, to see this account's keys and their prefixes.";
|
|
58510
|
-
}
|
|
58511
|
-
return { code: code ?? `HTTP ${result.status}`, message, ...suggestion !== undefined ? { suggestion } : {} };
|
|
58512
|
-
}
|
|
58513
|
-
function writeAccessFailure(ctx, result, context) {
|
|
58514
|
-
const { deps, apiUrl, json } = ctx;
|
|
58515
|
-
const { code, message, suggestion } = accessFailureDetails(result, context);
|
|
58516
|
-
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: context.self ? "key" : "device" });
|
|
58517
|
-
if (json) {
|
|
58518
|
-
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
58519
|
-
`);
|
|
58520
|
-
} else {
|
|
58521
|
-
deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
58522
|
-
`);
|
|
58523
|
-
}
|
|
58524
|
-
return 1;
|
|
58525
|
-
}
|
|
58526
|
-
async function keysAccess(args, ctx) {
|
|
58527
|
-
const { deps, apiUrl, json } = ctx;
|
|
58528
|
-
const parsed = parseArgs(args, { valueFlags: ["--access"], booleanFlags: ["--yes", "--history"] });
|
|
58529
|
-
if ("error" in parsed) {
|
|
58530
|
-
writeUsageFailure(deps, parsed.error, json);
|
|
58531
|
-
return 2;
|
|
58532
|
-
}
|
|
58533
|
-
const [target, extra] = parsed.positionals;
|
|
58534
|
-
if (target === undefined || extra !== undefined) {
|
|
58535
|
-
writeUsageFailure(deps, `${extra !== undefined ? `Unexpected argument: ${extra}. ` : "Name one key. "}${USAGE_ACCESS}`, json);
|
|
58536
|
-
return 2;
|
|
58537
|
-
}
|
|
58538
|
-
const accessFlag = parsed.values["--access"];
|
|
58539
|
-
const history = parsed.booleans.has("--history");
|
|
58540
|
-
const yes = parsed.booleans.has("--yes");
|
|
58541
|
-
if (accessFlag === undefined === !history) {
|
|
58542
|
-
writeUsageFailure(deps, `Pass exactly one of --access and --history. ${USAGE_ACCESS}`, json);
|
|
58543
|
-
return 2;
|
|
58544
|
-
}
|
|
58545
|
-
if (history) {
|
|
58546
|
-
if (yes) {
|
|
58547
|
-
writeUsageFailure(deps, "--yes only applies to a change, not to --history.", json);
|
|
58548
|
-
return 2;
|
|
58549
|
-
}
|
|
58550
|
-
if (target === SELF) {
|
|
58551
|
-
writeUsageFailure(deps, "--history needs the device token: name the key by prefix or label, not self.", json);
|
|
58552
|
-
return 2;
|
|
58553
|
-
}
|
|
58554
|
-
return keysAccessHistory(target, ctx);
|
|
58555
|
-
}
|
|
58556
|
-
const preset = ACCESS_LEVELS[accessFlag];
|
|
58557
|
-
if (preset === undefined) {
|
|
58558
|
-
writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
|
|
58559
|
-
return 2;
|
|
58560
|
-
}
|
|
58561
|
-
await printIdentity(ctx);
|
|
58562
|
-
const self = target === SELF;
|
|
58563
|
-
let path;
|
|
58564
|
-
let auth;
|
|
58565
|
-
let credentials;
|
|
58566
|
-
if (self) {
|
|
58567
|
-
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
58568
|
-
if (!apiKey) {
|
|
58569
|
-
writeLocalFailure(deps, NO_API_KEY2, json);
|
|
58570
|
-
return 1;
|
|
58571
|
-
}
|
|
58572
|
-
path = `${KEYS_PATH4}/self/access`;
|
|
58573
|
-
auth = "key";
|
|
58574
|
-
credentials = { apiKey };
|
|
58575
|
-
} else {
|
|
58576
|
-
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
58577
|
-
if (!deviceToken) {
|
|
58578
|
-
writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
|
|
58579
|
-
return 1;
|
|
58580
|
-
}
|
|
58581
|
-
const resolved = await resolveTargetKey(ctx, deviceToken, target, {
|
|
58582
|
-
codePrefix: "KEY_ACCESS",
|
|
58583
|
-
writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false })
|
|
58584
|
-
});
|
|
58585
|
-
if (!resolved.ok)
|
|
58586
|
-
return resolved.code;
|
|
58587
|
-
path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
|
|
58588
|
-
auth = "device";
|
|
58589
|
-
credentials = { deviceToken };
|
|
58590
|
-
}
|
|
58591
|
-
const failureContext = { self, preset };
|
|
58592
|
-
const call = (body) => apiRequest(path, { method: "PUT", body, auth, credentials, apiUrl, fetch: deps.fetch, env: deps.env });
|
|
58593
|
-
const preview = await call({ access: preset, dryRun: true });
|
|
58594
|
-
if (!preview.ok)
|
|
58595
|
-
return writeAccessFailure(ctx, preview, failureContext);
|
|
58596
|
-
const shown = preview.body;
|
|
58597
|
-
const name2 = keyName2(shown.keyPrefix, shown.label);
|
|
58598
|
-
if (shown.direction === "unchanged") {
|
|
58599
|
-
if (json)
|
|
58600
|
-
deps.stdout.write(`${JSON.stringify({ ...shown, command: "keys access" })}
|
|
58601
|
-
`);
|
|
58602
|
-
else
|
|
58603
|
-
deps.stdout.write(`${name2} is already ${levelName(shown.to)}. Nothing changed.
|
|
58604
|
-
`);
|
|
58605
|
-
return 0;
|
|
58606
|
-
}
|
|
58607
|
-
const widen = shown.direction === "widen";
|
|
58608
|
-
const interactive = deps.isTTY.stdin && deps.isTTY.stdout;
|
|
58609
|
-
if (widen && yes) {
|
|
58610
|
-
writeUsageFailure(deps, "--yes only skips the prompt when narrowing; nothing changed.", json);
|
|
58611
|
-
return 2;
|
|
58612
|
-
}
|
|
58613
|
-
if (widen && !interactive) {
|
|
58614
|
-
writeLocalFailure(deps, REQUIRES_TTY, json);
|
|
58615
|
-
return 1;
|
|
58616
|
-
}
|
|
58617
|
-
if (!widen && !yes && !interactive) {
|
|
58618
|
-
writeUsageFailure(deps, "Narrowing a key without a terminal needs --yes; nothing changed. Run it in an interactive shell, or pass --yes.", json);
|
|
58619
|
-
return 2;
|
|
58620
|
-
}
|
|
58621
|
-
const config = await deps.readConfig();
|
|
58622
|
-
const fields = effectiveProfileFields(config, ctx.profile);
|
|
58623
|
-
const account = fields.account ? shortAddress(fields.account) : "unknown";
|
|
58624
|
-
deps.stderr.write(`${accessScreen(shown, { account: fields.username ? `${fields.username} (${account})` : account, apiUrl })}
|
|
58625
|
-
`);
|
|
58626
|
-
if (widen) {
|
|
58627
|
-
const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
|
|
58628
|
-
if (typed.trim() !== shown.keyPrefix) {
|
|
58629
|
-
writeLocalFailure(deps, {
|
|
58630
|
-
code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
|
|
58631
|
-
message: `The acknowledgement is the key prefix ${shown.keyPrefix}; nothing changed.`,
|
|
58632
|
-
suggestion: `Run the command again and type ${shown.keyPrefix} at the prompt.`
|
|
58633
|
-
}, json);
|
|
58634
|
-
return 1;
|
|
58635
|
-
}
|
|
58636
|
-
} else if (!yes) {
|
|
58637
|
-
const answer = await deps.promptLine(`Change ${name2} from ${levelName(shown.from)} to ${levelName(shown.to)}? [y/N] `);
|
|
58638
|
-
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
|
|
58639
|
-
writeLocalFailure(deps, {
|
|
58640
|
-
code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
|
|
58641
|
-
message: "Not confirmed; nothing changed.",
|
|
58642
|
-
suggestion: "Run the command again and answer y, or pass --yes."
|
|
58643
|
-
}, json);
|
|
58644
|
-
return 1;
|
|
58645
|
-
}
|
|
58646
|
-
}
|
|
58647
|
-
const committed = await call({ access: preset, expectScopes: shown.from.scopes });
|
|
58648
|
-
if (!committed.ok)
|
|
58649
|
-
return writeAccessFailure(ctx, committed, failureContext);
|
|
58650
|
-
const result = committed.body;
|
|
58651
|
-
if (fields.keyPrefix !== undefined && fields.keyPrefix === result.keyPrefix && result.direction !== "unchanged") {
|
|
58652
|
-
const scopes = scopesForPreset(result.to.access);
|
|
58653
|
-
if (ctx.profile)
|
|
58654
|
-
await deps.updateProfile(ctx.profile, { scopes });
|
|
58655
|
-
else
|
|
58656
|
-
await deps.writeConfig({ scopes });
|
|
58657
|
-
}
|
|
58658
|
-
if (json) {
|
|
58659
|
-
deps.stdout.write(`${JSON.stringify({ ...result, command: "keys access" })}
|
|
58660
|
-
`);
|
|
58661
|
-
return 0;
|
|
58662
|
-
}
|
|
58663
|
-
if (result.direction === "unchanged") {
|
|
58664
|
-
deps.stdout.write(`${keyName2(result.keyPrefix, result.label)} is already ${levelName(result.to)}. Nothing changed.
|
|
58665
|
-
`);
|
|
58666
|
-
return 0;
|
|
58667
|
-
}
|
|
58668
|
-
deps.stdout.write(`Changed ${keyName2(result.keyPrefix, result.label)} to ${levelName(result.to)}.` + `${result.changeId ? ` Change id ${result.changeId}` : ""}
|
|
58669
|
-
`);
|
|
58670
|
-
return 0;
|
|
58671
|
-
}
|
|
58672
|
-
async function keysAccessHistory(target, ctx) {
|
|
58673
|
-
const { deps, apiUrl, json } = ctx;
|
|
58674
|
-
await printIdentity(ctx);
|
|
58675
|
-
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
58676
|
-
if (!deviceToken) {
|
|
58677
|
-
writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
|
|
58678
|
-
return 1;
|
|
58679
|
-
}
|
|
58680
|
-
const resolved = await resolveTargetKey(ctx, deviceToken, target, {
|
|
58681
|
-
codePrefix: "KEY_ACCESS",
|
|
58682
|
-
writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false, history: true })
|
|
58683
|
-
});
|
|
58684
|
-
if (!resolved.ok)
|
|
58685
|
-
return resolved.code;
|
|
58686
|
-
const result = await apiRequest(`${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access-changes`, {
|
|
58687
|
-
auth: "device",
|
|
58688
|
-
credentials: { deviceToken },
|
|
58689
|
-
apiUrl,
|
|
58690
|
-
fetch: deps.fetch,
|
|
58691
|
-
env: deps.env
|
|
58692
|
-
});
|
|
58693
|
-
if (!result.ok)
|
|
58694
|
-
return writeAccessFailure(ctx, result, { self: false, history: true });
|
|
58695
|
-
if (json) {
|
|
58696
|
-
deps.stdout.write(`${JSON.stringify({ ...result.body, command: "keys access" })}
|
|
59198
|
+
deps.stdout.write(`${row.address}
|
|
58697
59199
|
`);
|
|
58698
|
-
|
|
58699
|
-
}
|
|
58700
|
-
const rows = result.body.changes ?? [];
|
|
58701
|
-
if (rows.length === 0) {
|
|
58702
|
-
deps.stdout.write(`No access changes on key ${resolved.keyPrefix}.
|
|
59200
|
+
deps.stdout.write(` label ${row.label || "(none)"}
|
|
58703
59201
|
`);
|
|
58704
|
-
|
|
58705
|
-
}
|
|
58706
|
-
deps.stdout.write(`${renderTable(["Time", "From", "To", "Actor", "Device or key"], rows.map((row) => [
|
|
58707
|
-
formatTimestamp(row.at),
|
|
58708
|
-
levelName(row.from),
|
|
58709
|
-
levelName(row.to),
|
|
58710
|
-
row.actor,
|
|
58711
|
-
row.actorDevicePrefix ?? row.actorKeyPrefix ?? ""
|
|
58712
|
-
]))}
|
|
59202
|
+
if (row.derivation)
|
|
59203
|
+
deps.stdout.write(` derivation ${row.derivation}
|
|
58713
59204
|
`);
|
|
58714
|
-
|
|
58715
|
-
|
|
59205
|
+
if (row.exposureUnknown)
|
|
59206
|
+
deps.stdout.write(` history unknown (restored from the phrase)
|
|
58716
59207
|
`);
|
|
58717
|
-
|
|
58718
|
-
|
|
58719
|
-
|
|
58720
|
-
|
|
58721
|
-
|
|
58722
|
-
|
|
58723
|
-
// src/commands/keys-wallets.ts
|
|
58724
|
-
init_args();
|
|
58725
|
-
init_deps();
|
|
58726
|
-
init_profiles();
|
|
58727
|
-
init_render();
|
|
58728
|
-
var NO_API_KEY3 = {
|
|
58729
|
-
code: "NO_API_KEY",
|
|
58730
|
-
message: "No API key for this profile.",
|
|
58731
|
-
suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
|
|
58732
|
-
};
|
|
58733
|
-
function widenRefusedHint(prefix, walletIds) {
|
|
58734
|
-
const wallets2 = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
|
|
58735
|
-
return `To move TEE wallets to this key, run: candle tee rebind ${wallets2} --to-key ${prefix} ` + "(owner, device token; --label-prefix <p> names many at once). To grant a linked wallet to the key " + "instead, use the agent console's Agents tab in a signed-in session. If `tee rebind` says the wallets " + "are already bound, grant them to the key in the agent console's Agents tab (signed in).";
|
|
58736
|
-
}
|
|
58737
|
-
function formatTimestamp2(ms) {
|
|
58738
|
-
return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
|
|
59208
|
+
}
|
|
59209
|
+
deps.stdout.write(`External wallets are never delegated to Privy and never registered with Candle. Balances are read over your own RPC; Candle is not in their transactions.
|
|
59210
|
+
`);
|
|
59211
|
+
return 0;
|
|
59212
|
+
});
|
|
58739
59213
|
}
|
|
58740
|
-
async function
|
|
58741
|
-
const
|
|
58742
|
-
|
|
58743
|
-
|
|
58744
|
-
|
|
58745
|
-
|
|
58746
|
-
|
|
58747
|
-
|
|
58748
|
-
|
|
58749
|
-
|
|
58750
|
-
return
|
|
59214
|
+
async function externalSweep(args, ctx) {
|
|
59215
|
+
const parsed = parseArgs(args, {
|
|
59216
|
+
valueFlags: ["--to", "--rpc-url", "--keystore"],
|
|
59217
|
+
booleanFlags: ["--accept-older-copy"],
|
|
59218
|
+
pathFlags: ["--keystore"]
|
|
59219
|
+
});
|
|
59220
|
+
if ("error" in parsed)
|
|
59221
|
+
return usage(ctx, parsed.error);
|
|
59222
|
+
const [source, extra] = parsed.positionals;
|
|
59223
|
+
if (!source || extra !== undefined) {
|
|
59224
|
+
return usage(ctx, "Usage: candle external sweep <external> --to <vault> [--rpc-url <url>]");
|
|
58751
59225
|
}
|
|
58752
|
-
|
|
58753
|
-
|
|
58754
|
-
|
|
58755
|
-
|
|
59226
|
+
const to = parsed.values["--to"];
|
|
59227
|
+
if (!to)
|
|
59228
|
+
return usage(ctx, "--to <vault> is required: the vault receive key everything is sent to (never inferred).");
|
|
59229
|
+
const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
|
|
59230
|
+
if ("error" in solana)
|
|
59231
|
+
return usage(ctx, solana.error);
|
|
59232
|
+
if (!refuseEnvPassphrase(ctx))
|
|
58756
59233
|
return 1;
|
|
58757
|
-
|
|
58758
|
-
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
|
|
58759
|
-
auth: "key",
|
|
58760
|
-
credentials: { apiKey },
|
|
58761
|
-
apiUrl,
|
|
58762
|
-
fetch: deps.fetch,
|
|
58763
|
-
env: deps.env
|
|
58764
|
-
});
|
|
58765
|
-
if (!result.ok) {
|
|
58766
|
-
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
59234
|
+
if (!requireTty(ctx, "external sweep"))
|
|
58767
59235
|
return 1;
|
|
58768
|
-
}
|
|
58769
|
-
|
|
58770
|
-
|
|
59236
|
+
const { deps } = ctx;
|
|
59237
|
+
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
59238
|
+
if ("error" in resolvedVault)
|
|
59239
|
+
return usage(ctx, resolvedVault.error);
|
|
59240
|
+
const path = resolvedVault.path;
|
|
59241
|
+
return runVaultCommand(ctx, async ({ hold }) => {
|
|
59242
|
+
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
59243
|
+
const opened = await unlockInteractively(ctx, path, raw, {
|
|
59244
|
+
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
59245
|
+
});
|
|
59246
|
+
const vault = hold(opened.vault);
|
|
59247
|
+
assertNotEvmEntry(vault.index, source, "external sweep");
|
|
59248
|
+
assertNotEvmEntry(vault.index, to, "external sweep");
|
|
59249
|
+
const sourceEntry = requireExternalEntry(vault.index, source);
|
|
59250
|
+
const destination = findVaultRoleEntry(vault.index, to);
|
|
59251
|
+
if (destination === undefined || destination.role !== "vault") {
|
|
59252
|
+
const other = vault.index.entries.find((candidate) => candidate.label === to || candidate.address === to);
|
|
59253
|
+
throw new VaultError("VAULT_INDEX_INVALID", other === undefined ? `No vault key matches --to ${to}.` : `--to ${to} is ${describeRole(other)}; a sweep goes to a role:vault receive key only.`, { suggestion: "Name a vault key by label or address: candle vault status --unlock", exitCode: 2 });
|
|
59254
|
+
}
|
|
59255
|
+
if (destination.address === sourceEntry.address) {
|
|
59256
|
+
return usage(ctx, "The source and the destination are the same address.");
|
|
59257
|
+
}
|
|
59258
|
+
if (!ctx.json) {
|
|
59259
|
+
deps.stdout.write(`Sweep everything from external wallet:
|
|
58771
59260
|
`);
|
|
58772
|
-
|
|
58773
|
-
}
|
|
58774
|
-
const body = result.body;
|
|
58775
|
-
deps.stdout.write(body.walletScope === "selected" ? `Scope: selected — this profile can only spend from the wallets below.
|
|
58776
|
-
` : `Scope: all — this profile can spend from every wallet on the account, listed here or not.
|
|
59261
|
+
deps.stdout.write(` source ${sourceEntry.label} ${sourceEntry.address}
|
|
58777
59262
|
`);
|
|
58778
|
-
|
|
58779
|
-
deps.stdout.write(`Profile: ${body.profileId}
|
|
59263
|
+
deps.stdout.write(`to vault receive key:
|
|
58780
59264
|
`);
|
|
58781
|
-
|
|
58782
|
-
deps.stdout.write(`No wallets assigned.
|
|
59265
|
+
deps.stdout.write(` destination ${destination.label} ${destination.address}
|
|
58783
59266
|
`);
|
|
58784
|
-
|
|
58785
|
-
}
|
|
58786
|
-
const rows = body.wallets.map((w) => [
|
|
58787
|
-
w.linkedWalletId,
|
|
58788
|
-
w.chain,
|
|
58789
|
-
w.address,
|
|
58790
|
-
w.label ?? "-",
|
|
58791
|
-
w.spendCapable ? "yes" : "no",
|
|
58792
|
-
formatTimestamp2(w.assignedAt)
|
|
58793
|
-
]);
|
|
58794
|
-
deps.stdout.write(`${renderTable(["Id", "Chain", "Address", "Label", "Can sign", "Assigned"], rows)}
|
|
59267
|
+
deps.stdout.write(`SOL, classic SPL and Token-2022 balances move, signed locally with the external key.
|
|
58795
59268
|
`);
|
|
58796
|
-
return 0;
|
|
58797
|
-
}
|
|
58798
|
-
async function keysWalletsSet(args, ctx) {
|
|
58799
|
-
const { deps, apiUrl, json } = ctx;
|
|
58800
|
-
const parsed = parseArgs(args, { valueFlags: ["--wallets"] });
|
|
58801
|
-
if ("error" in parsed) {
|
|
58802
|
-
writeUsageFailure(deps, parsed.error, json);
|
|
58803
|
-
return 2;
|
|
58804
|
-
}
|
|
58805
|
-
const prefix = parsed.positionals[0];
|
|
58806
|
-
if (!prefix) {
|
|
58807
|
-
writeUsageFailure(deps, "Usage: candle keys wallets set <prefix> --wallets <id,id,...>", json);
|
|
58808
|
-
return 2;
|
|
58809
|
-
}
|
|
58810
|
-
const raw = parsed.values["--wallets"];
|
|
58811
|
-
if (raw === undefined) {
|
|
58812
|
-
writeUsageFailure(deps, 'Missing --wallets. Pass a comma-separated list, or "" to assign none.', json);
|
|
58813
|
-
return 2;
|
|
58814
|
-
}
|
|
58815
|
-
const walletIds = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
58816
|
-
await printIdentity(ctx);
|
|
58817
|
-
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
58818
|
-
if (!apiKey) {
|
|
58819
|
-
writeLocalFailure(deps, NO_API_KEY3, json);
|
|
58820
|
-
return 1;
|
|
58821
|
-
}
|
|
58822
|
-
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
|
|
58823
|
-
method: "PUT",
|
|
58824
|
-
body: { walletIds },
|
|
58825
|
-
auth: "key",
|
|
58826
|
-
credentials: { apiKey },
|
|
58827
|
-
apiUrl,
|
|
58828
|
-
fetch: deps.fetch,
|
|
58829
|
-
env: deps.env
|
|
58830
|
-
});
|
|
58831
|
-
if (!result.ok) {
|
|
58832
|
-
if (result.code !== "LOOSEN_REQUIRES_SESSION") {
|
|
58833
|
-
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
58834
|
-
return 1;
|
|
58835
59269
|
}
|
|
58836
|
-
|
|
58837
|
-
|
|
58838
|
-
|
|
58839
|
-
|
|
59270
|
+
await confirmLastSix(ctx, destination.address, "the vault destination");
|
|
59271
|
+
await opened.confirm(`sweep ${sourceEntry.label} to ${destination.address}`);
|
|
59272
|
+
const secret = await decryptKey(vault, sourceEntry.id);
|
|
59273
|
+
try {
|
|
59274
|
+
const outcome = await sweepEverythingTo({
|
|
59275
|
+
rpc: solana.rpc,
|
|
59276
|
+
ctx,
|
|
59277
|
+
secret64: secret,
|
|
59278
|
+
owner: sourceEntry.address,
|
|
59279
|
+
destination: destination.address,
|
|
59280
|
+
say: ctx.json ? () => {} : (line) => deps.stdout.write(`${line}
|
|
59281
|
+
`)
|
|
59282
|
+
});
|
|
59283
|
+
const complete = outcome.leftovers.length === 0;
|
|
59284
|
+
if (ctx.json) {
|
|
59285
|
+
writeJson(deps, {
|
|
59286
|
+
ok: complete,
|
|
59287
|
+
source: sourceEntry.address,
|
|
59288
|
+
sourceLabel: sourceEntry.label,
|
|
59289
|
+
destination: destination.address,
|
|
59290
|
+
receipts: outcome.receipts,
|
|
59291
|
+
leftovers: outcome.leftovers
|
|
59292
|
+
});
|
|
59293
|
+
return complete ? 0 : 3;
|
|
59294
|
+
}
|
|
59295
|
+
if (outcome.receipts.length === 0 && outcome.leftovers.length === 0) {
|
|
59296
|
+
deps.stdout.write(`Nothing to sweep: no balances found.
|
|
58840
59297
|
`);
|
|
58841
|
-
|
|
58842
|
-
|
|
58843
|
-
|
|
58844
|
-
|
|
59298
|
+
return 0;
|
|
59299
|
+
}
|
|
59300
|
+
if (complete) {
|
|
59301
|
+
deps.stdout.write(`Swept: ${outcome.receipts.length} transaction(s) finalized.
|
|
58845
59302
|
`);
|
|
58846
|
-
|
|
58847
|
-
|
|
58848
|
-
|
|
58849
|
-
` : `Assigned ${walletIds.length} wallet${walletIds.length === 1 ? "" : "s"} to ${prefix}.
|
|
59303
|
+
return 0;
|
|
59304
|
+
}
|
|
59305
|
+
deps.stdout.write(`Sweep incomplete: ${outcome.leftovers.length} leftover(s).
|
|
58850
59306
|
`);
|
|
58851
|
-
|
|
58852
|
-
}
|
|
58853
|
-
|
|
58854
|
-
|
|
58855
|
-
|
|
58856
|
-
if ("error" in parsed) {
|
|
58857
|
-
writeUsageFailure(deps, parsed.error, json);
|
|
58858
|
-
return 2;
|
|
58859
|
-
}
|
|
58860
|
-
const prefix = parsed.positionals[0];
|
|
58861
|
-
const scope = parsed.values["--scope"];
|
|
58862
|
-
if (!prefix || scope !== "all" && scope !== "selected") {
|
|
58863
|
-
writeUsageFailure(deps, "Usage: candle keys wallets scope <prefix> --scope <all|selected>", json);
|
|
58864
|
-
return 2;
|
|
58865
|
-
}
|
|
58866
|
-
await printIdentity(ctx);
|
|
58867
|
-
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
58868
|
-
if (!apiKey) {
|
|
58869
|
-
writeLocalFailure(deps, NO_API_KEY3, json);
|
|
58870
|
-
return 1;
|
|
58871
|
-
}
|
|
58872
|
-
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallet-scope`, {
|
|
58873
|
-
method: "PUT",
|
|
58874
|
-
body: { scope },
|
|
58875
|
-
auth: "key",
|
|
58876
|
-
credentials: { apiKey },
|
|
58877
|
-
apiUrl,
|
|
58878
|
-
fetch: deps.fetch,
|
|
58879
|
-
env: deps.env
|
|
58880
|
-
});
|
|
58881
|
-
if (!result.ok) {
|
|
58882
|
-
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
58883
|
-
return 1;
|
|
58884
|
-
}
|
|
58885
|
-
if (json) {
|
|
58886
|
-
deps.stdout.write(`${JSON.stringify(result.body)}
|
|
59307
|
+
for (const l of outcome.leftovers) {
|
|
59308
|
+
deps.stdout.write(` - ${l.kind}${l.mint ? ` ${l.mint}` : ""}${l.amountRaw ? ` (${l.amountRaw} raw)` : ""}: ${l.detail}
|
|
59309
|
+
`);
|
|
59310
|
+
}
|
|
59311
|
+
deps.stdout.write(`Re-run this sweep once the leftovers are resolved.
|
|
58887
59312
|
`);
|
|
59313
|
+
return 3;
|
|
59314
|
+
} finally {
|
|
59315
|
+
wipe(secret);
|
|
59316
|
+
}
|
|
59317
|
+
});
|
|
59318
|
+
}
|
|
59319
|
+
|
|
59320
|
+
// src/commands/help.ts
|
|
59321
|
+
async function help(args, ctx) {
|
|
59322
|
+
const word2 = args[0];
|
|
59323
|
+
if (word2 === undefined) {
|
|
59324
|
+
ctx.deps.stdout.write(renderTopLevel());
|
|
58888
59325
|
return 0;
|
|
58889
59326
|
}
|
|
58890
|
-
|
|
58891
|
-
|
|
59327
|
+
const topic = renderTopic(word2);
|
|
59328
|
+
if (topic === undefined) {
|
|
59329
|
+
ctx.deps.stderr.write(`Unknown command: ${word2}
|
|
58892
59330
|
`);
|
|
59331
|
+
ctx.deps.stderr.write(renderTopLevel());
|
|
59332
|
+
return 1;
|
|
59333
|
+
}
|
|
59334
|
+
ctx.deps.stdout.write(topic);
|
|
58893
59335
|
return 0;
|
|
58894
59336
|
}
|
|
58895
|
-
async function keysWallets(args, ctx) {
|
|
58896
|
-
const [verb, ...rest] = args;
|
|
58897
|
-
if (verb === "set")
|
|
58898
|
-
return keysWalletsSet(rest, ctx);
|
|
58899
|
-
if (verb === "scope")
|
|
58900
|
-
return keysWalletsScope(rest, ctx);
|
|
58901
|
-
return keysWalletsList(args, ctx);
|
|
58902
|
-
}
|
|
58903
59337
|
|
|
58904
|
-
// src/
|
|
58905
|
-
|
|
58906
|
-
init_evm_lite();
|
|
58907
|
-
init_render();
|
|
58908
|
-
init_solana_endpoint();
|
|
58909
|
-
init_solana_lite();
|
|
58910
|
-
init_trading();
|
|
58911
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
59338
|
+
// src/index.ts
|
|
59339
|
+
init_keys();
|
|
58912
59340
|
|
|
58913
|
-
// src/commands/
|
|
58914
|
-
|
|
59341
|
+
// src/commands/keys-access.ts
|
|
59342
|
+
init_agent_key_access();
|
|
58915
59343
|
init_args();
|
|
58916
|
-
|
|
58917
|
-
|
|
59344
|
+
init_deps();
|
|
59345
|
+
init_profiles();
|
|
58918
59346
|
init_render();
|
|
58919
|
-
|
|
58920
|
-
|
|
58921
|
-
|
|
58922
|
-
|
|
58923
|
-
|
|
58924
|
-
|
|
58925
|
-
|
|
58926
|
-
|
|
58927
|
-
|
|
58928
|
-
|
|
58929
|
-
|
|
58930
|
-
|
|
58931
|
-
|
|
58932
|
-
|
|
58933
|
-
|
|
58934
|
-
|
|
58935
|
-
|
|
58936
|
-
|
|
58937
|
-
|
|
58938
|
-
|
|
58939
|
-
|
|
58940
|
-
return
|
|
58941
|
-
}
|
|
59347
|
+
init_promote_support();
|
|
59348
|
+
init_keys();
|
|
59349
|
+
init_tee_rebind();
|
|
59350
|
+
var KEYS_PATH4 = "/api/v1/agent/keys";
|
|
59351
|
+
var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|read-write|read-write-transfer> [--yes] [--json]
|
|
59352
|
+
` + " candle keys access <prefix|label> --history [--json]";
|
|
59353
|
+
var SELF = "self";
|
|
59354
|
+
var CLI_SPELLING = Object.fromEntries(Object.entries(ACCESS_LEVELS).map(([spelling, preset]) => [preset, spelling]));
|
|
59355
|
+
var NO_API_KEY2 = {
|
|
59356
|
+
code: "API_KEY_REQUIRED",
|
|
59357
|
+
message: "candle keys access self changes the profile's own API key, and this profile holds none.",
|
|
59358
|
+
suggestion: "Select the profile holding the key with --profile, or name the key by prefix to use the device token."
|
|
59359
|
+
};
|
|
59360
|
+
var REQUIRES_TTY = {
|
|
59361
|
+
code: "KEY_ACCESS_REQUIRES_TTY",
|
|
59362
|
+
message: "Widening a key needs a terminal: the key prefix is typed back, and nothing else supplies it.",
|
|
59363
|
+
suggestion: "Run it in an interactive shell; there is no flag and no environment variable to widen a key."
|
|
59364
|
+
};
|
|
59365
|
+
function levelName(side) {
|
|
59366
|
+
const preset = side.access ?? presetForScopes(side.scopes);
|
|
59367
|
+
if (preset)
|
|
59368
|
+
return AGENT_KEY_PRESET_LABELS[preset];
|
|
59369
|
+
return `custom (${accessCell(side.scopes)})`;
|
|
58942
59370
|
}
|
|
58943
|
-
|
|
58944
|
-
|
|
58945
|
-
|
|
58946
|
-
} catch (error) {
|
|
58947
|
-
if (isRateLimited(error))
|
|
58948
|
-
throw tradingRateLimited(ctx, client.endpoint.host, error);
|
|
58949
|
-
throw error;
|
|
58950
|
-
}
|
|
59371
|
+
function keyName2(keyPrefix, label) {
|
|
59372
|
+
const cleaned = labelCell(label ?? undefined);
|
|
59373
|
+
return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
|
|
58951
59374
|
}
|
|
58952
|
-
function
|
|
58953
|
-
|
|
58954
|
-
` : `${JSON.stringify(result, null, 2)}
|
|
58955
|
-
`);
|
|
58956
|
-
return 0;
|
|
59375
|
+
function listWords(words) {
|
|
59376
|
+
return words.length <= 1 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
|
|
58957
59377
|
}
|
|
58958
|
-
|
|
58959
|
-
|
|
58960
|
-
|
|
58961
|
-
|
|
58962
|
-
|
|
58963
|
-
|
|
58964
|
-
|
|
58965
|
-
if (
|
|
58966
|
-
|
|
58967
|
-
`);
|
|
58968
|
-
else {
|
|
58969
|
-
const state = !reading ? "not measured" : reading.state === "settled" ? "settled, amount not measured" : reading.state;
|
|
58970
|
-
ctx.deps.stderr.write(`Settlement: ${state}. Re-check with GET /api/v1/agent/swap/jobs/${id} (candle swap status ${id}).
|
|
58971
|
-
`);
|
|
59378
|
+
function linkedTransferLine(ready) {
|
|
59379
|
+
const assets = Object.entries(ready.assets);
|
|
59380
|
+
const missing = assets.filter(([, ok]) => !ok).map(([asset]) => asset.toUpperCase());
|
|
59381
|
+
const readyAssets = assets.filter(([, ok]) => ok).map(([asset]) => asset.toUpperCase());
|
|
59382
|
+
const needs = [];
|
|
59383
|
+
if (!ready.txLimit)
|
|
59384
|
+
needs.push("a USD transaction limit");
|
|
59385
|
+
if (missing.length > 0) {
|
|
59386
|
+
needs.push(`${missing.map((asset) => `a ${asset}`).join(" and ")} spend cap`);
|
|
58972
59387
|
}
|
|
58973
|
-
|
|
58974
|
-
|
|
58975
|
-
|
|
58976
|
-
return
|
|
59388
|
+
if (needs.length === 0)
|
|
59389
|
+
return null;
|
|
59390
|
+
const readyPart = readyAssets.length > 0 ? `; ${listWords(readyAssets)} ${readyAssets.length === 1 ? "is" : "are"} ready` : "";
|
|
59391
|
+
return [
|
|
59392
|
+
`Warning: to send to linked wallets it also needs ${needs.join(" and ")}${readyPart}.`,
|
|
59393
|
+
" Set them in the web key manager: Agents, Keys, this key, Spend limits."
|
|
59394
|
+
].join(`
|
|
59395
|
+
`);
|
|
58977
59396
|
}
|
|
58978
|
-
|
|
58979
|
-
|
|
58980
|
-
const
|
|
58981
|
-
const
|
|
58982
|
-
|
|
58983
|
-
|
|
58984
|
-
|
|
58985
|
-
|
|
58986
|
-
|
|
58987
|
-
|
|
58988
|
-
|
|
58989
|
-
|
|
58990
|
-
|
|
58991
|
-
|
|
59397
|
+
var LOCALLY_RENDERED = new Set(["MOVES_FUNDS", "VAULT_TRANSFER_READY", "LINKED_TRANSFER_NEEDS_LIMITS"]);
|
|
59398
|
+
function accessScreen(shown, identity) {
|
|
59399
|
+
const environment = candleEnvironment(identity.apiUrl) ?? "not a Candle host";
|
|
59400
|
+
const label = labelCell(shown.label ?? undefined);
|
|
59401
|
+
const lines = [
|
|
59402
|
+
`Key ${shown.keyPrefix}${label ? ` ${label}` : ""}`,
|
|
59403
|
+
`Candle account ${identity.account}`,
|
|
59404
|
+
`API ${identity.apiUrl} (${environment})`,
|
|
59405
|
+
"",
|
|
59406
|
+
`Access ${levelName(shown.from)} -> ${levelName(shown.to)} (${shown.direction})`,
|
|
59407
|
+
`Adds ${shown.added.length > 0 ? formatScopesForSummary(shown.added) : "nothing"}`,
|
|
59408
|
+
`Removes ${shown.removed.length > 0 ? shown.removed.join(", ") : "nothing"}`,
|
|
59409
|
+
""
|
|
59410
|
+
];
|
|
59411
|
+
const bound = shown.effects.boundTeeWallets;
|
|
59412
|
+
if (bound && bound.count > 0) {
|
|
59413
|
+
const names = bound.sample.map((w) => labelCell(w.label ?? undefined) || shortAddress(w.address));
|
|
59414
|
+
const more = bound.count > names.length ? `, and ${bound.count - names.length} more` : "";
|
|
59415
|
+
lines.push(`TEE wallets on this key: ${bound.count} (${names.join(", ")}${more})`);
|
|
59416
|
+
if (shown.direction === "widen" && shown.added.includes("transfer:bound")) {
|
|
59417
|
+
lines.push("It can move these wallets' funds to their vaults as soon as you confirm.");
|
|
58992
59418
|
}
|
|
58993
59419
|
}
|
|
58994
|
-
|
|
58995
|
-
|
|
58996
|
-
|
|
58997
|
-
|
|
58998
|
-
|
|
58999
|
-
const parsed = parseArgs(args, { valueFlags: ["--kind"], booleanFlags: ["--wait"] });
|
|
59000
|
-
if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "") || parsed.values["--kind"] !== undefined && !["trade", "swap", "launch"].includes(parsed.values["--kind"])) {
|
|
59001
|
-
writeUsageFailure(ctx.deps, "Usage: candle swap status <id> [--kind trade|swap|launch] [--wait]", ctx.json);
|
|
59002
|
-
return 2;
|
|
59003
|
-
}
|
|
59004
|
-
try {
|
|
59005
|
-
const key = await tradingKey(ctx);
|
|
59006
|
-
const id = parsed.positionals[0];
|
|
59007
|
-
const kind = parsed.values["--kind"];
|
|
59008
|
-
const facts = bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge);
|
|
59009
|
-
const found = await lookupOperation(ctx, key, id, kind);
|
|
59010
|
-
if (!found)
|
|
59011
|
-
throw new TradingError("JOB_NOT_FOUND", "No operation found on the selected rail(s). This command does not resend a write.");
|
|
59012
|
-
const result = parsed.booleans.has("--wait") ? await waitForSettlement(ctx, key, id, found, facts) : { ...found, ...bridgeStatusField(ctx, found, facts) };
|
|
59013
|
-
return printTradingResult(ctx, result);
|
|
59014
|
-
} catch (error) {
|
|
59015
|
-
return tradingFailure(ctx, error);
|
|
59016
|
-
}
|
|
59017
|
-
}
|
|
59018
|
-
var bridgeFactsSchema = exports_external.object({
|
|
59019
|
-
from: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
|
|
59020
|
-
to: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
|
|
59021
|
-
recipient: exports_external.string(),
|
|
59022
|
-
statusCheck: exports_external.string().optional()
|
|
59023
|
-
});
|
|
59024
|
-
function bridgeFactsOf(raw) {
|
|
59025
|
-
const parsed = bridgeFactsSchema.safeParse(raw);
|
|
59026
|
-
return parsed.success ? parsed.data : undefined;
|
|
59027
|
-
}
|
|
59028
|
-
function isBridgeJob(job, facts) {
|
|
59029
|
-
return facts !== undefined || job.bridge !== null && typeof job.bridge === "object";
|
|
59030
|
-
}
|
|
59031
|
-
function bridgeStatusField(ctx, found, facts) {
|
|
59032
|
-
const job = found.job ?? {};
|
|
59033
|
-
if (found.kind !== "swap" || !isBridgeJob(job, facts))
|
|
59034
|
-
return {};
|
|
59035
|
-
const status = describeBridgeJob(job, facts);
|
|
59036
|
-
for (const line of status.lines)
|
|
59037
|
-
ctx.deps.stderr.write(`${line}
|
|
59038
|
-
`);
|
|
59039
|
-
const { lines: _lines, ...rest } = status;
|
|
59040
|
-
return { bridgeStatus: rest };
|
|
59041
|
-
}
|
|
59042
|
-
function waitFinished(current, facts, sent) {
|
|
59043
|
-
const job = current.job ?? {};
|
|
59044
|
-
if (current.kind === "swap" && isBridgeJob(job, facts)) {
|
|
59045
|
-
const status = describeBridgeJob(job, facts);
|
|
59046
|
-
if (sent && status.phase === "not_broadcast")
|
|
59047
|
-
return false;
|
|
59048
|
-
return bridgeStatusFinal(status);
|
|
59420
|
+
const ready = shown.effects.linkedTransferReady;
|
|
59421
|
+
if (shown.direction === "widen" && ready) {
|
|
59422
|
+
const line = linkedTransferLine(ready);
|
|
59423
|
+
if (line)
|
|
59424
|
+
lines.push(line);
|
|
59049
59425
|
}
|
|
59050
|
-
const
|
|
59051
|
-
|
|
59052
|
-
}
|
|
59053
|
-
async function waitForSettlement(ctx, key, id, first, facts, sent = false) {
|
|
59054
|
-
const deadline = ctx.deps.now() + BRIDGE_WAIT_MS;
|
|
59055
|
-
let current = first;
|
|
59056
|
-
while (!waitFinished(current, facts, sent) && ctx.deps.now() < deadline) {
|
|
59057
|
-
await ctx.deps.sleep(BRIDGE_WAIT_POLL_MS);
|
|
59058
|
-
current = { ...current, ...operationSchema.parse(await request(ctx, key, jobPath(current.kind, id))) };
|
|
59426
|
+
for (const warning of shown.warnings) {
|
|
59427
|
+
if (!LOCALLY_RENDERED.has(warning.code))
|
|
59428
|
+
lines.push(`Warning: ${warning.message}`);
|
|
59059
59429
|
}
|
|
59060
|
-
|
|
59061
|
-
|
|
59062
|
-
|
|
59063
|
-
ctx.deps.stderr.write(`Still not final after ten minutes. Re-check with candle swap status ${id} --wait.
|
|
59430
|
+
if (lines[lines.length - 1] !== "")
|
|
59431
|
+
lines.push("");
|
|
59432
|
+
return lines.join(`
|
|
59064
59433
|
`);
|
|
59065
|
-
return { ...current, ...field, waited: { final: finished } };
|
|
59066
59434
|
}
|
|
59067
|
-
|
|
59068
|
-
|
|
59069
|
-
|
|
59070
|
-
|
|
59071
|
-
|
|
59072
|
-
|
|
59073
|
-
|
|
59074
|
-
}
|
|
59075
|
-
|
|
59076
|
-
|
|
59077
|
-
|
|
59435
|
+
function accessFailureDetails(result, context) {
|
|
59436
|
+
let code = result.code;
|
|
59437
|
+
let message = result.message;
|
|
59438
|
+
let suggestion;
|
|
59439
|
+
if (result.status === 404 && result.code === undefined) {
|
|
59440
|
+
code = "KEY_ACCESS_UNSUPPORTED";
|
|
59441
|
+
message = context.history ? "This Candle API has no key access history yet." : "This Candle API cannot change a key's access yet; nothing changed.";
|
|
59442
|
+
} else if (result.code === "LOOSEN_REQUIRES_SESSION" && context.self) {
|
|
59443
|
+
const level = context.preset ? CLI_SPELLING[context.preset] : "<level>";
|
|
59444
|
+
suggestion = `Widen it from a signed-in session or with the device token: candle keys access <prefix> --access ${level}`;
|
|
59445
|
+
} else if (result.code === "KEY_ACCESS_STALE") {
|
|
59446
|
+
suggestion = "Run the command again; the preview will show the key's current access.";
|
|
59447
|
+
} else if (result.code === "KEY_ACCESS_PARTNER_KEY") {
|
|
59448
|
+
suggestion = "Partner keys are managed by Candle; ask your Candle contact to change one.";
|
|
59449
|
+
} else if (result.status === 404 && result.code === "VALIDATION_FAILED") {
|
|
59450
|
+
suggestion = "Run: candle keys list, to see this account's keys and their prefixes.";
|
|
59078
59451
|
}
|
|
59079
|
-
|
|
59080
|
-
if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 18)
|
|
59081
|
-
throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
|
|
59082
|
-
return decimals;
|
|
59452
|
+
return { code: code ?? `HTTP ${result.status}`, message, ...suggestion !== undefined ? { suggestion } : {} };
|
|
59083
59453
|
}
|
|
59084
|
-
|
|
59085
|
-
|
|
59086
|
-
|
|
59087
|
-
|
|
59088
|
-
|
|
59089
|
-
|
|
59090
|
-
|
|
59454
|
+
function writeAccessFailure(ctx, result, context) {
|
|
59455
|
+
const { deps, apiUrl, json } = ctx;
|
|
59456
|
+
const { code, message, suggestion } = accessFailureDetails(result, context);
|
|
59457
|
+
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: context.self ? "key" : "device" });
|
|
59458
|
+
if (json) {
|
|
59459
|
+
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
59460
|
+
`);
|
|
59461
|
+
} else {
|
|
59462
|
+
deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
59463
|
+
`);
|
|
59091
59464
|
}
|
|
59092
|
-
|
|
59465
|
+
return 1;
|
|
59093
59466
|
}
|
|
59094
|
-
async function
|
|
59095
|
-
const
|
|
59096
|
-
|
|
59097
|
-
booleanFlags: ["--yes", "--wait"]
|
|
59098
|
-
});
|
|
59467
|
+
async function keysAccess(args, ctx) {
|
|
59468
|
+
const { deps, apiUrl, json } = ctx;
|
|
59469
|
+
const parsed = parseArgs(args, { valueFlags: ["--access"], booleanFlags: ["--yes", "--history"] });
|
|
59099
59470
|
if ("error" in parsed) {
|
|
59100
|
-
writeUsageFailure(
|
|
59471
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
59101
59472
|
return 2;
|
|
59102
59473
|
}
|
|
59103
|
-
const
|
|
59104
|
-
|
|
59105
|
-
|
|
59106
|
-
if (parsed.positionals.length !== 2 || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
|
|
59107
|
-
writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> [--wallet <tee-or-embedded>] [--to <tee>] [--wait] [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]. Solana: SOL, USDC, CNDL or a mint. Hood: ETH, USDG or a 0x token. SOL or USDC to ETH or USDG, or back, is a bridge between two TEE wallets.", ctx.json);
|
|
59474
|
+
const [target, extra] = parsed.positionals;
|
|
59475
|
+
if (target === undefined || extra !== undefined) {
|
|
59476
|
+
writeUsageFailure(deps, `${extra !== undefined ? `Unexpected argument: ${extra}. ` : "Name one key. "}${USAGE_ACCESS}`, json);
|
|
59108
59477
|
return 2;
|
|
59109
59478
|
}
|
|
59110
|
-
|
|
59111
|
-
|
|
59112
|
-
|
|
59113
|
-
|
|
59114
|
-
|
|
59115
|
-
|
|
59116
|
-
|
|
59117
|
-
|
|
59118
|
-
|
|
59119
|
-
|
|
59120
|
-
|
|
59121
|
-
...bridge
|
|
59122
|
-
});
|
|
59123
|
-
if (flags["--to"] !== undefined || parsed.booleans.has("--wait"))
|
|
59124
|
-
throw new TradingUsage("--to and --wait are for a bridge: SOL or USDC to ETH or USDG, or back.");
|
|
59125
|
-
const chain2 = pairChain(fromAsset, toAsset);
|
|
59126
|
-
const walletFlag = flags["--wallet"];
|
|
59127
|
-
const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
|
|
59128
|
-
if (named !== undefined && named !== chain2)
|
|
59129
|
-
throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, chain2);
|
|
59130
|
-
if (chain2 === "hood")
|
|
59131
|
-
return await hoodSwap(ctx, {
|
|
59132
|
-
flags,
|
|
59133
|
-
yes: parsed.booleans.has("--yes"),
|
|
59134
|
-
id,
|
|
59135
|
-
slippage,
|
|
59136
|
-
from: fromAsset,
|
|
59137
|
-
to: toAsset
|
|
59138
|
-
});
|
|
59139
|
-
const from = fromAsset.asset;
|
|
59140
|
-
const to = toAsset.asset;
|
|
59141
|
-
if (from === to)
|
|
59142
|
-
throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
|
|
59143
|
-
const fromBase = baseAsset(from);
|
|
59144
|
-
const toBase = baseAsset(to);
|
|
59145
|
-
if (!fromBase && !toBase)
|
|
59146
|
-
throw new TradingError("PAIR_UNSUPPORTED", "A token trade must have SOL, USDC or CNDL on one side; token-to-token routing is unavailable.");
|
|
59147
|
-
if (flags["--amount"])
|
|
59148
|
-
rawAmount(flags["--amount"], 18);
|
|
59149
|
-
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
59150
|
-
if (percent !== undefined && percent > 100000000n)
|
|
59151
|
-
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
59152
|
-
const kind = fromBase && toBase ? "swap" : "trade";
|
|
59153
|
-
const key = await tradingKey(ctx);
|
|
59154
|
-
const prior = await lookupOperation(ctx, key, id, kind);
|
|
59155
|
-
if (prior)
|
|
59156
|
-
return printTradingResult(ctx, prior);
|
|
59157
|
-
const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
|
|
59158
|
-
if (payerWallet.kind === "embedded" && kind === "swap")
|
|
59159
|
-
throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
|
|
59160
|
-
const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
|
|
59161
|
-
const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
|
|
59162
|
-
const decimals = await decimalsFor(ctx, from, solana);
|
|
59163
|
-
const outDecimals = await decimalsFor(ctx, to, solana);
|
|
59164
|
-
let amountRaw;
|
|
59165
|
-
if (percent !== undefined) {
|
|
59166
|
-
const reader = await solana();
|
|
59167
|
-
let balance;
|
|
59168
|
-
if (from === "SOL")
|
|
59169
|
-
balance = await tradingRead(ctx, reader, () => reader.rpc.getBalance(wallet.address));
|
|
59170
|
-
else {
|
|
59171
|
-
const mint = BASES[from]?.mint ?? from;
|
|
59172
|
-
const accounts = (await tradingRead(ctx, reader, () => Promise.all([
|
|
59173
|
-
reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_PROGRAM_ID),
|
|
59174
|
-
reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_2022_PROGRAM_ID)
|
|
59175
|
-
]))).flat();
|
|
59176
|
-
balance = accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
|
|
59177
|
-
}
|
|
59178
|
-
amountRaw = (balance * percent / 100000000n).toString();
|
|
59179
|
-
if (amountRaw === "0")
|
|
59180
|
-
throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
|
|
59181
|
-
} else
|
|
59182
|
-
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
59183
|
-
if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
|
|
59184
|
-
throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
|
|
59185
|
-
if (payerWallet.kind === "embedded")
|
|
59186
|
-
await assertDeferredExecuteSupported(ctx, key, id);
|
|
59187
|
-
if (!await claimOperation(ctx, key, id, kind))
|
|
59188
|
-
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
59189
|
-
ctx.deps.stderr.write(`Operation: ${id}
|
|
59190
|
-
`);
|
|
59191
|
-
const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
|
|
59192
|
-
const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
|
|
59193
|
-
clientTradeId: id,
|
|
59194
|
-
from,
|
|
59195
|
-
to,
|
|
59196
|
-
amountRaw,
|
|
59197
|
-
maxSlippageBps: slippage,
|
|
59198
|
-
payer
|
|
59199
|
-
}) : await request(ctx, key, "/api/v1/trade/agent/build", {
|
|
59200
|
-
clientTradeId: id,
|
|
59201
|
-
chain: "solana",
|
|
59202
|
-
mint: fromBase ? to : from,
|
|
59203
|
-
side: fromBase ? "buy" : "sell",
|
|
59204
|
-
quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
|
|
59205
|
-
amountRaw,
|
|
59206
|
-
maxSlippageBps: slippage,
|
|
59207
|
-
payer,
|
|
59208
|
-
...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
|
|
59209
|
-
});
|
|
59210
|
-
if (built.job || built.status === "executed")
|
|
59211
|
-
return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
|
|
59212
|
-
const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
|
|
59213
|
-
if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
|
|
59214
|
-
throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
|
|
59215
|
-
if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
|
|
59216
|
-
throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
|
|
59217
|
-
const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
|
|
59218
|
-
if (!artifacts)
|
|
59219
|
-
throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
|
|
59220
|
-
if (kind === "trade" && artifacts.quoteAsset !== (fromBase ?? toBase)?.toLowerCase())
|
|
59221
|
-
throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not the requested pair. Nothing was signed.`);
|
|
59222
|
-
if (data?.status !== "built" || typeof data.minOutRaw !== "string" || !/^\d+$/.test(data.minOutRaw) || !data.fee || !Number.isFinite(data.fee.bps))
|
|
59223
|
-
throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete quote; nothing was signed.");
|
|
59224
|
-
const minimumRaw = !fromBase && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
|
|
59225
|
-
const quote = {
|
|
59226
|
-
intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from} to ${to}`,
|
|
59227
|
-
wallet: wallet.address,
|
|
59228
|
-
venue: artifacts.quoteSource ?? artifacts.venue,
|
|
59229
|
-
priceImpactPct: artifacts.priceImpactPct ?? null,
|
|
59230
|
-
fee: data.fee,
|
|
59231
|
-
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to}`,
|
|
59232
|
-
minOutRaw: data.minOutRaw,
|
|
59233
|
-
minimumReceivedRaw: minimumRaw,
|
|
59234
|
-
tokenRisks: artifacts.tokenRisks ?? []
|
|
59235
|
-
};
|
|
59236
|
-
if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
|
|
59237
|
-
return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind, quote });
|
|
59238
|
-
if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
|
|
59239
|
-
throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
|
|
59240
|
-
if (payerWallet.kind === "embedded") {
|
|
59241
|
-
const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
|
|
59242
|
-
return printTradingResult(ctx, {
|
|
59243
|
-
...executed,
|
|
59244
|
-
clientTradeId: id,
|
|
59245
|
-
kind,
|
|
59246
|
-
quote,
|
|
59247
|
-
wallet: safeText(payerWallet.address)
|
|
59248
|
-
});
|
|
59479
|
+
const accessFlag = parsed.values["--access"];
|
|
59480
|
+
const history = parsed.booleans.has("--history");
|
|
59481
|
+
const yes = parsed.booleans.has("--yes");
|
|
59482
|
+
if (accessFlag === undefined === !history) {
|
|
59483
|
+
writeUsageFailure(deps, `Pass exactly one of --access and --history. ${USAGE_ACCESS}`, json);
|
|
59484
|
+
return 2;
|
|
59485
|
+
}
|
|
59486
|
+
if (history) {
|
|
59487
|
+
if (yes) {
|
|
59488
|
+
writeUsageFailure(deps, "--yes only applies to a change, not to --history.", json);
|
|
59489
|
+
return 2;
|
|
59249
59490
|
}
|
|
59250
|
-
|
|
59251
|
-
|
|
59252
|
-
|
|
59253
|
-
|
|
59254
|
-
|
|
59255
|
-
const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
|
|
59256
|
-
const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
|
|
59257
|
-
clientTradeId: id,
|
|
59258
|
-
swapId: data.swapId,
|
|
59259
|
-
signedTransactionsBase64: [signed]
|
|
59260
|
-
}) : await request(ctx, key, "/api/v1/trade/agent/submit", { clientTradeId: id, signedTransactions: [signed] });
|
|
59261
|
-
const settled = kind === "swap" ? reportSettlement(ctx, result.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
|
|
59262
|
-
return printTradingResult(ctx, {
|
|
59263
|
-
...result,
|
|
59264
|
-
clientTradeId: id,
|
|
59265
|
-
kind,
|
|
59266
|
-
quote,
|
|
59267
|
-
wallet: safeText(wallet.address),
|
|
59268
|
-
...settled
|
|
59269
|
-
});
|
|
59270
|
-
} catch (error) {
|
|
59271
|
-
return tradingFailure(ctx, error, id);
|
|
59491
|
+
if (target === SELF) {
|
|
59492
|
+
writeUsageFailure(deps, "--history needs the device token: name the key by prefix or label, not self.", json);
|
|
59493
|
+
return 2;
|
|
59494
|
+
}
|
|
59495
|
+
return keysAccessHistory(target, ctx);
|
|
59272
59496
|
}
|
|
59273
|
-
|
|
59274
|
-
|
|
59275
|
-
|
|
59276
|
-
|
|
59277
|
-
if (rpc)
|
|
59278
|
-
return rpc;
|
|
59279
|
-
const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
59280
|
-
if ("error" in resolved)
|
|
59281
|
-
throw new TradingUsage(resolved.error);
|
|
59282
|
-
ctx.deps.stderr.write(`Reading from ${rpcHostOf2(resolved.url)} (Hood RPC; reads only, nothing is sent there)
|
|
59283
|
-
`);
|
|
59284
|
-
rpc = createEvmRpc(resolved.url, ctx.deps.fetch);
|
|
59285
|
-
return rpc;
|
|
59286
|
-
};
|
|
59287
|
-
}
|
|
59288
|
-
async function evmRead(what, read) {
|
|
59289
|
-
try {
|
|
59290
|
-
return await read();
|
|
59291
|
-
} catch (error) {
|
|
59292
|
-
if (error instanceof TradingError || error instanceof TradingUsage)
|
|
59293
|
-
throw error;
|
|
59294
|
-
throw new TradingError("RPC_FAILED", `Reading ${what} over the Hood RPC failed; nothing was built or signed.`);
|
|
59497
|
+
const preset = ACCESS_LEVELS[accessFlag];
|
|
59498
|
+
if (preset === undefined) {
|
|
59499
|
+
writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
|
|
59500
|
+
return 2;
|
|
59295
59501
|
}
|
|
59296
|
-
|
|
59297
|
-
|
|
59298
|
-
|
|
59299
|
-
|
|
59300
|
-
|
|
59301
|
-
|
|
59302
|
-
|
|
59303
|
-
|
|
59304
|
-
|
|
59305
|
-
|
|
59306
|
-
|
|
59307
|
-
|
|
59308
|
-
|
|
59309
|
-
|
|
59310
|
-
|
|
59311
|
-
|
|
59312
|
-
|
|
59313
|
-
|
|
59314
|
-
|
|
59315
|
-
notice = outcome.appended ? outcome.notice : skipped(outcome.notice);
|
|
59316
|
-
} catch (error) {
|
|
59317
|
-
notice = skipped(error instanceof Error ? error.message : "the append failed");
|
|
59502
|
+
await printIdentity(ctx);
|
|
59503
|
+
const self = target === SELF;
|
|
59504
|
+
let path;
|
|
59505
|
+
let auth;
|
|
59506
|
+
let credentials;
|
|
59507
|
+
if (self) {
|
|
59508
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59509
|
+
if (!apiKey) {
|
|
59510
|
+
writeLocalFailure(deps, NO_API_KEY2, json);
|
|
59511
|
+
return 1;
|
|
59512
|
+
}
|
|
59513
|
+
path = `${KEYS_PATH4}/self/access`;
|
|
59514
|
+
auth = "key";
|
|
59515
|
+
credentials = { apiKey };
|
|
59516
|
+
} else {
|
|
59517
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
59518
|
+
if (!deviceToken) {
|
|
59519
|
+
writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
|
|
59520
|
+
return 1;
|
|
59318
59521
|
}
|
|
59522
|
+
const resolved = await resolveTargetKey(ctx, deviceToken, target, {
|
|
59523
|
+
codePrefix: "KEY_ACCESS",
|
|
59524
|
+
writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false })
|
|
59525
|
+
});
|
|
59526
|
+
if (!resolved.ok)
|
|
59527
|
+
return resolved.code;
|
|
59528
|
+
path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
|
|
59529
|
+
auth = "device";
|
|
59530
|
+
credentials = { deviceToken };
|
|
59319
59531
|
}
|
|
59320
|
-
|
|
59321
|
-
|
|
59532
|
+
const failureContext = { self, preset };
|
|
59533
|
+
const call = (body) => apiRequest(path, { method: "PUT", body, auth, credentials, apiUrl, fetch: deps.fetch, env: deps.env });
|
|
59534
|
+
const preview = await call({ access: preset, dryRun: true });
|
|
59535
|
+
if (!preview.ok)
|
|
59536
|
+
return writeAccessFailure(ctx, preview, failureContext);
|
|
59537
|
+
const shown = preview.body;
|
|
59538
|
+
const name2 = keyName2(shown.keyPrefix, shown.label);
|
|
59539
|
+
if (shown.direction === "unchanged") {
|
|
59540
|
+
if (json)
|
|
59541
|
+
deps.stdout.write(`${JSON.stringify({ ...shown, command: "keys access" })}
|
|
59322
59542
|
`);
|
|
59323
|
-
|
|
59324
|
-
}
|
|
59325
|
-
var HOOD_TRADE_LEGS = ["approval", "permit2Approval", "trade", "feeTransfer"];
|
|
59326
|
-
function describeHoodLegs(first, hasFee, primary = "trade") {
|
|
59327
|
-
const kinds = plannedLegKinds(first.legKind, first.plannedLegCount, hasFee, primary);
|
|
59328
|
-
const names = {
|
|
59329
|
-
approval: "approve",
|
|
59330
|
-
permit2Approval: "Permit2 approve",
|
|
59331
|
-
trade: "trade",
|
|
59332
|
-
feeTransfer: "fee",
|
|
59333
|
-
bridgeDeposit: "bridge deposit"
|
|
59334
|
-
};
|
|
59335
|
-
return kinds ? kinds.map((kind) => names[kind] ?? kind) : [`${first.plannedLegCount} legs, starting with ${names[first.legKind] ?? first.legKind}`];
|
|
59336
|
-
}
|
|
59337
|
-
async function hoodSwap(ctx, args) {
|
|
59338
|
-
const { flags, id, from, to } = args;
|
|
59339
|
-
if (from.asset === to.asset)
|
|
59340
|
-
throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
|
|
59341
|
-
if (!from.base && !to.base)
|
|
59342
|
-
throw new TradingError("PAIR_UNSUPPORTED", "A Hood token trade must have ETH or USDG on one side; token-to-token routing is unavailable.");
|
|
59343
|
-
if (flags["--amount"])
|
|
59344
|
-
rawAmount(flags["--amount"], 18);
|
|
59345
|
-
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
59346
|
-
if (percent !== undefined && percent > 100000000n)
|
|
59347
|
-
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
59348
|
-
const kind = from.base && to.base ? "swap" : "trade";
|
|
59349
|
-
const key = await tradingKey(ctx);
|
|
59350
|
-
const prior = await lookupOperation(ctx, key, id, kind);
|
|
59351
|
-
if (prior)
|
|
59352
|
-
return printTradingResult(ctx, prior);
|
|
59353
|
-
const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
|
|
59354
|
-
if (payer.kind === "embedded" && kind === "swap")
|
|
59355
|
-
throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
|
|
59356
|
-
const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
|
|
59357
|
-
const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
|
|
59358
|
-
const decimals = await hoodDecimals(from, rpc);
|
|
59359
|
-
const outDecimals = await hoodDecimals(to, rpc);
|
|
59360
|
-
let amountRaw;
|
|
59361
|
-
if (percent !== undefined) {
|
|
59362
|
-
const balance = from.asset === "ETH" ? await evmRead("the ETH balance", () => rpc().getBalance(payerAddress)) : await evmRead(`the ${from.asset} balance`, () => rpc().erc20BalanceOf(from.asset === "USDG" ? HOOD_USDG_ADDRESS : from.asset, payerAddress));
|
|
59363
|
-
amountRaw = (balance * percent / 100000000n).toString();
|
|
59364
|
-
if (amountRaw === "0")
|
|
59365
|
-
throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
|
|
59366
|
-
} else
|
|
59367
|
-
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
59368
|
-
if (payer.kind === "embedded")
|
|
59369
|
-
await assertDeferredExecuteSupported(ctx, key, id);
|
|
59370
|
-
if (!await claimOperation(ctx, key, id, kind))
|
|
59371
|
-
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
59372
|
-
ctx.deps.stderr.write(`Operation: ${id}
|
|
59543
|
+
else
|
|
59544
|
+
deps.stdout.write(`${name2} is already ${levelName(shown.to)}. Nothing changed.
|
|
59373
59545
|
`);
|
|
59374
|
-
|
|
59375
|
-
const base = from.base ?? to.base;
|
|
59376
|
-
const token = from.base ? to.asset : from.asset;
|
|
59377
|
-
const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
|
|
59378
|
-
clientTradeId: id,
|
|
59379
|
-
from: from.asset,
|
|
59380
|
-
to: to.asset,
|
|
59381
|
-
amountRaw,
|
|
59382
|
-
maxSlippageBps: args.slippage,
|
|
59383
|
-
payer: payerBody
|
|
59384
|
-
}) : await request(ctx, key, "/api/v1/trade/agent/build", {
|
|
59385
|
-
clientTradeId: id,
|
|
59386
|
-
chain: "hood",
|
|
59387
|
-
mint: token,
|
|
59388
|
-
side: from.base ? "buy" : "sell",
|
|
59389
|
-
quoteAsset: base.toLowerCase(),
|
|
59390
|
-
amountRaw,
|
|
59391
|
-
maxSlippageBps: args.slippage,
|
|
59392
|
-
payer: payerBody,
|
|
59393
|
-
...payer.kind === "embedded" ? { deferExecution: true } : {}
|
|
59394
|
-
});
|
|
59395
|
-
if (built.job || built.status === "executed")
|
|
59396
|
-
return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
|
|
59397
|
-
const body = kind === "swap" ? built.payload : built;
|
|
59398
|
-
const data = swapBuildSchema.parse(body);
|
|
59399
|
-
const echoed = kind === "swap" ? body?.recipient : body?.walletAddress;
|
|
59400
|
-
if (body?.chain !== "hood" || typeof echoed !== "string" || echoed.toLowerCase() !== payerAddress.toLowerCase())
|
|
59401
|
-
throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
|
|
59402
|
-
const artifacts = kind === "swap" ? { ...data, quoteAsset: undefined, quoteSource: undefined } : data.artifacts;
|
|
59403
|
-
if (!artifacts)
|
|
59404
|
-
throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
|
|
59405
|
-
if (kind === "trade" && artifacts.quoteAsset !== base.toLowerCase())
|
|
59406
|
-
throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not ${base}. Nothing was signed.`);
|
|
59407
|
-
let sequenced;
|
|
59408
|
-
if (payer.kind === "tee") {
|
|
59409
|
-
const parsed = sequencedSchema.safeParse(body);
|
|
59410
|
-
if (!parsed.success)
|
|
59411
|
-
throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet trades one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
|
|
59412
|
-
sequenced = parsed.data;
|
|
59546
|
+
return 0;
|
|
59413
59547
|
}
|
|
59414
|
-
const
|
|
59415
|
-
const
|
|
59416
|
-
|
|
59417
|
-
|
|
59418
|
-
|
|
59419
|
-
priceImpactPct: artifacts.priceImpactPct ?? null,
|
|
59420
|
-
fee: data.fee,
|
|
59421
|
-
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to.asset}`,
|
|
59422
|
-
minOutRaw: data.minOutRaw,
|
|
59423
|
-
minimumReceivedRaw: minimumRaw,
|
|
59424
|
-
tokenRisks: artifacts.tokenRisks ?? []
|
|
59425
|
-
};
|
|
59426
|
-
if (sequenced) {
|
|
59427
|
-
const leg = sequenced.nextLeg;
|
|
59428
|
-
const maxFee = BigInt(leg.maxFeePerGas);
|
|
59429
|
-
const reserve = sweepReserveFloor(maxFee, kind === "trade" ? [token] : []);
|
|
59430
|
-
quote.legs = describeHoodLegs(sequenced, BigInt(data.fee.feeRaw) > 0n);
|
|
59431
|
-
quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
|
|
59432
|
-
quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a trade never spends it`;
|
|
59433
|
-
quote.operationId = sequenced.operationId;
|
|
59548
|
+
const widen = shown.direction === "widen";
|
|
59549
|
+
const interactive = deps.isTTY.stdin && deps.isTTY.stdout;
|
|
59550
|
+
if (widen && yes) {
|
|
59551
|
+
writeUsageFailure(deps, "--yes only skips the prompt when narrowing; nothing changed.", json);
|
|
59552
|
+
return 2;
|
|
59434
59553
|
}
|
|
59435
|
-
if (
|
|
59436
|
-
|
|
59437
|
-
|
|
59554
|
+
if (widen && !interactive) {
|
|
59555
|
+
writeLocalFailure(deps, REQUIRES_TTY, json);
|
|
59556
|
+
return 1;
|
|
59557
|
+
}
|
|
59558
|
+
if (!widen && !yes && !interactive) {
|
|
59559
|
+
writeUsageFailure(deps, "Narrowing a key without a terminal needs --yes; nothing changed. Run it in an interactive shell, or pass --yes.", json);
|
|
59560
|
+
return 2;
|
|
59561
|
+
}
|
|
59562
|
+
const config = await deps.readConfig();
|
|
59563
|
+
const fields = effectiveProfileFields(config, ctx.profile);
|
|
59564
|
+
const account = fields.account ? shortAddress(fields.account) : "unknown";
|
|
59565
|
+
deps.stderr.write(`${accessScreen(shown, { account: fields.username ? `${fields.username} (${account})` : account, apiUrl })}
|
|
59566
|
+
`);
|
|
59567
|
+
if (widen) {
|
|
59568
|
+
const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
|
|
59569
|
+
if (typed.trim() !== shown.keyPrefix) {
|
|
59570
|
+
writeLocalFailure(deps, {
|
|
59571
|
+
code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
|
|
59572
|
+
message: `The acknowledgement is the key prefix ${shown.keyPrefix}; nothing changed.`,
|
|
59573
|
+
suggestion: `Run the command again and type ${shown.keyPrefix} at the prompt.`
|
|
59574
|
+
}, json);
|
|
59575
|
+
return 1;
|
|
59576
|
+
}
|
|
59577
|
+
} else if (!yes) {
|
|
59578
|
+
const answer = await deps.promptLine(`Change ${name2} from ${levelName(shown.from)} to ${levelName(shown.to)}? [y/N] `);
|
|
59579
|
+
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
|
|
59580
|
+
writeLocalFailure(deps, {
|
|
59581
|
+
code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
|
|
59582
|
+
message: "Not confirmed; nothing changed.",
|
|
59583
|
+
suggestion: "Run the command again and answer y, or pass --yes."
|
|
59584
|
+
}, json);
|
|
59585
|
+
return 1;
|
|
59586
|
+
}
|
|
59587
|
+
}
|
|
59588
|
+
const committed = await call({ access: preset, expectScopes: shown.from.scopes });
|
|
59589
|
+
if (!committed.ok)
|
|
59590
|
+
return writeAccessFailure(ctx, committed, failureContext);
|
|
59591
|
+
const result = committed.body;
|
|
59592
|
+
if (fields.keyPrefix !== undefined && fields.keyPrefix === result.keyPrefix && result.direction !== "unchanged") {
|
|
59593
|
+
const scopes = scopesForPreset(result.to.access);
|
|
59594
|
+
if (ctx.profile)
|
|
59595
|
+
await deps.updateProfile(ctx.profile, { scopes });
|
|
59596
|
+
else
|
|
59597
|
+
await deps.writeConfig({ scopes });
|
|
59598
|
+
}
|
|
59599
|
+
if (json) {
|
|
59600
|
+
deps.stdout.write(`${JSON.stringify({ ...result, command: "keys access" })}
|
|
59601
|
+
`);
|
|
59602
|
+
return 0;
|
|
59603
|
+
}
|
|
59604
|
+
if (result.direction === "unchanged") {
|
|
59605
|
+
deps.stdout.write(`${keyName2(result.keyPrefix, result.label)} is already ${levelName(result.to)}. Nothing changed.
|
|
59438
59606
|
`);
|
|
59439
|
-
return
|
|
59440
|
-
success: true,
|
|
59441
|
-
status: "cancelled",
|
|
59442
|
-
clientTradeId: id,
|
|
59443
|
-
kind,
|
|
59444
|
-
quote,
|
|
59445
|
-
...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
|
|
59446
|
-
});
|
|
59607
|
+
return 0;
|
|
59447
59608
|
}
|
|
59448
|
-
|
|
59449
|
-
|
|
59450
|
-
|
|
59451
|
-
|
|
59452
|
-
|
|
59609
|
+
deps.stdout.write(`Changed ${keyName2(result.keyPrefix, result.label)} to ${levelName(result.to)}.` + `${result.changeId ? ` Change id ${result.changeId}` : ""}
|
|
59610
|
+
`);
|
|
59611
|
+
return 0;
|
|
59612
|
+
}
|
|
59613
|
+
async function keysAccessHistory(target, ctx) {
|
|
59614
|
+
const { deps, apiUrl, json } = ctx;
|
|
59615
|
+
await printIdentity(ctx);
|
|
59616
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
59617
|
+
if (!deviceToken) {
|
|
59618
|
+
writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
|
|
59619
|
+
return 1;
|
|
59453
59620
|
}
|
|
59454
|
-
const
|
|
59455
|
-
|
|
59456
|
-
|
|
59457
|
-
const recorded = kind === "trade" ? token : HOOD_USDG_ADDRESS;
|
|
59458
|
-
const notices = [];
|
|
59459
|
-
const run2 = await runSequencedLegs(ctx, key, {
|
|
59460
|
-
wallet,
|
|
59461
|
-
first: sequenced,
|
|
59462
|
-
submitPath: kind === "swap" ? "/api/v1/agent/swap/submit" : "/api/v1/trade/agent/submit",
|
|
59463
|
-
submitFields: kind === "swap" ? { clientTradeId: id, swapId: data.swapId } : { clientTradeId: id },
|
|
59464
|
-
unwrap: (answer) => kind === "swap" ? answer.payload ?? {} : answer,
|
|
59465
|
-
clientId: id,
|
|
59466
|
-
kind,
|
|
59467
|
-
allowedLegs: HOOD_TRADE_LEGS,
|
|
59468
|
-
onLanded: async (_leg) => {
|
|
59469
|
-
const notice = await recordTradedToken(ctx, wallet.address, toChecksumAddress(recorded));
|
|
59470
|
-
if (notice)
|
|
59471
|
-
notices.push(notice);
|
|
59472
|
-
}
|
|
59621
|
+
const resolved = await resolveTargetKey(ctx, deviceToken, target, {
|
|
59622
|
+
codePrefix: "KEY_ACCESS",
|
|
59623
|
+
writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false, history: true })
|
|
59473
59624
|
});
|
|
59474
|
-
|
|
59475
|
-
|
|
59476
|
-
|
|
59477
|
-
|
|
59478
|
-
|
|
59479
|
-
|
|
59480
|
-
|
|
59481
|
-
|
|
59482
|
-
wallet: safeText(wallet.address),
|
|
59483
|
-
operationId: sequenced.operationId,
|
|
59484
|
-
landedLegs: run2.landed,
|
|
59485
|
-
evmRecord: { token: toChecksumAddress(recorded), notices }
|
|
59625
|
+
if (!resolved.ok)
|
|
59626
|
+
return resolved.code;
|
|
59627
|
+
const result = await apiRequest(`${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access-changes`, {
|
|
59628
|
+
auth: "device",
|
|
59629
|
+
credentials: { deviceToken },
|
|
59630
|
+
apiUrl,
|
|
59631
|
+
fetch: deps.fetch,
|
|
59632
|
+
env: deps.env
|
|
59486
59633
|
});
|
|
59634
|
+
if (!result.ok)
|
|
59635
|
+
return writeAccessFailure(ctx, result, { self: false, history: true });
|
|
59636
|
+
if (json) {
|
|
59637
|
+
deps.stdout.write(`${JSON.stringify({ ...result.body, command: "keys access" })}
|
|
59638
|
+
`);
|
|
59639
|
+
return 0;
|
|
59640
|
+
}
|
|
59641
|
+
const rows = result.body.changes ?? [];
|
|
59642
|
+
if (rows.length === 0) {
|
|
59643
|
+
deps.stdout.write(`No access changes on key ${resolved.keyPrefix}.
|
|
59644
|
+
`);
|
|
59645
|
+
return 0;
|
|
59646
|
+
}
|
|
59647
|
+
deps.stdout.write(`${renderTable(["Time", "From", "To", "Actor", "Device or key"], rows.map((row) => [
|
|
59648
|
+
formatTimestamp(row.at),
|
|
59649
|
+
levelName(row.from),
|
|
59650
|
+
levelName(row.to),
|
|
59651
|
+
row.actor,
|
|
59652
|
+
row.actorDevicePrefix ?? row.actorKeyPrefix ?? ""
|
|
59653
|
+
]))}
|
|
59654
|
+
`);
|
|
59655
|
+
if (rows.some((row) => row.actor === "device"))
|
|
59656
|
+
deps.stdout.write(`${PORTAL_REVOKE_LINE}
|
|
59657
|
+
`);
|
|
59658
|
+
return 0;
|
|
59487
59659
|
}
|
|
59488
|
-
|
|
59489
|
-
|
|
59490
|
-
|
|
59491
|
-
|
|
59492
|
-
|
|
59493
|
-
|
|
59494
|
-
|
|
59495
|
-
|
|
59496
|
-
|
|
59497
|
-
|
|
59498
|
-
|
|
59499
|
-
|
|
59500
|
-
|
|
59501
|
-
}
|
|
59502
|
-
|
|
59503
|
-
|
|
59504
|
-
|
|
59505
|
-
if (from === "USDG")
|
|
59506
|
-
return evmRead("the USDG balance", () => evm().erc20BalanceOf(HOOD_USDG_ADDRESS, address));
|
|
59507
|
-
const reader = await solana();
|
|
59508
|
-
if (from === "SOL")
|
|
59509
|
-
return tradingRead(ctx, reader, () => reader.rpc.getBalance(address));
|
|
59510
|
-
const mint = BASES.USDC?.mint;
|
|
59511
|
-
const accounts = (await tradingRead(ctx, reader, () => Promise.all([
|
|
59512
|
-
reader.rpc.getTokenAccountsByOwner(address, TOKEN_PROGRAM_ID),
|
|
59513
|
-
reader.rpc.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID)
|
|
59514
|
-
]))).flat();
|
|
59515
|
-
return accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
|
|
59660
|
+
|
|
59661
|
+
// src/index.ts
|
|
59662
|
+
init_keys_embedded_wallet();
|
|
59663
|
+
|
|
59664
|
+
// src/commands/keys-wallets.ts
|
|
59665
|
+
init_args();
|
|
59666
|
+
init_deps();
|
|
59667
|
+
init_profiles();
|
|
59668
|
+
init_render();
|
|
59669
|
+
var NO_API_KEY3 = {
|
|
59670
|
+
code: "NO_API_KEY",
|
|
59671
|
+
message: "No API key for this profile.",
|
|
59672
|
+
suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
|
|
59673
|
+
};
|
|
59674
|
+
function widenRefusedHint(prefix, walletIds) {
|
|
59675
|
+
const wallets2 = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
|
|
59676
|
+
return `To move TEE wallets to this key, run: candle tee rebind ${wallets2} --to-key ${prefix} ` + "(owner, device token; --label-prefix <p> names many at once). To grant a linked wallet to the key " + "instead, use the agent console's Agents tab in a signed-in session. If `tee rebind` says the wallets " + "are already bound, grant them to the key in the agent console's Agents tab (signed in).";
|
|
59516
59677
|
}
|
|
59517
|
-
|
|
59518
|
-
|
|
59519
|
-
|
|
59520
|
-
|
|
59521
|
-
const
|
|
59522
|
-
const
|
|
59523
|
-
if (
|
|
59524
|
-
|
|
59525
|
-
|
|
59526
|
-
rawAmount(flags["--amount"], 18);
|
|
59527
|
-
const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
|
|
59528
|
-
if (percent !== undefined && percent > 100000000n)
|
|
59529
|
-
throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
|
|
59530
|
-
const key = await tradingKey(ctx);
|
|
59531
|
-
const prior = await lookupOperation(ctx, key, id, "swap");
|
|
59532
|
-
if (prior)
|
|
59533
|
-
return printTradingResult(ctx, {
|
|
59534
|
-
...prior,
|
|
59535
|
-
...bridgeStatusField(ctx, prior, bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge))
|
|
59536
|
-
});
|
|
59537
|
-
const payer = await tradingPayer(ctx, key, walletFlag, "swap:write", origin);
|
|
59538
|
-
if (payer.kind !== "tee")
|
|
59539
|
-
throw new TradingError("CHAIN_MISMATCH", `Only a TEE wallet bridges, and the embedded wallet ${safeText(payer.address)} is not one. Name a ${chainName(origin)} TEE wallet with --wallet. Nothing was built.`);
|
|
59540
|
-
const wallet = payer.wallet;
|
|
59541
|
-
const { rows } = await listTradingWallets(ctx, key, "swap:write");
|
|
59542
|
-
const onDestination = rows.filter((row) => rowChain(row) === destination);
|
|
59543
|
-
const toFlag = flags["--to"];
|
|
59544
|
-
let toWalletId;
|
|
59545
|
-
let candidates = onDestination.filter((row) => row.active);
|
|
59546
|
-
if (toFlag !== undefined) {
|
|
59547
|
-
const matches = rows.filter((row) => matchesName(row, toFlag));
|
|
59548
|
-
const match = matches[0];
|
|
59549
|
-
if (matches.length > 1)
|
|
59550
|
-
throw new TradingError("TEE_WALLET_REQUIRED", `"${safeText(toFlag)}" matches ${matches.length} TEE wallets on this key: ${matches.map(describeWallet).join("; ")}. Name one by id or address.`);
|
|
59551
|
-
if (!match || rowChain(match) !== destination)
|
|
59552
|
-
throw new TradingError("BRIDGE_DESTINATION_MISSING", `--to ${safeText(toFlag)} is not a ${chainName(destination)} TEE wallet on this key. A bridge lands only in this key's own TEE wallet on ${chainName(destination)}${onDestination.length > 0 ? `: ${onDestination.map(describeWallet).join("; ")}` : ""}. Nothing was built.`);
|
|
59553
|
-
if (!match.active)
|
|
59554
|
-
throw new TradingError("TEE_WALLET_INACTIVE", `The destination ${describeWallet(match)} is not a verified-active TEE wallet. Nothing was built.`);
|
|
59555
|
-
toWalletId = match.id;
|
|
59556
|
-
candidates = [match];
|
|
59557
|
-
} else if (candidates.length !== 1) {
|
|
59558
|
-
throw new TradingError("BRIDGE_DESTINATION_MISSING", candidates.length === 0 ? `This key has no active ${chainName(destination)} TEE wallet to bridge into. Promote one onto this key (candle vault promote), then bridge. Nothing was built.` : `This key has more than one ${chainName(destination)} TEE wallet; name the destination with --to: ${candidates.map(describeWallet).join("; ")}. Nothing was built.`);
|
|
59678
|
+
function formatTimestamp2(ms) {
|
|
59679
|
+
return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
|
|
59680
|
+
}
|
|
59681
|
+
async function keysWalletsList(args, ctx) {
|
|
59682
|
+
const { deps, apiUrl, json } = ctx;
|
|
59683
|
+
const parsed = parseArgs(args, {});
|
|
59684
|
+
if ("error" in parsed) {
|
|
59685
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
59686
|
+
return 2;
|
|
59559
59687
|
}
|
|
59560
|
-
const
|
|
59561
|
-
|
|
59562
|
-
|
|
59563
|
-
|
|
59564
|
-
|
|
59565
|
-
|
|
59566
|
-
|
|
59567
|
-
|
|
59568
|
-
|
|
59569
|
-
|
|
59570
|
-
|
|
59571
|
-
|
|
59572
|
-
|
|
59573
|
-
|
|
59574
|
-
|
|
59575
|
-
|
|
59576
|
-
|
|
59577
|
-
from,
|
|
59578
|
-
to,
|
|
59579
|
-
amountRaw,
|
|
59580
|
-
maxSlippageBps: args.slippage,
|
|
59581
|
-
payer: { type: "linked", linkedWalletId: wallet.id },
|
|
59582
|
-
...toWalletId !== undefined ? { toWalletId } : {}
|
|
59688
|
+
const prefix = parsed.positionals[0];
|
|
59689
|
+
if (!prefix) {
|
|
59690
|
+
writeUsageFailure(deps, "Usage: candle keys wallets <prefix>", json);
|
|
59691
|
+
return 2;
|
|
59692
|
+
}
|
|
59693
|
+
await printIdentity(ctx);
|
|
59694
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59695
|
+
if (!apiKey) {
|
|
59696
|
+
writeLocalFailure(deps, NO_API_KEY3, json);
|
|
59697
|
+
return 1;
|
|
59698
|
+
}
|
|
59699
|
+
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
|
|
59700
|
+
auth: "key",
|
|
59701
|
+
credentials: { apiKey },
|
|
59702
|
+
apiUrl,
|
|
59703
|
+
fetch: deps.fetch,
|
|
59704
|
+
env: deps.env
|
|
59583
59705
|
});
|
|
59584
|
-
if (
|
|
59585
|
-
|
|
59586
|
-
|
|
59587
|
-
const parsedBuild = bridgeBuildSchema.safeParse(body);
|
|
59588
|
-
if (!parsedBuild.success)
|
|
59589
|
-
throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete bridge quote; nothing was signed.");
|
|
59590
|
-
const data = parsedBuild.data;
|
|
59591
|
-
if (data.venue !== "relay")
|
|
59592
|
-
throw new TradingError("INVALID_RESPONSE", `A bridge goes through Relay, not ${safeText(data.venue)}; nothing was signed.`);
|
|
59593
|
-
if (data.fee.bps !== 0 || data.fee.feeRaw !== "0")
|
|
59594
|
-
throw new TradingError("INVALID_RESPONSE", "A bridge carries no Candle fee, and this quote has one; nothing was signed.");
|
|
59595
|
-
const recipient = candidates.find((row) => destination === "hood" ? sameEvmAddress(row.address, data.recipient) : row.address === data.recipient);
|
|
59596
|
-
if (!recipient)
|
|
59597
|
-
throw new TradingError("INVALID_RESPONSE", `Candle named ${safeText(data.recipient)} as the destination, which is not ${toWalletId !== undefined ? "the TEE wallet --to named" : `this key's ${chainName(destination)} TEE wallet`}; nothing was signed.`);
|
|
59598
|
-
const facts = {
|
|
59599
|
-
from,
|
|
59600
|
-
to,
|
|
59601
|
-
recipient: recipient.address,
|
|
59602
|
-
...data.statusChecks[0] !== undefined ? { statusCheck: data.statusChecks[0] } : {}
|
|
59603
|
-
};
|
|
59604
|
-
await saveOperationBridge(ctx, key, id, { ...facts });
|
|
59605
|
-
const refused = (problem) => new TradingError("RELAY_STEP_REFUSED", `Relay's deposit did not pass this machine's check (${problem}); nothing was signed.`);
|
|
59606
|
-
let sequenced;
|
|
59607
|
-
let transaction;
|
|
59608
|
-
const hoodOrigin = from === "ETH" || from === "USDG" ? from : undefined;
|
|
59609
|
-
if (hoodOrigin) {
|
|
59610
|
-
const parsedLeg = sequencedSchema.safeParse(body);
|
|
59611
|
-
if (!parsedLeg.success)
|
|
59612
|
-
throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet bridges one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
|
|
59613
|
-
sequenced = parsedLeg.data;
|
|
59614
|
-
if (typeof data.walletAddress !== "string" || !sameEvmAddress(data.walletAddress, wallet.address))
|
|
59615
|
-
throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
|
|
59616
|
-
const plan = plannedLegKinds(sequenced.legKind, sequenced.plannedLegCount, false, "bridgeDeposit");
|
|
59617
|
-
if (!bridgePlanAdmitted(hoodOrigin, plan))
|
|
59618
|
-
throw refused(`the plan is ${sequenced.plannedLegCount} leg(s) starting with ${sequenced.legKind}`);
|
|
59619
|
-
const problem = relayHoodLegProblem(sequenced.legKind, sequenced.nextLeg, {
|
|
59620
|
-
origin: hoodOrigin,
|
|
59621
|
-
payer: wallet.address,
|
|
59622
|
-
amountRaw
|
|
59623
|
-
});
|
|
59624
|
-
if (problem)
|
|
59625
|
-
throw refused(problem);
|
|
59626
|
-
} else {
|
|
59627
|
-
if (data.transactionsBase64?.length !== 1)
|
|
59628
|
-
throw refused(`Candle returned ${data.transactionsBase64?.length ?? 0} transactions, not one deposit`);
|
|
59629
|
-
transaction = data.transactionsBase64[0];
|
|
59630
|
-
const reader = await solana();
|
|
59631
|
-
const problem = await tradingRead(ctx, reader, () => relaySolanaDepositProblem(transaction, wallet.address, reader.rpc));
|
|
59632
|
-
if (problem)
|
|
59633
|
-
throw refused(problem);
|
|
59706
|
+
if (!result.ok) {
|
|
59707
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
59708
|
+
return 1;
|
|
59634
59709
|
}
|
|
59635
|
-
|
|
59636
|
-
|
|
59637
|
-
|
|
59638
|
-
|
|
59639
|
-
wallet: wallet.address,
|
|
59640
|
-
venue: "relay",
|
|
59641
|
-
priceImpactPct: null,
|
|
59642
|
-
minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to} (Relay's estimate ${decimalAmount(data.expectedOutRaw, outDecimals)} ${to}, less the ${args.slippage} bps slippage bound)`,
|
|
59643
|
-
minimumReceivedRaw: minimumRaw,
|
|
59644
|
-
expectedOutRaw: data.expectedOutRaw,
|
|
59645
|
-
destination: `${recipient.label ? `${recipient.label} ` : ""}${recipient.address} (${chainName(destination)} TEE wallet on this key)`,
|
|
59646
|
-
candleFee: "none",
|
|
59647
|
-
relayFees: data.venueCostUsd !== undefined ? `about $${data.venueCostUsd} as Relay reports it` : "not reported",
|
|
59648
|
-
estimatedTime: data.venueTimeEstimateSec !== undefined ? `about ${data.venueTimeEstimateSec}s as Relay reports it` : "not reported",
|
|
59649
|
-
tokenRisks: []
|
|
59650
|
-
};
|
|
59651
|
-
if (sequenced) {
|
|
59652
|
-
const leg = sequenced.nextLeg;
|
|
59653
|
-
const maxFee = BigInt(leg.maxFeePerGas);
|
|
59654
|
-
const reserve = sweepReserveFloor(maxFee, []);
|
|
59655
|
-
quote.legs = describeHoodLegs(sequenced, false, "bridgeDeposit");
|
|
59656
|
-
quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
|
|
59657
|
-
quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a bridge never spends it`;
|
|
59658
|
-
quote.operationId = sequenced.operationId;
|
|
59710
|
+
if (json) {
|
|
59711
|
+
deps.stdout.write(`${JSON.stringify(result.body)}
|
|
59712
|
+
`);
|
|
59713
|
+
return 0;
|
|
59659
59714
|
}
|
|
59660
|
-
|
|
59661
|
-
|
|
59662
|
-
|
|
59715
|
+
const body = result.body;
|
|
59716
|
+
deps.stdout.write(body.walletScope === "selected" ? `Scope: selected — this profile can only spend from the wallets below.
|
|
59717
|
+
` : `Scope: all — this profile can spend from every wallet on the account, listed here or not.
|
|
59663
59718
|
`);
|
|
59664
|
-
|
|
59665
|
-
|
|
59666
|
-
|
|
59667
|
-
|
|
59668
|
-
|
|
59669
|
-
|
|
59670
|
-
|
|
59671
|
-
});
|
|
59719
|
+
if (body.profileId)
|
|
59720
|
+
deps.stdout.write(`Profile: ${body.profileId}
|
|
59721
|
+
`);
|
|
59722
|
+
if (body.wallets.length === 0) {
|
|
59723
|
+
deps.stdout.write(`No wallets assigned.
|
|
59724
|
+
`);
|
|
59725
|
+
return 0;
|
|
59672
59726
|
}
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59677
|
-
|
|
59678
|
-
|
|
59679
|
-
|
|
59680
|
-
|
|
59681
|
-
|
|
59682
|
-
|
|
59683
|
-
|
|
59684
|
-
|
|
59685
|
-
|
|
59686
|
-
|
|
59687
|
-
|
|
59688
|
-
|
|
59689
|
-
|
|
59690
|
-
|
|
59691
|
-
depositHash = run2.landed.find((leg) => leg.kind === "bridgeDeposit")?.hash;
|
|
59692
|
-
result = {
|
|
59693
|
-
...run2.final,
|
|
59694
|
-
chain: "hood",
|
|
59695
|
-
operationId: sequenced.operationId,
|
|
59696
|
-
landedLegs: run2.landed
|
|
59697
|
-
};
|
|
59698
|
-
} else {
|
|
59699
|
-
const signed = await relaySign(ctx, key, wallet, transaction);
|
|
59700
|
-
const submitted = await request(ctx, key, "/api/v1/agent/swap/submit", {
|
|
59701
|
-
clientTradeId: id,
|
|
59702
|
-
swapId: data.swapId,
|
|
59703
|
-
signedTransactionsBase64: [signed]
|
|
59704
|
-
});
|
|
59705
|
-
const payload = submitted.payload ?? {};
|
|
59706
|
-
const hashes = Array.isArray(payload.hashes) ? payload.hashes.filter((h) => typeof h === "string") : [];
|
|
59707
|
-
depositHash = hashes[0];
|
|
59708
|
-
result = submitted;
|
|
59727
|
+
const rows = body.wallets.map((w) => [
|
|
59728
|
+
w.linkedWalletId,
|
|
59729
|
+
w.chain,
|
|
59730
|
+
w.address,
|
|
59731
|
+
w.label ?? "-",
|
|
59732
|
+
w.spendCapable ? "yes" : "no",
|
|
59733
|
+
formatTimestamp2(w.assignedAt)
|
|
59734
|
+
]);
|
|
59735
|
+
deps.stdout.write(`${renderTable(["Id", "Chain", "Address", "Label", "Can sign", "Assigned"], rows)}
|
|
59736
|
+
`);
|
|
59737
|
+
return 0;
|
|
59738
|
+
}
|
|
59739
|
+
async function keysWalletsSet(args, ctx) {
|
|
59740
|
+
const { deps, apiUrl, json } = ctx;
|
|
59741
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallets"] });
|
|
59742
|
+
if ("error" in parsed) {
|
|
59743
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
59744
|
+
return 2;
|
|
59709
59745
|
}
|
|
59710
|
-
|
|
59746
|
+
const prefix = parsed.positionals[0];
|
|
59747
|
+
if (!prefix) {
|
|
59748
|
+
writeUsageFailure(deps, "Usage: candle keys wallets set <prefix> --wallets <id,id,...>", json);
|
|
59749
|
+
return 2;
|
|
59750
|
+
}
|
|
59751
|
+
const raw = parsed.values["--wallets"];
|
|
59752
|
+
if (raw === undefined) {
|
|
59753
|
+
writeUsageFailure(deps, 'Missing --wallets. Pass a comma-separated list, or "" to assign none.', json);
|
|
59754
|
+
return 2;
|
|
59755
|
+
}
|
|
59756
|
+
const walletIds = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
59757
|
+
await printIdentity(ctx);
|
|
59758
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59759
|
+
if (!apiKey) {
|
|
59760
|
+
writeLocalFailure(deps, NO_API_KEY3, json);
|
|
59761
|
+
return 1;
|
|
59762
|
+
}
|
|
59763
|
+
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
|
|
59764
|
+
method: "PUT",
|
|
59765
|
+
body: { walletIds },
|
|
59766
|
+
auth: "key",
|
|
59767
|
+
credentials: { apiKey },
|
|
59768
|
+
apiUrl,
|
|
59769
|
+
fetch: deps.fetch,
|
|
59770
|
+
env: deps.env
|
|
59771
|
+
});
|
|
59772
|
+
if (!result.ok) {
|
|
59773
|
+
if (result.code !== "LOOSEN_REQUIRES_SESSION") {
|
|
59774
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
59775
|
+
return 1;
|
|
59776
|
+
}
|
|
59777
|
+
const hint = widenRefusedHint(prefix, walletIds);
|
|
59778
|
+
writeFailure(deps, { ...result, uiHint: hint }, { apiUrl, authType: "key" }, json);
|
|
59779
|
+
if (!json)
|
|
59780
|
+
deps.stderr.write(`${hint}
|
|
59711
59781
|
`);
|
|
59712
|
-
|
|
59713
|
-
|
|
59714
|
-
|
|
59715
|
-
|
|
59716
|
-
bridge: { from, to, destination: recipient.address, ...depositHash ? { depositHash } : {} },
|
|
59717
|
-
quote,
|
|
59718
|
-
wallet: safeText(wallet.address)
|
|
59719
|
-
};
|
|
59720
|
-
if (!args.wait)
|
|
59721
|
-
return printTradingResult(ctx, receipt);
|
|
59722
|
-
try {
|
|
59723
|
-
const found = await lookupOperation(ctx, key, id, "swap");
|
|
59724
|
-
if (!found)
|
|
59725
|
-
return printTradingResult(ctx, { ...receipt, waited: { final: false } });
|
|
59726
|
-
const waited = await waitForSettlement(ctx, key, id, found, facts, true);
|
|
59727
|
-
return printTradingResult(ctx, {
|
|
59728
|
-
...receipt,
|
|
59729
|
-
job: waited.job,
|
|
59730
|
-
bridgeStatus: waited.bridgeStatus,
|
|
59731
|
-
waited: waited.waited
|
|
59732
|
-
});
|
|
59733
|
-
} catch (error) {
|
|
59734
|
-
const reason = safeText(error instanceof Error ? describeRpcFailure(error) : String(error));
|
|
59735
|
-
ctx.deps.stderr.write(`Could not follow the bridge (${reason}). The deposit was sent; do not send it again. Re-check with candle swap status ${id} --wait.
|
|
59782
|
+
return 1;
|
|
59783
|
+
}
|
|
59784
|
+
if (json) {
|
|
59785
|
+
deps.stdout.write(`${JSON.stringify(result.body)}
|
|
59736
59786
|
`);
|
|
59737
|
-
return
|
|
59787
|
+
return 0;
|
|
59788
|
+
}
|
|
59789
|
+
deps.stdout.write(walletIds.length === 0 ? `Cleared every wallet assignment on ${prefix}.
|
|
59790
|
+
` : `Assigned ${walletIds.length} wallet${walletIds.length === 1 ? "" : "s"} to ${prefix}.
|
|
59791
|
+
`);
|
|
59792
|
+
return 0;
|
|
59793
|
+
}
|
|
59794
|
+
async function keysWalletsScope(args, ctx) {
|
|
59795
|
+
const { deps, apiUrl, json } = ctx;
|
|
59796
|
+
const parsed = parseArgs(args, { valueFlags: ["--scope"] });
|
|
59797
|
+
if ("error" in parsed) {
|
|
59798
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
59799
|
+
return 2;
|
|
59800
|
+
}
|
|
59801
|
+
const prefix = parsed.positionals[0];
|
|
59802
|
+
const scope = parsed.values["--scope"];
|
|
59803
|
+
if (!prefix || scope !== "all" && scope !== "selected") {
|
|
59804
|
+
writeUsageFailure(deps, "Usage: candle keys wallets scope <prefix> --scope <all|selected>", json);
|
|
59805
|
+
return 2;
|
|
59806
|
+
}
|
|
59807
|
+
await printIdentity(ctx);
|
|
59808
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59809
|
+
if (!apiKey) {
|
|
59810
|
+
writeLocalFailure(deps, NO_API_KEY3, json);
|
|
59811
|
+
return 1;
|
|
59812
|
+
}
|
|
59813
|
+
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallet-scope`, {
|
|
59814
|
+
method: "PUT",
|
|
59815
|
+
body: { scope },
|
|
59816
|
+
auth: "key",
|
|
59817
|
+
credentials: { apiKey },
|
|
59818
|
+
apiUrl,
|
|
59819
|
+
fetch: deps.fetch,
|
|
59820
|
+
env: deps.env
|
|
59821
|
+
});
|
|
59822
|
+
if (!result.ok) {
|
|
59823
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
59824
|
+
return 1;
|
|
59825
|
+
}
|
|
59826
|
+
if (json) {
|
|
59827
|
+
deps.stdout.write(`${JSON.stringify(result.body)}
|
|
59828
|
+
`);
|
|
59829
|
+
return 0;
|
|
59738
59830
|
}
|
|
59831
|
+
deps.stdout.write(scope === "selected" ? `${prefix} is now limited to its assigned wallets.
|
|
59832
|
+
` : `${prefix} can now spend from every wallet on the account.
|
|
59833
|
+
`);
|
|
59834
|
+
return 0;
|
|
59835
|
+
}
|
|
59836
|
+
async function keysWallets(args, ctx) {
|
|
59837
|
+
const [verb, ...rest] = args;
|
|
59838
|
+
if (verb === "set")
|
|
59839
|
+
return keysWalletsSet(rest, ctx);
|
|
59840
|
+
if (verb === "scope")
|
|
59841
|
+
return keysWalletsScope(rest, ctx);
|
|
59842
|
+
return keysWalletsList(args, ctx);
|
|
59739
59843
|
}
|
|
59740
59844
|
|
|
59741
59845
|
// src/commands/launch.ts
|
|
59846
|
+
init_args();
|
|
59847
|
+
init_evm_lite();
|
|
59848
|
+
init_render();
|
|
59849
|
+
init_solana_endpoint();
|
|
59850
|
+
init_solana_lite();
|
|
59851
|
+
init_trading();
|
|
59852
|
+
init_swap();
|
|
59853
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
59742
59854
|
var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
|
|
59743
59855
|
var QUOTE_CHAIN = {
|
|
59744
59856
|
sol: "solana",
|
|
@@ -59985,6 +60097,7 @@ init_render();
|
|
|
59985
60097
|
init_solana_endpoint();
|
|
59986
60098
|
init_solana_lite();
|
|
59987
60099
|
init_trading();
|
|
60100
|
+
init_swap();
|
|
59988
60101
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
59989
60102
|
var LP_SCOPE = "lp:write";
|
|
59990
60103
|
var CONFIRM_POLL_MS3 = 2000;
|
|
@@ -60916,6 +61029,7 @@ function hyperliquidRelayBody(typedData) {
|
|
|
60916
61029
|
// src/commands/perps.ts
|
|
60917
61030
|
init_render();
|
|
60918
61031
|
init_trading();
|
|
61032
|
+
init_swap();
|
|
60919
61033
|
var PERPS_SCOPE = "perps:write";
|
|
60920
61034
|
var DEC_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
|
|
60921
61035
|
var CLOID_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
@@ -62508,8 +62622,12 @@ async function portfolio(args, ctx) {
|
|
|
62508
62622
|
});
|
|
62509
62623
|
if ("error" in parsed)
|
|
62510
62624
|
return usage(ctx, parsed.error);
|
|
62511
|
-
if (parsed.positionals.length >
|
|
62512
|
-
return usage(ctx, `Unexpected argument: ${parsed.positionals[
|
|
62625
|
+
if (parsed.positionals.length > 1)
|
|
62626
|
+
return usage(ctx, `Unexpected argument: ${parsed.positionals[1]}`);
|
|
62627
|
+
const filter = parsed.positionals[0];
|
|
62628
|
+
if (filter !== undefined && filter.trim() === "")
|
|
62629
|
+
return usage(ctx, "The filter is empty.");
|
|
62630
|
+
const keep = (wallet) => filter === undefined || matches(wallet, filter);
|
|
62513
62631
|
const { deps } = ctx;
|
|
62514
62632
|
const chainFlag = parsed.values["--chain"];
|
|
62515
62633
|
if (chainFlag !== undefined && !CHAINS2.includes(chainFlag)) {
|
|
@@ -62550,6 +62668,7 @@ async function portfolio(args, ctx) {
|
|
|
62550
62668
|
let vaultEntries;
|
|
62551
62669
|
let evmEntries;
|
|
62552
62670
|
let vaultReason;
|
|
62671
|
+
let known = 0;
|
|
62553
62672
|
const raw = await readVaultRaw(resolvedVault.path);
|
|
62554
62673
|
if (raw === null) {
|
|
62555
62674
|
vaultReason = `no vault at ${resolvedVault.path}`;
|
|
@@ -62561,6 +62680,9 @@ async function portfolio(args, ctx) {
|
|
|
62561
62680
|
const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
|
|
62562
62681
|
vaultEntries = showSolana ? vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role })) : [];
|
|
62563
62682
|
evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
|
|
62683
|
+
known += vaultEntries.length + evmEntries.length;
|
|
62684
|
+
vaultEntries = vaultEntries.filter(keep);
|
|
62685
|
+
evmEntries = evmEntries.filter(keep);
|
|
62564
62686
|
}
|
|
62565
62687
|
await printIdentity(ctx);
|
|
62566
62688
|
const rpcHost = solana?.endpoint.host;
|
|
@@ -62603,7 +62725,9 @@ async function portfolio(args, ctx) {
|
|
|
62603
62725
|
writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
|
|
62604
62726
|
return 1;
|
|
62605
62727
|
}
|
|
62606
|
-
const
|
|
62728
|
+
const answered = candle.body;
|
|
62729
|
+
known += (showSolana ? (answered.tee ?? []).length + (answered.embedded ?? []).length : 0) + (showHood ? (answered.hood?.tee ?? []).length + (answered.hood?.embedded ?? []).length : 0);
|
|
62730
|
+
const fromCandle = filter === undefined ? answered : filterCandle(answered, keep);
|
|
62607
62731
|
const prices = { ...fromCandle.prices ?? {} };
|
|
62608
62732
|
const candleHood = showHood ? fromCandle.hood : undefined;
|
|
62609
62733
|
let priceFailure;
|
|
@@ -62750,9 +62874,10 @@ async function portfolio(args, ctx) {
|
|
|
62750
62874
|
};
|
|
62751
62875
|
const unavailable = chains.flatMap((chain2) => unavailableByChain[chain2]);
|
|
62752
62876
|
const lpUnread = (lpSection?.unreadable ?? []).length;
|
|
62753
|
-
const
|
|
62754
|
-
const
|
|
62755
|
-
const
|
|
62877
|
+
const listStatus = walletListStatus(answered);
|
|
62878
|
+
const shownUnread = unavailable.length > 0 || lpUnread > 0;
|
|
62879
|
+
const listNote = listStatus === "cut-off" ? "cut-off" : listStatus === "unknown" && !shownUnread ? "unknown" : null;
|
|
62880
|
+
const complete = listNote === null && !shownUnread;
|
|
62756
62881
|
const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
|
|
62757
62882
|
const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
|
|
62758
62883
|
const lp = lpSection ? {
|
|
@@ -62772,26 +62897,31 @@ async function portfolio(args, ctx) {
|
|
|
62772
62897
|
}
|
|
62773
62898
|
const hoodTotals = byChain.hood;
|
|
62774
62899
|
if (hoodTotals) {
|
|
62775
|
-
const
|
|
62776
|
-
|
|
62777
|
-
|
|
62900
|
+
const counted = filter === undefined ? groups[0]?.wallets ?? [] : all;
|
|
62901
|
+
const reasons = filter === undefined ? { ...candleHood?.unpricedByReason ?? {} } : {};
|
|
62902
|
+
let localUnpriced = 0;
|
|
62903
|
+
for (const w of counted) {
|
|
62778
62904
|
if (w.chain !== "hood")
|
|
62779
62905
|
continue;
|
|
62780
62906
|
for (const h of w.holdings ?? []) {
|
|
62781
62907
|
if (h.priceUsd !== null)
|
|
62782
62908
|
continue;
|
|
62783
|
-
|
|
62909
|
+
localUnpriced += 1;
|
|
62784
62910
|
const reason = h.unpricedReason ?? "source-unavailable";
|
|
62785
62911
|
reasons[reason] = (reasons[reason] ?? 0) + 1;
|
|
62786
62912
|
}
|
|
62787
62913
|
}
|
|
62788
|
-
if (
|
|
62789
|
-
hoodTotals.unpriced =
|
|
62914
|
+
if (filter !== undefined)
|
|
62915
|
+
hoodTotals.unpriced = localUnpriced;
|
|
62916
|
+
else if (candleHood)
|
|
62917
|
+
hoodTotals.unpriced = (candleHood.unpriced ?? 0) + localUnpriced;
|
|
62790
62918
|
hoodTotals.unpricedByReason = reasons;
|
|
62791
62919
|
}
|
|
62920
|
+
const matched = filter === undefined ? undefined : { filter, matched: all.length, wallets: known };
|
|
62792
62921
|
if (ctx.json) {
|
|
62793
62922
|
writeJson(deps, {
|
|
62794
62923
|
ok: true,
|
|
62924
|
+
...matched ?? {},
|
|
62795
62925
|
chains,
|
|
62796
62926
|
totalUsd,
|
|
62797
62927
|
unpriced,
|
|
@@ -62806,18 +62936,79 @@ async function portfolio(args, ctx) {
|
|
|
62806
62936
|
});
|
|
62807
62937
|
return complete ? 0 : 3;
|
|
62808
62938
|
}
|
|
62939
|
+
if (matched) {
|
|
62940
|
+
deps.stdout.write(matched.matched === 0 ? `No wallet matches "${terminalText(matched.filter)}". ${matched.wallets} ${matched.wallets === 1 ? "wallet" : "wallets"} on this account and vault; candle portfolio with no filter shows them.
|
|
62941
|
+
` : `${matched.matched} of ${matched.wallets} wallets match "${terminalText(matched.filter)}". Every figure below is for those wallets only.
|
|
62942
|
+
`);
|
|
62943
|
+
if (matched.matched === 0) {
|
|
62944
|
+
writeListNote(deps.stdout, listNote);
|
|
62945
|
+
return complete ? 0 : 3;
|
|
62946
|
+
}
|
|
62947
|
+
}
|
|
62809
62948
|
writeTable(ctx, groups, {
|
|
62810
62949
|
totalUsd,
|
|
62811
62950
|
unpriced,
|
|
62812
62951
|
chains,
|
|
62813
62952
|
byChain,
|
|
62814
62953
|
unavailableByChain,
|
|
62815
|
-
|
|
62954
|
+
listNote,
|
|
62816
62955
|
lp
|
|
62817
62956
|
});
|
|
62818
62957
|
return complete ? 0 : 3;
|
|
62819
62958
|
});
|
|
62820
62959
|
}
|
|
62960
|
+
function matches(wallet, filter) {
|
|
62961
|
+
const needle = filter.toLowerCase();
|
|
62962
|
+
return (wallet.label ?? "").toLowerCase().includes(needle) || wallet.address.toLowerCase().includes(needle);
|
|
62963
|
+
}
|
|
62964
|
+
function filterCandle(body, keep) {
|
|
62965
|
+
const tee = (body.tee ?? []).filter(keep);
|
|
62966
|
+
const embedded = (body.embedded ?? []).filter(keep);
|
|
62967
|
+
const kept = new Set([...tee, ...embedded].map((row) => row.address));
|
|
62968
|
+
const hoodTee = (body.hood?.tee ?? []).filter(keep);
|
|
62969
|
+
const hoodEmbedded = (body.hood?.embedded ?? []).filter(keep);
|
|
62970
|
+
const hoodKept = new Set([...hoodTee, ...hoodEmbedded].map((row) => row.address));
|
|
62971
|
+
return {
|
|
62972
|
+
...body,
|
|
62973
|
+
tee,
|
|
62974
|
+
embedded,
|
|
62975
|
+
unavailable: (body.unavailable ?? []).filter((address) => kept.has(address)),
|
|
62976
|
+
...body.lp ? {
|
|
62977
|
+
lp: {
|
|
62978
|
+
...body.lp,
|
|
62979
|
+
positions: body.lp.positions.filter((position) => kept.has(position.wallet)),
|
|
62980
|
+
unreadable: body.lp.unreadable.filter((position) => kept.has(position.wallet))
|
|
62981
|
+
}
|
|
62982
|
+
} : {},
|
|
62983
|
+
...body.hood ? {
|
|
62984
|
+
hood: {
|
|
62985
|
+
tee: hoodTee,
|
|
62986
|
+
embedded: hoodEmbedded,
|
|
62987
|
+
unavailable: (body.hood.unavailable ?? []).filter((address) => hoodKept.has(address)),
|
|
62988
|
+
unpriced: 0,
|
|
62989
|
+
unpricedByReason: {}
|
|
62990
|
+
}
|
|
62991
|
+
} : {}
|
|
62992
|
+
};
|
|
62993
|
+
}
|
|
62994
|
+
function walletListStatus(answered) {
|
|
62995
|
+
if (typeof answered.walletsComplete === "boolean")
|
|
62996
|
+
return answered.walletsComplete ? "complete" : "cut-off";
|
|
62997
|
+
if (answered.complete !== false)
|
|
62998
|
+
return "complete";
|
|
62999
|
+
const readFailed = (answered.unavailable ?? []).length > 0 || (answered.hood?.unavailable ?? []).length > 0 || (answered.lp?.unreadable ?? []).length > 0;
|
|
63000
|
+
return readFailed ? "unknown" : "cut-off";
|
|
63001
|
+
}
|
|
63002
|
+
var LIST_CUT_OFF_LINE = `Candle listed only part of this account's wallets. The total is partial.
|
|
63003
|
+
`;
|
|
63004
|
+
var LIST_UNKNOWN_LINE = `Candle did not say whether it listed every wallet. One it left out would be missing here, so this is not a complete result.
|
|
63005
|
+
`;
|
|
63006
|
+
function writeListNote(stdout, note) {
|
|
63007
|
+
if (note === "cut-off")
|
|
63008
|
+
stdout.write(LIST_CUT_OFF_LINE);
|
|
63009
|
+
else if (note === "unknown")
|
|
63010
|
+
stdout.write(LIST_UNKNOWN_LINE);
|
|
63011
|
+
}
|
|
62821
63012
|
async function startEvmRead(addresses, rpcUrl, fetchFn) {
|
|
62822
63013
|
const rpc = createEvmRpc(rpcUrl, fetchFn);
|
|
62823
63014
|
const host = rpcHostOf2(rpcUrl);
|
|
@@ -63128,10 +63319,7 @@ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEE
|
|
|
63128
63319
|
deps.stdout.write(`${unavailable} ${unavailable === 1 ? "wallet" : "wallets"} could not be read in full (${unreadByChain.map(([chain2, n]) => `${n} on ${CHAIN_NAMES[chain2]}`).join(", ")}). What was not read is marked "not read" and is not in the total.
|
|
63129
63320
|
`);
|
|
63130
63321
|
}
|
|
63131
|
-
|
|
63132
|
-
deps.stdout.write(`Candle listed only part of this account's wallets. The total is partial.
|
|
63133
|
-
`);
|
|
63134
|
-
}
|
|
63322
|
+
writeListNote(deps.stdout, totals.listNote);
|
|
63135
63323
|
const truncated = groups.flatMap((g) => g.wallets).filter((w) => w.truncated).length;
|
|
63136
63324
|
if (truncated > 0) {
|
|
63137
63325
|
deps.stdout.write(`${truncated} Hood ${truncated === 1 ? "wallet holds" : "wallets hold"} more tokens than Candle lists. The rest are not shown and not in the total.
|
|
@@ -63953,6 +64141,7 @@ async function signMessage2(args, ctx) {
|
|
|
63953
64141
|
}
|
|
63954
64142
|
|
|
63955
64143
|
// src/index.ts
|
|
64144
|
+
init_swap();
|
|
63956
64145
|
init_tee();
|
|
63957
64146
|
init_tee_rebind();
|
|
63958
64147
|
|
|
@@ -63962,6 +64151,7 @@ init_evm_lite();
|
|
|
63962
64151
|
init_profiles();
|
|
63963
64152
|
init_render();
|
|
63964
64153
|
init_trading();
|
|
64154
|
+
init_swap();
|
|
63965
64155
|
var USAGE4 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
|
|
63966
64156
|
var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
|
|
63967
64157
|
var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
@@ -63999,10 +64189,10 @@ async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
|
|
|
63999
64189
|
}
|
|
64000
64190
|
const rowChain2 = hood ? "evm" : "solana";
|
|
64001
64191
|
const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === rowChain2 && row.revokedAt === undefined && !same(row.address, source.address));
|
|
64002
|
-
const
|
|
64003
|
-
if (
|
|
64004
|
-
throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${
|
|
64005
|
-
const match =
|
|
64192
|
+
const matches2 = rows.filter((row) => row.label === to || row._id === to || same(row.address, to));
|
|
64193
|
+
if (matches2.length > 1)
|
|
64194
|
+
throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches2.length} linked wallets: ${matches2.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
|
|
64195
|
+
const match = matches2[0];
|
|
64006
64196
|
if (match)
|
|
64007
64197
|
return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
|
|
64008
64198
|
if (same(to, source.address))
|
|
@@ -68149,7 +68339,7 @@ async function vaultList(args, ctx) {
|
|
|
68149
68339
|
throw missingVault(ctx, resolvedVault);
|
|
68150
68340
|
const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
|
|
68151
68341
|
const all = vault.index.entries;
|
|
68152
|
-
const matched = filter === undefined ? all : all.filter((entry) =>
|
|
68342
|
+
const matched = filter === undefined ? all : all.filter((entry) => matches2(entry, filter));
|
|
68153
68343
|
const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
|
|
68154
68344
|
const requests = Math.ceil(solana.length / CHUNK2);
|
|
68155
68345
|
const rpcHost = solanaClient?.endpoint.host;
|
|
@@ -68284,7 +68474,7 @@ total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.len
|
|
|
68284
68474
|
return complete ? 0 : 3;
|
|
68285
68475
|
});
|
|
68286
68476
|
}
|
|
68287
|
-
function
|
|
68477
|
+
function matches2(entry, filter) {
|
|
68288
68478
|
const needle = filter.toLowerCase();
|
|
68289
68479
|
return entry.label.toLowerCase().includes(needle) || entry.address.toLowerCase().includes(needle);
|
|
68290
68480
|
}
|
|
@@ -70346,8 +70536,8 @@ async function runRebindPhase(ctx, phase, results) {
|
|
|
70346
70536
|
async function verifySubjectSecret(vault, subject) {
|
|
70347
70537
|
const secret = await decryptKey(vault, subject.id);
|
|
70348
70538
|
try {
|
|
70349
|
-
const
|
|
70350
|
-
if (!
|
|
70539
|
+
const matches3 = subject.chain === "evm" ? sameEvmAddress(evmAddressFromSecret(secret), subject.address) : addressFromSecret64(secret) === subject.address;
|
|
70540
|
+
if (!matches3) {
|
|
70351
70541
|
throw new VaultError("VAULT_VERIFY_FAILED", "Stored secret does not match the subject address.");
|
|
70352
70542
|
}
|
|
70353
70543
|
} catch (error) {
|
|
@@ -70837,7 +71027,7 @@ New vault at ${path}, verified in full (all eight steps).
|
|
|
70837
71027
|
const solana = rpcUrlFlag === undefined ? undefined : solanaClientFor(ctx, flagEndpoint(rpcUrlFlag));
|
|
70838
71028
|
const derived = await deriveWithinBounds(ctx, vault, counts, solana);
|
|
70839
71029
|
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
70840
|
-
let
|
|
71030
|
+
let matches3 = {
|
|
70841
71031
|
matched: [],
|
|
70842
71032
|
unmatched: [],
|
|
70843
71033
|
account: undefined,
|
|
@@ -70845,16 +71035,16 @@ New vault at ${path}, verified in full (all eight steps).
|
|
|
70845
71035
|
reason: "no API key is stored, so no exposure could be read"
|
|
70846
71036
|
};
|
|
70847
71037
|
if (apiKey !== undefined) {
|
|
70848
|
-
|
|
71038
|
+
matches3 = await matchAgainstAccount(ctx, apiKey, derived);
|
|
70849
71039
|
} else {
|
|
70850
71040
|
deps.stderr.write(`No API key is stored for this profile, so this restore read no exposure at all.
|
|
70851
71041
|
`);
|
|
70852
71042
|
}
|
|
70853
|
-
const outcome = await writeRestoredIndex(ctx, vault, derived,
|
|
71043
|
+
const outcome = await writeRestoredIndex(ctx, vault, derived, matches3, counts);
|
|
70854
71044
|
committed = true;
|
|
70855
71045
|
const config = await deps.readConfig();
|
|
70856
71046
|
const ambient = Boolean(deps.env[RPC_URL_ENV]?.trim()) || Boolean(ctx.profile !== undefined && config.profiles?.[ctx.profile]?.rpcUrl?.trim());
|
|
70857
|
-
const result = reportRestore(ctx, vault, derived,
|
|
71047
|
+
const result = reportRestore(ctx, vault, derived, matches3, outcome, counts, ambient);
|
|
70858
71048
|
const footer = nonDefaultVaultFooter(resolvedVault);
|
|
70859
71049
|
if (footer !== undefined)
|
|
70860
71050
|
deps.stdout.write(footer);
|
|
@@ -71071,9 +71261,9 @@ This profile acts as account ${read.account}.
|
|
|
71071
71261
|
}
|
|
71072
71262
|
return { matched, unmatched, externalListed, account: read.account, complete: true };
|
|
71073
71263
|
}
|
|
71074
|
-
async function writeRestoredIndex(ctx, vault, derived,
|
|
71264
|
+
async function writeRestoredIndex(ctx, vault, derived, matches3, counts) {
|
|
71075
71265
|
const now = new Date(ctx.deps.now()).toISOString();
|
|
71076
|
-
const matchByAddress = new Map(
|
|
71266
|
+
const matchByAddress = new Map(matches3.matched.map((match) => [match.entry.address, match.row]));
|
|
71077
71267
|
const entries = derived.entries.map((entry) => {
|
|
71078
71268
|
const row = matchByAddress.get(entry.address);
|
|
71079
71269
|
if (entry.branch === "evmTee") {
|
|
@@ -71143,7 +71333,7 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
71143
71333
|
exposed[located.branch].push(located.index);
|
|
71144
71334
|
}
|
|
71145
71335
|
const highestMatched = { solanaVault: -1, solanaTee: -1, solanaExternal: -1 };
|
|
71146
|
-
for (const match of
|
|
71336
|
+
for (const match of matches3.matched) {
|
|
71147
71337
|
if (match.entry.branch === "solanaVault")
|
|
71148
71338
|
highestMatched.solanaVault = Math.max(highestMatched.solanaVault, match.entry.index);
|
|
71149
71339
|
if (match.entry.branch === "solanaTee")
|
|
@@ -71168,12 +71358,12 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
71168
71358
|
},
|
|
71169
71359
|
discovery: {
|
|
71170
71360
|
restoredAt: now,
|
|
71171
|
-
account:
|
|
71361
|
+
account: matches3.account ?? "",
|
|
71172
71362
|
requestedCounts: counts.requested,
|
|
71173
71363
|
highestMatched,
|
|
71174
71364
|
complete: false
|
|
71175
71365
|
},
|
|
71176
|
-
...
|
|
71366
|
+
...matches3.complete ? { exposureReconciledAt: now } : {}
|
|
71177
71367
|
};
|
|
71178
71368
|
await commitVault(vault, { index: { hd, entries }, addKeys: derived.entries.map((entry) => entry.blob) }, ctx.deps);
|
|
71179
71369
|
return { entries, hd };
|
|
@@ -71201,7 +71391,7 @@ function teeFieldsFor(row, ctx, network2 = "solana-mainnet") {
|
|
|
71201
71391
|
}
|
|
71202
71392
|
return { role: "tee-wallet", tee: { ...common, lifecycle: "stranded", grantIdentity } };
|
|
71203
71393
|
}
|
|
71204
|
-
function reportRestore(ctx, vault, derived,
|
|
71394
|
+
function reportRestore(ctx, vault, derived, matches3, outcome, counts, ambientSolanaEndpoint) {
|
|
71205
71395
|
const { deps } = ctx;
|
|
71206
71396
|
deps.stdout.write(`
|
|
71207
71397
|
Recovered ${outcome.entries.length} address(es) from the phrase.
|
|
@@ -71244,29 +71434,29 @@ Every one of them is recorded with an unknown history and stays that way: this p
|
|
|
71244
71434
|
deps.stdout.write(` evm tee: indices 0 to ${counts.evmTee - 1}, on m/44'/60'/n'/1'/0'. The sealed EVM record is not restored by a phrase: a sweep of these wallets needs --from-block <n> (a Hood block at or before the first transfer in) or --token <0x...> to find tokens beyond USDG and WETH.
|
|
71245
71435
|
`);
|
|
71246
71436
|
}
|
|
71247
|
-
if ((
|
|
71437
|
+
if ((matches3.externalListed?.length ?? 0) > 0) {
|
|
71248
71438
|
deps.stdout.write(`
|
|
71249
|
-
${
|
|
71439
|
+
${matches3.externalListed?.length} address(es) this account imported are external-branch keys of this root. An external key is never registered with Candle, so this read does not flag them; their history is unknown like every other recovered key's.
|
|
71250
71440
|
`);
|
|
71251
71441
|
}
|
|
71252
|
-
if (!
|
|
71253
|
-
if (
|
|
71442
|
+
if (!matches3.complete) {
|
|
71443
|
+
if (matches3.refused !== undefined)
|
|
71254
71444
|
deps.stdout.write(`
|
|
71255
|
-
${
|
|
71445
|
+
${matches3.refused}: the account was not confirmed.
|
|
71256
71446
|
`);
|
|
71257
71447
|
deps.stdout.write(`
|
|
71258
|
-
No exposure was recorded: ${
|
|
71448
|
+
No exposure was recorded: ${matches3.reason ?? "the linked-wallet read did not complete"}.
|
|
71259
71449
|
`);
|
|
71260
|
-
deps.stdout.write(
|
|
71450
|
+
deps.stdout.write(matches3.refused !== undefined ? `The vault was created, verified and written in full; nothing in it is flagged. Run \`candle vault reconcile-exposure\` once you are on the right profile.
|
|
71261
71451
|
` : `A partial list that happened to be empty would read as good news, so nothing was flagged at all. Run \`candle vault reconcile-exposure\` once the read succeeds.
|
|
71262
71452
|
`);
|
|
71263
71453
|
return 3;
|
|
71264
71454
|
}
|
|
71265
|
-
if (
|
|
71455
|
+
if (matches3.unmatched.length > 0) {
|
|
71266
71456
|
deps.stdout.write(`
|
|
71267
|
-
${
|
|
71457
|
+
${matches3.unmatched.length} address(es) this account imported were NOT derived by this restore:
|
|
71268
71458
|
`);
|
|
71269
|
-
for (const address of
|
|
71459
|
+
for (const address of matches3.unmatched)
|
|
71270
71460
|
deps.stdout.write(` ${address}
|
|
71271
71461
|
`);
|
|
71272
71462
|
deps.stdout.write(`
|
|
@@ -71278,7 +71468,7 @@ Either this root derives them at an index beyond the bounds used here, in which
|
|
|
71278
71468
|
}
|
|
71279
71469
|
closeVault(vault);
|
|
71280
71470
|
deps.stdout.write(`
|
|
71281
|
-
${
|
|
71471
|
+
${matches3.matched.length} of them are addresses this account imported, and each is flagged as remotely exposed.
|
|
71282
71472
|
`);
|
|
71283
71473
|
return 0;
|
|
71284
71474
|
}
|
|
@@ -72141,6 +72331,7 @@ init_wallets();
|
|
|
72141
72331
|
init_args();
|
|
72142
72332
|
init_render();
|
|
72143
72333
|
init_trading();
|
|
72334
|
+
init_swap();
|
|
72144
72335
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
72145
72336
|
var USAGE6 = "Usage: candle wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
|
|
72146
72337
|
var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|