@candledottv/cli 0.11.7 → 0.11.9
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 +134 -12
- package/dist/index.js +2560 -433
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -278,6 +278,16 @@ var init_args = __esm(() => {
|
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
// src/agent-key-access.ts
|
|
281
|
+
function scopesForPreset(preset) {
|
|
282
|
+
switch (preset) {
|
|
283
|
+
case "read":
|
|
284
|
+
return [...READ_SCOPES];
|
|
285
|
+
case "readwrite":
|
|
286
|
+
return [...READWRITE_SCOPES];
|
|
287
|
+
case "readwritetransfer":
|
|
288
|
+
return [...READWRITETRANSFER_SCOPES];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
281
291
|
function sameSet(scopes, preset) {
|
|
282
292
|
const held = new Set(scopes);
|
|
283
293
|
return held.size === preset.length && preset.every((scope) => held.has(scope));
|
|
@@ -287,6 +297,8 @@ function presetForScopes(scopes) {
|
|
|
287
297
|
return "read";
|
|
288
298
|
if (sameSet(scopes, READWRITE_SCOPES))
|
|
289
299
|
return "readwrite";
|
|
300
|
+
if (sameSet(scopes, READWRITETRANSFER_SCOPES))
|
|
301
|
+
return "readwritetransfer";
|
|
290
302
|
return null;
|
|
291
303
|
}
|
|
292
304
|
function agentKeyCapabilities(scopes) {
|
|
@@ -295,6 +307,7 @@ function agentKeyCapabilities(scopes) {
|
|
|
295
307
|
launch: scopes.includes("launch:write"),
|
|
296
308
|
trade: scopes.includes("swap:write"),
|
|
297
309
|
transfer: scopes.includes("transfer:write"),
|
|
310
|
+
linkedTransfer: scopes.includes("transfer:bound"),
|
|
298
311
|
report: scopes.includes("activity:write")
|
|
299
312
|
};
|
|
300
313
|
}
|
|
@@ -308,7 +321,7 @@ function agentKeyAccess(scopes) {
|
|
|
308
321
|
function sortAgentKeyScopes(scopes) {
|
|
309
322
|
return [...scopes].sort();
|
|
310
323
|
}
|
|
311
|
-
var READ_SCOPES, READWRITE_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
|
|
324
|
+
var READ_SCOPES, READWRITE_SCOPES, READWRITETRANSFER_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
|
|
312
325
|
var init_agent_key_access = __esm(() => {
|
|
313
326
|
READ_SCOPES = ["account:read"];
|
|
314
327
|
READWRITE_SCOPES = [
|
|
@@ -319,18 +332,32 @@ var init_agent_key_access = __esm(() => {
|
|
|
319
332
|
"transfer:write",
|
|
320
333
|
"account:read"
|
|
321
334
|
];
|
|
322
|
-
|
|
335
|
+
READWRITETRANSFER_SCOPES = [
|
|
336
|
+
"launch:write",
|
|
337
|
+
"launch:read",
|
|
338
|
+
"activity:write",
|
|
339
|
+
"swap:write",
|
|
340
|
+
"transfer:write",
|
|
341
|
+
"account:read",
|
|
342
|
+
"transfer:bound"
|
|
343
|
+
];
|
|
344
|
+
AGENT_KEY_PRESET_LABELS = {
|
|
345
|
+
read: "Read",
|
|
346
|
+
readwrite: "Read:Write",
|
|
347
|
+
readwritetransfer: "Read:Write:Transfer"
|
|
348
|
+
};
|
|
323
349
|
AGENT_KEY_CAPABILITY_CHIPS = [
|
|
324
350
|
["Launch", "launch"],
|
|
325
351
|
["Trade", "trade"],
|
|
326
352
|
["Transfer", "transfer"],
|
|
353
|
+
["Linked transfer", "linkedTransfer"],
|
|
327
354
|
["Report", "report"]
|
|
328
355
|
];
|
|
329
356
|
});
|
|
330
357
|
|
|
331
358
|
// src/render.ts
|
|
332
359
|
function formatScopesForSummary(scopes) {
|
|
333
|
-
return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
|
|
360
|
+
return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope).join(", ");
|
|
334
361
|
}
|
|
335
362
|
function terminalText(value) {
|
|
336
363
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
@@ -436,7 +463,7 @@ function portalDeviceUrl(apiUrl, portalOrigin) {
|
|
|
436
463
|
return `${apiUrl}/agents`;
|
|
437
464
|
}
|
|
438
465
|
}
|
|
439
|
-
var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets";
|
|
466
|
+
var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets", TRANSFER_BOUND_NOTE = "moves funds -- this key can move funds out of the wallet it runs, to your linked wallets and its vault";
|
|
440
467
|
var init_render = __esm(() => {
|
|
441
468
|
init_agent_key_access();
|
|
442
469
|
ALL_AGENT_SCOPES = [
|
|
@@ -445,9 +472,10 @@ var init_render = __esm(() => {
|
|
|
445
472
|
"account:read",
|
|
446
473
|
"activity:write",
|
|
447
474
|
"swap:write",
|
|
448
|
-
"transfer:write"
|
|
475
|
+
"transfer:write",
|
|
476
|
+
"transfer:bound"
|
|
449
477
|
];
|
|
450
|
-
DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write");
|
|
478
|
+
DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write" && scope !== "transfer:bound");
|
|
451
479
|
});
|
|
452
480
|
|
|
453
481
|
// src/profiles.ts
|
|
@@ -1021,6 +1049,9 @@ function createView(arr) {
|
|
|
1021
1049
|
function rotr(word, shift) {
|
|
1022
1050
|
return word << 32 - shift | word >>> shift;
|
|
1023
1051
|
}
|
|
1052
|
+
function rotl(word, shift) {
|
|
1053
|
+
return word << shift | word >>> 32 - shift >>> 0;
|
|
1054
|
+
}
|
|
1024
1055
|
function byteSwap(word) {
|
|
1025
1056
|
return word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255;
|
|
1026
1057
|
}
|
|
@@ -2086,7 +2117,7 @@ var gcd = (a, b) => b === 0 ? a : gcd(b, a % b), radix2carry = (from, to) => fro
|
|
|
2086
2117
|
if (s.length > 0 && !re.test(s))
|
|
2087
2118
|
throw new Error("invalid base64");
|
|
2088
2119
|
return Uint8Array.fromBase64(s, { alphabet: alphabet2, lastChunkHandling: "strict" });
|
|
2089
|
-
}, base64, base64nopad, base64url, base64urlnopad, genBase58 = (abc) => chain(radix(58), alphabet(abc), join3("")), base58, base58flickr, base58xrp, BECH_ALPHABET, POLYMOD_GENERATORS, bech32, bech32m, hasHexBuiltin2, hexBuiltin, hex;
|
|
2120
|
+
}, base64, base64nopad, base64url, base64urlnopad, genBase58 = (abc) => chain(radix(58), alphabet(abc), join3("")), base58, base58flickr, base58xrp, createBase58check = (sha2563) => chain(checksum(4, (data) => sha2563(sha2563(data))), base58), BECH_ALPHABET, POLYMOD_GENERATORS, bech32, bech32m, hasHexBuiltin2, hexBuiltin, hex;
|
|
2090
2121
|
var init_esm = __esm(() => {
|
|
2091
2122
|
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
2092
2123
|
powers = /* @__PURE__ */ (() => {
|
|
@@ -2164,6 +2195,9 @@ function libraryInstallInstruction(platform) {
|
|
|
2164
2195
|
}
|
|
2165
2196
|
|
|
2166
2197
|
// src/fido2-helper/protocol.ts
|
|
2198
|
+
function excludeCapacity(device) {
|
|
2199
|
+
return device.maxCredentialCountInList ?? 1;
|
|
2200
|
+
}
|
|
2167
2201
|
var HELPER_PROTOCOL = 1, RP_ID = "cli.candle.tv", PROBE_MAX_CREDENTIALS = 16, AUTHDATA_FLAG_UP = 1, AUTHDATA_FLAG_UV = 4, AUTHDATA_MIN_LENGTH = 37;
|
|
2168
2202
|
var init_protocol = () => {};
|
|
2169
2203
|
|
|
@@ -3144,7 +3178,19 @@ var init_errors = __esm(() => {
|
|
|
3144
3178
|
"TIER_REQUIRED",
|
|
3145
3179
|
"LINKED_WALLET_ROOM_UNREADABLE",
|
|
3146
3180
|
"PROMOTE_ACCOUNT_UNRESOLVED",
|
|
3147
|
-
"TRANSFER_SWEEP_PENDING"
|
|
3181
|
+
"TRANSFER_SWEEP_PENDING",
|
|
3182
|
+
"VAULT_KEY_ALREADY_ENROLLED",
|
|
3183
|
+
"EVM_CHAIN_MISMATCH",
|
|
3184
|
+
"EVM_NONCE_STALE",
|
|
3185
|
+
"EVM_TOKEN_UNREADABLE",
|
|
3186
|
+
"EVM_AMOUNT_PRECISION",
|
|
3187
|
+
"EVM_INSUFFICIENT_FOR_FEES",
|
|
3188
|
+
"EVM_DESTINATION_INVALID",
|
|
3189
|
+
"EVM_SELF_TRANSFER",
|
|
3190
|
+
"EVM_RECIPIENT_IS_TOKEN",
|
|
3191
|
+
"EVM_TRANSFER_REVERTED",
|
|
3192
|
+
"TRANSFER_CHAIN_MISMATCH",
|
|
3193
|
+
"SOLANA_COMMAND_EVM_KEY"
|
|
3148
3194
|
];
|
|
3149
3195
|
VaultError = class VaultError extends Error {
|
|
3150
3196
|
code;
|
|
@@ -5957,6 +6003,9 @@ function translateHelperFailure(code, message, platform) {
|
|
|
5957
6003
|
LIBRARY_MISSING: "VAULT_HELPER_MISSING"
|
|
5958
6004
|
};
|
|
5959
6005
|
const suggestion = "Nothing was derived and no other factor was tried.";
|
|
6006
|
+
if (code === "CREDENTIAL_EXCLUDED") {
|
|
6007
|
+
return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `This security key already holds one of this vault's credentials, so enrolling it again would not add a second key (${message}). Nothing was written.`, { suggestion: ALREADY_ENROLLED_SUGGESTION });
|
|
6008
|
+
}
|
|
5960
6009
|
if (code === "DEVICE_NOT_READABLE") {
|
|
5961
6010
|
return new VaultError("VAULT_AUTHENTICATOR_NOT_READABLE", platform === "linux" ? HIDRAW_MESSAGE : `A security key is attached but this user cannot open it: ${message}.`, { suggestion: "No other factor is substituted." });
|
|
5962
6011
|
}
|
|
@@ -6017,6 +6066,51 @@ async function callHelper(deps, helperPath, request) {
|
|
|
6017
6066
|
throw translateHelperFailure(String(response.code), String(response.message), deps.platform);
|
|
6018
6067
|
return response;
|
|
6019
6068
|
}
|
|
6069
|
+
function alreadyEnrolled(device, envelopeId) {
|
|
6070
|
+
const product = device.product || "This security key";
|
|
6071
|
+
return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `${product} (--device ${device.deviceId}) already holds this vault's security key credential${envelopeId !== undefined ? ` for envelope ${envelopeId}` : ""}, so enrolling it again would not add a second key. Nothing was written.`, {
|
|
6072
|
+
suggestion: ALREADY_ENROLLED_SUGGESTION,
|
|
6073
|
+
...envelopeId !== undefined ? { details: { envelopeId } } : {}
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
function helperPredatesExcludeList(helperPath, leftover) {
|
|
6077
|
+
return new VaultError("VAULT_HELPER_MISSING", `The security key helper at ${helperPath} predates the exclude list this command needs; reinstall the CLI so candle and candle-fido2 come from the same release.`, {
|
|
6078
|
+
suggestion: leftover ? "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again." : "Nothing was sent to any key and nothing was written. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again."
|
|
6079
|
+
});
|
|
6080
|
+
}
|
|
6081
|
+
function assertHelperExcludeList(info, helperPath, excludeCount) {
|
|
6082
|
+
if (excludeCount === 0)
|
|
6083
|
+
return;
|
|
6084
|
+
const features = Array.isArray(info.features) ? info.features : [];
|
|
6085
|
+
if (!features.includes(EXCLUDE_LIST_FEATURE))
|
|
6086
|
+
throw helperPredatesExcludeList(helperPath, false);
|
|
6087
|
+
}
|
|
6088
|
+
function assertExcludeListFits(device, excludeCount) {
|
|
6089
|
+
const capacity = excludeCapacity(device);
|
|
6090
|
+
if (excludeCount <= capacity)
|
|
6091
|
+
return;
|
|
6092
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${device.product || "This security key"} (--device ${device.deviceId}) takes at most ${capacity} credential${capacity === 1 ? "" : "s"} in one request${device.maxCredentialCountInList === null ? " (it reports no maxCredentialCountInList)" : ""}, and this vault has ${excludeCount} security key credentials to exclude, so it cannot enroll on this vault. Nothing was sent to it.`, { suggestion: "Nothing was written. Use a security key that reports a larger maxCredentialCountInList." });
|
|
6093
|
+
}
|
|
6094
|
+
function assertDeviceServesFactor(device) {
|
|
6095
|
+
if (!device.extensions.includes("hmac-secret")) {
|
|
6096
|
+
throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
|
|
6097
|
+
}
|
|
6098
|
+
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6099
|
+
throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
async function listSecurityKeys(deps, opts) {
|
|
6103
|
+
const location = await locateFido2Helper(deps);
|
|
6104
|
+
if (location.state === "absent")
|
|
6105
|
+
throw helperMissing(location);
|
|
6106
|
+
const info = await callHelper(deps, location.path, {
|
|
6107
|
+
op: "info",
|
|
6108
|
+
vaultId: opts.vaultId,
|
|
6109
|
+
envelopeId: opts.envelopeId,
|
|
6110
|
+
digest: base64.encode(operationDigest({ vaultId: opts.vaultId, envelopeId: opts.envelopeId, op: "info", nonce: b64u(randomBytes2(16)) }))
|
|
6111
|
+
});
|
|
6112
|
+
return { helperPath: location.path, info };
|
|
6113
|
+
}
|
|
6020
6114
|
function describeDeviceForList(device) {
|
|
6021
6115
|
const product = device.product || "security key";
|
|
6022
6116
|
return `--device ${device.deviceId} ${product}${device.manufacturer ? ` (${device.manufacturer})` : ""}${device.readable ? "" : " [not readable by this user]"}`;
|
|
@@ -6099,17 +6193,12 @@ async function openSecurityKeySession(deps, opts) {
|
|
|
6099
6193
|
deps.stderr.write(`Using the attached security key: ${device.product || "security key"} (--device ${device.deviceId})
|
|
6100
6194
|
`);
|
|
6101
6195
|
}
|
|
6102
|
-
if (opts.requireFeatures)
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
}
|
|
6106
|
-
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6107
|
-
throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
|
|
6108
|
-
}
|
|
6109
|
-
}
|
|
6196
|
+
if (opts.requireFeatures)
|
|
6197
|
+
assertDeviceServesFactor(device);
|
|
6198
|
+
opts.beforePin?.(device, info);
|
|
6110
6199
|
const session = { helperPath: location.path, device, snapshotId: info.snapshotId };
|
|
6111
6200
|
if (device.options.clientPin === true) {
|
|
6112
|
-
const typed = await deps.promptSecret(`PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6201
|
+
const typed = await deps.promptSecret(opts.pinPrompt?.(device) ?? `PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6113
6202
|
if (typed === "") {
|
|
6114
6203
|
throw new VaultError("VAULT_PIN_REQUIRED", "This security key needs its PIN and none was typed; nothing was sent to it.");
|
|
6115
6204
|
}
|
|
@@ -6163,7 +6252,7 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6163
6252
|
if (opts.deviceFlag === undefined && allReadable && !uncertain)
|
|
6164
6253
|
state.set(envelopeId, "not attached");
|
|
6165
6254
|
}
|
|
6166
|
-
return { state, holder };
|
|
6255
|
+
return { state, holder, devices, credentials: credentialOf };
|
|
6167
6256
|
} catch {
|
|
6168
6257
|
return;
|
|
6169
6258
|
}
|
|
@@ -6171,20 +6260,37 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6171
6260
|
async function registerCredential(deps, session, opts) {
|
|
6172
6261
|
deps.stderr.write(`Touch ${session.device.product || "the security key"} to register the vault's credential on it.
|
|
6173
6262
|
`);
|
|
6174
|
-
const digest = operationDigest({
|
|
6175
|
-
const response = await callHelper(deps, session.helperPath, {
|
|
6176
|
-
op: "register",
|
|
6263
|
+
const digest = operationDigest({
|
|
6177
6264
|
vaultId: opts.vaultId,
|
|
6178
6265
|
envelopeId: opts.envelopeId,
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
expectSnapshot: session.snapshotId,
|
|
6182
|
-
rpId: RP_ID,
|
|
6183
|
-
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6184
|
-
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6185
|
-
clientDataHash: base64.encode(digest),
|
|
6186
|
-
...session.pin !== undefined ? { pin: session.pin } : {}
|
|
6266
|
+
op: "register",
|
|
6267
|
+
nonce: b64u(randomBytes2(16))
|
|
6187
6268
|
});
|
|
6269
|
+
const exclude = opts.excludeCredentialIds ?? [];
|
|
6270
|
+
let response;
|
|
6271
|
+
try {
|
|
6272
|
+
response = await callHelper(deps, session.helperPath, {
|
|
6273
|
+
op: "register",
|
|
6274
|
+
vaultId: opts.vaultId,
|
|
6275
|
+
envelopeId: opts.envelopeId,
|
|
6276
|
+
digest: base64.encode(digest),
|
|
6277
|
+
deviceId: session.device.deviceId,
|
|
6278
|
+
expectSnapshot: session.snapshotId,
|
|
6279
|
+
rpId: RP_ID,
|
|
6280
|
+
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6281
|
+
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6282
|
+
clientDataHash: base64.encode(digest),
|
|
6283
|
+
...session.pin !== undefined ? { pin: session.pin } : {},
|
|
6284
|
+
...exclude.length > 0 ? { excludeCredentialIds: exclude } : {}
|
|
6285
|
+
});
|
|
6286
|
+
} catch (error) {
|
|
6287
|
+
if (error instanceof VaultError && error.code === "VAULT_KEY_ALREADY_ENROLLED")
|
|
6288
|
+
throw alreadyEnrolled(session.device);
|
|
6289
|
+
throw error;
|
|
6290
|
+
}
|
|
6291
|
+
if (exclude.length > 0 && response.excluded !== exclude.length) {
|
|
6292
|
+
throw helperPredatesExcludeList(session.helperPath, true);
|
|
6293
|
+
}
|
|
6188
6294
|
const authData = base64.decode(response.authData);
|
|
6189
6295
|
assertAuthenticatorData(authData, RP_ID, "registration");
|
|
6190
6296
|
return {
|
|
@@ -6224,7 +6330,7 @@ async function assertPrf(deps, session, envelope, vaultId, purpose) {
|
|
|
6224
6330
|
}
|
|
6225
6331
|
return prfOutput;
|
|
6226
6332
|
}
|
|
6227
|
-
var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.";
|
|
6333
|
+
var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.", ALREADY_ENROLLED_SUGGESTION = "Insert a different security key, or name one with --device.", EXCLUDE_LIST_FEATURE = "exclude-list";
|
|
6228
6334
|
var init_fido2 = __esm(() => {
|
|
6229
6335
|
init_sha256();
|
|
6230
6336
|
init_esm();
|
|
@@ -8669,7 +8775,7 @@ async function keysList(args, ctx) {
|
|
|
8669
8775
|
async function keysCreate(args, ctx) {
|
|
8670
8776
|
const { deps, apiUrl, json } = ctx;
|
|
8671
8777
|
const parsed = parseArgs(args, {
|
|
8672
|
-
valueFlags: ["--scopes", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8778
|
+
valueFlags: ["--scopes", "--access", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8673
8779
|
});
|
|
8674
8780
|
if ("error" in parsed) {
|
|
8675
8781
|
writeUsageFailure(deps, parsed.error, json);
|
|
@@ -8679,7 +8785,20 @@ async function keysCreate(args, ctx) {
|
|
|
8679
8785
|
writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
|
|
8680
8786
|
return 2;
|
|
8681
8787
|
}
|
|
8682
|
-
|
|
8788
|
+
if (parsed.values["--access"] !== undefined && parsed.values["--scopes"] !== undefined) {
|
|
8789
|
+
writeUsageFailure(deps, "--access and --scopes are mutually exclusive; pass one of them.", json);
|
|
8790
|
+
return 2;
|
|
8791
|
+
}
|
|
8792
|
+
let accessScopes;
|
|
8793
|
+
if (parsed.values["--access"] !== undefined) {
|
|
8794
|
+
const preset = ACCESS_LEVELS[parsed.values["--access"]];
|
|
8795
|
+
if (preset === undefined) {
|
|
8796
|
+
writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
|
|
8797
|
+
return 2;
|
|
8798
|
+
}
|
|
8799
|
+
accessScopes = scopesForPreset(preset);
|
|
8800
|
+
}
|
|
8801
|
+
const requestedScopes = accessScopes ?? (parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined);
|
|
8683
8802
|
const environment = parsed.values["--environment"];
|
|
8684
8803
|
const label = parsed.values["--label"]?.trim();
|
|
8685
8804
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > 64)) {
|
|
@@ -8775,7 +8894,7 @@ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError
|
|
|
8775
8894
|
`);
|
|
8776
8895
|
}
|
|
8777
8896
|
if (!requestedScopes) {
|
|
8778
|
-
deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8897
|
+
deps.stdout.write(`No --access or --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8779
8898
|
`);
|
|
8780
8899
|
}
|
|
8781
8900
|
if (storeError === undefined) {
|
|
@@ -8842,7 +8961,7 @@ async function keysRevoke(args, ctx) {
|
|
|
8842
8961
|
}
|
|
8843
8962
|
return 0;
|
|
8844
8963
|
}
|
|
8845
|
-
var KEYS_PATH = "/api/v1/agent/keys", NO_DEVICE_TOKEN;
|
|
8964
|
+
var KEYS_PATH = "/api/v1/agent/keys", ACCESS_LEVELS, NO_DEVICE_TOKEN;
|
|
8846
8965
|
var init_keys = __esm(() => {
|
|
8847
8966
|
init_agent_key_access();
|
|
8848
8967
|
init_args();
|
|
@@ -8850,6 +8969,11 @@ var init_keys = __esm(() => {
|
|
|
8850
8969
|
init_profiles();
|
|
8851
8970
|
init_render();
|
|
8852
8971
|
init_secret_store();
|
|
8972
|
+
ACCESS_LEVELS = {
|
|
8973
|
+
read: "read",
|
|
8974
|
+
"read-write": "readwrite",
|
|
8975
|
+
"read-write-transfer": "readwritetransfer"
|
|
8976
|
+
};
|
|
8853
8977
|
NO_DEVICE_TOKEN = {
|
|
8854
8978
|
code: "NO_DEVICE_TOKEN",
|
|
8855
8979
|
message: "No device token available.",
|
|
@@ -8862,7 +8986,7 @@ function tokenMintFilters(key) {
|
|
|
8862
8986
|
return [{ dataSize: 82 }, memcmp(4, key)];
|
|
8863
8987
|
}
|
|
8864
8988
|
function tokenFreezeFilters(key) {
|
|
8865
|
-
return [{ dataSize: 82 }, memcmp(46,
|
|
8989
|
+
return [{ dataSize: 82 }, memcmp(46, concat3(COPTION_SOME_U32, key))];
|
|
8866
8990
|
}
|
|
8867
8991
|
function token2022MintFilters(key) {
|
|
8868
8992
|
const authority = memcmp(4, key);
|
|
@@ -8872,7 +8996,7 @@ function token2022MintFilters(key) {
|
|
|
8872
8996
|
];
|
|
8873
8997
|
}
|
|
8874
8998
|
function token2022FreezeFilters(key) {
|
|
8875
|
-
const authority = memcmp(46,
|
|
8999
|
+
const authority = memcmp(46, concat3(COPTION_SOME_U32, key));
|
|
8876
9000
|
return [
|
|
8877
9001
|
[{ dataSize: 82 }, authority],
|
|
8878
9002
|
[memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
|
|
@@ -8882,7 +9006,7 @@ function keepSetAuthority(hits) {
|
|
|
8882
9006
|
return hits.filter((hit) => hit.data.length === COPTION_SOME_U32.length && hit.data.every((byte, i) => byte === COPTION_SOME_U32[i])).map((hit) => hit.pubkey);
|
|
8883
9007
|
}
|
|
8884
9008
|
function programUpgradeFilters(key) {
|
|
8885
|
-
return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12,
|
|
9009
|
+
return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat3(COPTION_SOME_U8, key))];
|
|
8886
9010
|
}
|
|
8887
9011
|
function programIdFilters(programData) {
|
|
8888
9012
|
return [memcmp(0, LOADER_PROGRAM_TAG), memcmp(4, programData)];
|
|
@@ -9232,7 +9356,7 @@ function authoritiesJson(result) {
|
|
|
9232
9356
|
found: result.found.map(({ address, role, target, program }) => ({ address, role, target, program }))
|
|
9233
9357
|
};
|
|
9234
9358
|
}
|
|
9235
|
-
var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111", ROLE_GROUP_IDS, PAGINATED_TOKEN_GROUPS, RPC_PAGINATION_REQUIRED = -32600, MAX_V2_PAGES = 10, RPC_METHOD_NOT_FOUND = -32601, REQUESTS_PER_KEY = 9, b642 = (bytes) => base64.encode(bytes),
|
|
9359
|
+
var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111", ROLE_GROUP_IDS, PAGINATED_TOKEN_GROUPS, RPC_PAGINATION_REQUIRED = -32600, MAX_V2_PAGES = 10, RPC_METHOD_NOT_FOUND = -32601, REQUESTS_PER_KEY = 9, b642 = (bytes) => base64.encode(bytes), concat3 = (...parts) => {
|
|
9236
9360
|
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
9237
9361
|
let at = 0;
|
|
9238
9362
|
for (const part of parts) {
|
|
@@ -9354,6 +9478,7 @@ __export(exports_promote_support, {
|
|
|
9354
9478
|
confirmPrompt: () => confirmPrompt,
|
|
9355
9479
|
confirmPromotion: () => confirmPromotion,
|
|
9356
9480
|
assertNotPinnedDestination: () => assertNotPinnedDestination,
|
|
9481
|
+
assertNotEvmEntry: () => assertNotEvmEntry,
|
|
9357
9482
|
assertInPlacePreconditions: () => assertInPlacePreconditions,
|
|
9358
9483
|
assertColdVaultDestination: () => assertColdVaultDestination,
|
|
9359
9484
|
applyPromotion: () => applyPromotion,
|
|
@@ -9361,6 +9486,7 @@ __export(exports_promote_support, {
|
|
|
9361
9486
|
CONFIRM_WORD: () => CONFIRM_WORD
|
|
9362
9487
|
});
|
|
9363
9488
|
function assertColdVaultDestination(index, destinationLabelOrAddress, opts = {}) {
|
|
9489
|
+
assertNotEvmEntry(index, destinationLabelOrAddress, "this destination");
|
|
9364
9490
|
const destination = findVaultRoleEntry(index, destinationLabelOrAddress);
|
|
9365
9491
|
if (destination === undefined) {
|
|
9366
9492
|
throw new VaultError("PROMOTE_DESTINATION_NOT_COLD", `No vault key matches ${destinationLabelOrAddress}.`, {
|
|
@@ -9388,11 +9514,20 @@ function assertColdVaultDestination(index, destinationLabelOrAddress, opts = {})
|
|
|
9388
9514
|
}
|
|
9389
9515
|
return destination;
|
|
9390
9516
|
}
|
|
9517
|
+
function assertNotEvmEntry(index, labelOrAddress, command) {
|
|
9518
|
+
const named = index.entries.find((entry) => entry.chain === "evm" && (entry.label === labelOrAddress || entry.address.toLowerCase() === labelOrAddress.toLowerCase()));
|
|
9519
|
+
if (named === undefined)
|
|
9520
|
+
return;
|
|
9521
|
+
throw new VaultError("SOLANA_COMMAND_EVM_KEY", `${named.label || named.address} is an EVM key (${named.address}); ${command} works on Solana keys only.`, {
|
|
9522
|
+
suggestion: `Nothing was signed or written. An EVM vault key moves funds with: candle vault transfer <0x address> --from ${named.label || named.address}`
|
|
9523
|
+
});
|
|
9524
|
+
}
|
|
9391
9525
|
function findVaultRoleEntry(index, labelOrAddress) {
|
|
9392
|
-
const
|
|
9526
|
+
const solana = index.entries.filter((entry) => entry.chain === "solana");
|
|
9527
|
+
const byLabel = solana.find((entry) => entry.role === "vault" && entry.label !== undefined && entry.label === labelOrAddress);
|
|
9393
9528
|
if (byLabel !== undefined)
|
|
9394
9529
|
return byLabel;
|
|
9395
|
-
return
|
|
9530
|
+
return solana.find((entry) => entry.address === labelOrAddress);
|
|
9396
9531
|
}
|
|
9397
9532
|
function findTransferSource(index, labelOrAddress) {
|
|
9398
9533
|
const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
|
|
@@ -9413,6 +9548,7 @@ function assertNotPinnedDestination(index, subjectAddress) {
|
|
|
9413
9548
|
});
|
|
9414
9549
|
}
|
|
9415
9550
|
function assertInPlacePreconditions(index, subjectLabel, sweepToLabel, opts) {
|
|
9551
|
+
assertNotEvmEntry(index, subjectLabel, "vault promote");
|
|
9416
9552
|
const subject = findEntryByLabelOrAddress(index, subjectLabel);
|
|
9417
9553
|
if (subject === undefined) {
|
|
9418
9554
|
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `No entry matches ${subjectLabel}.`);
|
|
@@ -13707,11 +13843,13 @@ async function listTradingWallets(ctx, key, scope) {
|
|
|
13707
13843
|
const rows = [];
|
|
13708
13844
|
const cursors = new Set;
|
|
13709
13845
|
let appId = "";
|
|
13846
|
+
let scopes = [];
|
|
13710
13847
|
for (;; ) {
|
|
13711
13848
|
const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
|
|
13712
13849
|
if (scope !== undefined && !response.scopes?.includes(scope))
|
|
13713
13850
|
throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
|
|
13714
13851
|
appId = response.privyAppId ?? "";
|
|
13852
|
+
scopes = response.scopes;
|
|
13715
13853
|
if (!Array.isArray(response.page))
|
|
13716
13854
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
13717
13855
|
rows.push(...response.page);
|
|
@@ -13722,7 +13860,7 @@ async function listTradingWallets(ctx, key, scope) {
|
|
|
13722
13860
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
|
|
13723
13861
|
cursors.add(cursor);
|
|
13724
13862
|
}
|
|
13725
|
-
return { rows, appId };
|
|
13863
|
+
return { rows, appId, scopes };
|
|
13726
13864
|
}
|
|
13727
13865
|
function matchesName(row, name) {
|
|
13728
13866
|
return row.id === name || row.address === name || row.label === name;
|
|
@@ -13759,25 +13897,24 @@ async function tradingWallet(ctx, key, name, scope) {
|
|
|
13759
13897
|
}
|
|
13760
13898
|
return await completeTradingWallet(ctx, matches[0], appId, scope);
|
|
13761
13899
|
}
|
|
13762
|
-
async function tradingPayer(ctx, key, name) {
|
|
13763
|
-
const
|
|
13764
|
-
const { rows, appId } = await listTradingWallets(ctx, key, scope);
|
|
13900
|
+
async function tradingPayer(ctx, key, name, scope = "swap:write") {
|
|
13901
|
+
const { rows, appId, scopes } = await listTradingWallets(ctx, key, scope);
|
|
13765
13902
|
const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
|
|
13766
|
-
const asEmbedded = () => ({ kind: "embedded", address: embedded });
|
|
13903
|
+
const asEmbedded = () => ({ kind: "embedded", address: embedded, scopes });
|
|
13767
13904
|
const options = [
|
|
13768
13905
|
...rows.map((row) => `TEE ${describeWallet(row)}`),
|
|
13769
13906
|
...embedded ? [`embedded (${embedded})`] : []
|
|
13770
13907
|
].join("; ");
|
|
13771
13908
|
if (name === undefined) {
|
|
13772
13909
|
if (rows.length === 1 && !embedded)
|
|
13773
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
|
|
13910
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope), scopes };
|
|
13774
13911
|
if (rows.length === 0 && embedded)
|
|
13775
13912
|
return asEmbedded();
|
|
13776
13913
|
throw new TradingError("PAYER_REQUIRED", options.length === 0 ? "This account has no wallet that can pay for a swap. Enrol a TEE wallet, or create an embedded wallet in the app." : `Name the payer with --wallet. This account can pay from: ${options}.`);
|
|
13777
13914
|
}
|
|
13778
13915
|
const matches = rows.filter((row) => matchesName(row, name));
|
|
13779
13916
|
if (matches.length === 1)
|
|
13780
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
|
|
13917
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope), scopes };
|
|
13781
13918
|
if (matches.length === 0 && embedded === name)
|
|
13782
13919
|
return asEmbedded();
|
|
13783
13920
|
throw new TradingError("TEE_WALLET_REQUIRED", matches.length > 1 ? `"${name}" matches ${matches.length} TEE wallets on this key: ${teeWalletList(matches)}. Name one by id or address.` : options.length === 0 ? `"${name}" is not a wallet this account can pay from, and it has none: enrol a TEE wallet, or create an embedded wallet in the app.` : `"${name}" is not a wallet this account can pay from. It can pay from: ${options}.`);
|
|
@@ -22179,6 +22316,8 @@ __export(exports_vault_support, {
|
|
|
22179
22316
|
menuSafeText: () => menuSafeText,
|
|
22180
22317
|
findExternalEntry: () => findExternalEntry,
|
|
22181
22318
|
factorMenu: () => factorMenu,
|
|
22319
|
+
factorAddPassphraseOnlyReason: () => factorAddPassphraseOnlyReason,
|
|
22320
|
+
factorAddAmong: () => factorAddAmong,
|
|
22182
22321
|
describeRole: () => describeRole,
|
|
22183
22322
|
describeEntry: () => describeEntry,
|
|
22184
22323
|
derivationNotice: () => derivationNotice,
|
|
@@ -22277,6 +22416,20 @@ async function requireVaultRaw(ctx, resolved) {
|
|
|
22277
22416
|
throw missingVault(ctx, resolved);
|
|
22278
22417
|
return raw;
|
|
22279
22418
|
}
|
|
22419
|
+
function factorAddAmong(envelopes) {
|
|
22420
|
+
return {
|
|
22421
|
+
envelopeIds: envelopes.filter((envelope) => isPassphraseEnvelope(envelope) || isCtap2Envelope(envelope)).map((envelope) => envelope.id),
|
|
22422
|
+
because: factorAddPassphraseOnlyReason,
|
|
22423
|
+
excludedBecause: "adding a factor opens the vault with the passphrase or a security key."
|
|
22424
|
+
};
|
|
22425
|
+
}
|
|
22426
|
+
function factorAddPassphraseOnlyReason(unusable) {
|
|
22427
|
+
if (unusable.length === 0) {
|
|
22428
|
+
return "adding a factor opens the vault with the passphrase or a security key, and this vault has no security key.";
|
|
22429
|
+
}
|
|
22430
|
+
const keys = unusable.map((entry) => `${entry.word} ${entry.id} cannot be used on this machine: ${entry.availability}`);
|
|
22431
|
+
return `adding a factor opens the vault with the passphrase or a security key, and ${keys.join("; ")}.`;
|
|
22432
|
+
}
|
|
22280
22433
|
function derivationNotice(line, purpose) {
|
|
22281
22434
|
if (purpose === undefined)
|
|
22282
22435
|
return line;
|
|
@@ -22303,6 +22456,9 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22303
22456
|
const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
|
|
22304
22457
|
const notice = noticeFor(opts.purpose);
|
|
22305
22458
|
if (choice.kind === "passphrase") {
|
|
22459
|
+
if (opts.onFactorChosen) {
|
|
22460
|
+
await opts.onFactorChosen(choice.envelopeId !== undefined ? { kind: "passphrase", envelopeId: choice.envelopeId } : { kind: "passphrase" });
|
|
22461
|
+
}
|
|
22306
22462
|
if (choice.onlyBecause !== undefined)
|
|
22307
22463
|
deps.stderr.write(`${PASSPHRASE_ONLY_PREFIX}${choice.onlyBecause}
|
|
22308
22464
|
`);
|
|
@@ -22324,6 +22480,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22324
22480
|
}
|
|
22325
22481
|
if (choice.kind === "touch-id") {
|
|
22326
22482
|
const envelope2 = choice.envelope;
|
|
22483
|
+
if (opts.onFactorChosen)
|
|
22484
|
+
await opts.onFactorChosen({ kind: "touch-id", envelope: envelope2 });
|
|
22327
22485
|
const session2 = await openEnclaveSession(deps, envelope2.helper);
|
|
22328
22486
|
const open5 = async (p, r, reason2) => {
|
|
22329
22487
|
const current = parseVaultFile(r);
|
|
@@ -22352,6 +22510,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22352
22510
|
}
|
|
22353
22511
|
if (choice.kind === "passkey") {
|
|
22354
22512
|
const envelope2 = choice.envelope;
|
|
22513
|
+
if (opts.onFactorChosen)
|
|
22514
|
+
await opts.onFactorChosen({ kind: "passkey", envelope: envelope2 });
|
|
22355
22515
|
const session2 = await openPasskeySession(deps, envelope2.helper);
|
|
22356
22516
|
const open5 = async (p, r, purpose) => {
|
|
22357
22517
|
const current = parseVaultFile(r);
|
|
@@ -22379,11 +22539,14 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22379
22539
|
};
|
|
22380
22540
|
}
|
|
22381
22541
|
const envelope = choice.envelope;
|
|
22542
|
+
const keyDevice = opts.onFactorChosen ? await opts.onFactorChosen({ kind: "security-key", envelope }) : undefined;
|
|
22382
22543
|
const session = await openSecurityKeySession(deps, {
|
|
22383
22544
|
vaultId: file.vaultId,
|
|
22384
22545
|
envelopeId: envelope.id,
|
|
22385
|
-
|
|
22386
|
-
|
|
22546
|
+
...keyDevice !== undefined ? { preferDevice: keyDevice.preferDevice } : {
|
|
22547
|
+
deviceFlag: ctx.vaultDevice,
|
|
22548
|
+
...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {}
|
|
22549
|
+
},
|
|
22387
22550
|
requireFeatures: false
|
|
22388
22551
|
});
|
|
22389
22552
|
const open4 = async (p, r) => {
|
|
@@ -22477,7 +22640,7 @@ async function chooseFactor(ctx, vaultId, allEnvelopes, facts, requested, opts =
|
|
|
22477
22640
|
}
|
|
22478
22641
|
if (passphrases.length === 0 && drivable.length === 1)
|
|
22479
22642
|
return choiceFor(drivable[0]);
|
|
22480
|
-
const attached = ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22643
|
+
const attached = "attached" in opts ? opts.attached : ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22481
22644
|
const notUsableHere = envelopes.filter((envelope) => !isPassphraseEnvelope(envelope) && !drivable.includes(envelope));
|
|
22482
22645
|
const menu = factorMenu({
|
|
22483
22646
|
keys: drivableKeys,
|
|
@@ -22836,6 +22999,12 @@ async function findTeeInVault(ctx, address, opts = {}) {
|
|
|
22836
22999
|
return { hit: false };
|
|
22837
23000
|
const opened = await unlockInteractively(ctx, path, raw, { acceptOlderCopy: true });
|
|
22838
23001
|
const vault = opened.vault;
|
|
23002
|
+
try {
|
|
23003
|
+
assertNotEvmEntry(vault.index, address, "this command");
|
|
23004
|
+
} catch (error) {
|
|
23005
|
+
closeVault(vault);
|
|
23006
|
+
throw error;
|
|
23007
|
+
}
|
|
22839
23008
|
const entry = vault.index.entries.find((candidate) => candidate.address === address && candidate.role === "tee-wallet");
|
|
22840
23009
|
if (entry === undefined) {
|
|
22841
23010
|
closeVault(vault);
|
|
@@ -22849,6 +23018,7 @@ function vaultOwnsTeeAddress(vault, address) {
|
|
|
22849
23018
|
var init_tee_lookup = __esm(() => {
|
|
22850
23019
|
init_args();
|
|
22851
23020
|
init_vault_support();
|
|
23021
|
+
init_promote_support();
|
|
22852
23022
|
init_store();
|
|
22853
23023
|
});
|
|
22854
23024
|
|
|
@@ -38715,7 +38885,7 @@ init_render();
|
|
|
38715
38885
|
init_secret_store();
|
|
38716
38886
|
|
|
38717
38887
|
// src/version.ts
|
|
38718
|
-
var CLI_VERSION = "0.11.
|
|
38888
|
+
var CLI_VERSION = "0.11.9";
|
|
38719
38889
|
|
|
38720
38890
|
// src/commands/auth.ts
|
|
38721
38891
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -38737,6 +38907,10 @@ async function authLogin(args, ctx) {
|
|
|
38737
38907
|
return 2;
|
|
38738
38908
|
}
|
|
38739
38909
|
const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
|
|
38910
|
+
if (scopes?.includes("transfer:bound")) {
|
|
38911
|
+
writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Read:Write:Transfer key with: candle keys create --access read-write-transfer", json);
|
|
38912
|
+
return 2;
|
|
38913
|
+
}
|
|
38740
38914
|
const label = parsed.values["--label"]?.trim();
|
|
38741
38915
|
const noBrowser = parsed.booleans.has("--no-browser");
|
|
38742
38916
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > MAX_CLIENT_NAME_LENGTH)) {
|
|
@@ -39106,6 +39280,10 @@ var ENVIRONMENT = [
|
|
|
39106
39280
|
description: "Unlocks the encrypted-file backend where no OS keychain exists"
|
|
39107
39281
|
},
|
|
39108
39282
|
{ name: "CANDLE_SOLANA_RPC_URL", description: "Solana RPC endpoint, when --rpc-url is not given" },
|
|
39283
|
+
{
|
|
39284
|
+
name: "CANDLE_EVM_RPC_URL",
|
|
39285
|
+
description: "EVM RPC endpoint for an EVM vault key, when --rpc-url (transfer) or --evm-rpc-url (list) is not given; without it the built-in Hood RPC is used"
|
|
39286
|
+
},
|
|
39109
39287
|
{
|
|
39110
39288
|
name: "CANDLE_FIDO2_HELPER",
|
|
39111
39289
|
description: "Path to the candle-fido2 helper, when it is not beside the binary"
|
|
@@ -39253,6 +39431,32 @@ var HELP = {
|
|
|
39253
39431
|
],
|
|
39254
39432
|
env: ENV_API
|
|
39255
39433
|
},
|
|
39434
|
+
transfer: {
|
|
39435
|
+
group: "Trade",
|
|
39436
|
+
summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
|
|
39437
|
+
description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Read:Write:Transfer key (transfer:bound); from that wallet, Candle allows only its own pinned vault or another of the account's wallets you linked while signed in or marked trusted. To a linked wallet the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm.",
|
|
39438
|
+
usage: [
|
|
39439
|
+
"candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
|
|
39440
|
+
],
|
|
39441
|
+
rows: [],
|
|
39442
|
+
flags: [
|
|
39443
|
+
{
|
|
39444
|
+
invocation: "--to <address|wallet name|vault>",
|
|
39445
|
+
description: "Where the funds go: vault is the wallet's own pin"
|
|
39446
|
+
},
|
|
39447
|
+
{ invocation: "--asset <SOL|USDC|CNDL>", description: "A base asset (required for a linked-wallet destination)" },
|
|
39448
|
+
{ invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
|
|
39449
|
+
{ invocation: "--amount <decimal|max>", description: "How much, or max for the whole spendable balance" },
|
|
39450
|
+
{ invocation: "--wallet <tee>", description: "The TEE wallet the funds leave; optional with one payer" },
|
|
39451
|
+
{ invocation: "--rpc-url <url>", description: "Your own Solana RPC, to read a --mint's decimals" },
|
|
39452
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt (the destination is still printed)" }
|
|
39453
|
+
],
|
|
39454
|
+
examples: [
|
|
39455
|
+
"candle transfer --to vault --asset SOL --amount max --wallet AgentOne",
|
|
39456
|
+
"candle transfer --to treasury --asset USDC --amount 250 --wallet AgentOne --yes --json"
|
|
39457
|
+
],
|
|
39458
|
+
env: ENV_API
|
|
39459
|
+
},
|
|
39256
39460
|
launch: {
|
|
39257
39461
|
group: "Trade",
|
|
39258
39462
|
summary: "Create a Solana token (the first buy is a separate swap)",
|
|
@@ -39296,20 +39500,26 @@ var HELP = {
|
|
|
39296
39500
|
keys: {
|
|
39297
39501
|
group: "Account",
|
|
39298
39502
|
summary: "API keys, and the wallets each key may use",
|
|
39299
|
-
description: "API keys are minted over the device token and shown exactly once. A key's wallet set and scope decide which wallets an agent holding it may act on.",
|
|
39503
|
+
description: "API keys are minted over the device token and shown exactly once. A key's wallet set and scope decide which wallets an agent holding it may act on. To give a key TEE wallets, move them to it with candle tee rebind; keys wallets set cannot widen a key's set from the CLI.",
|
|
39300
39504
|
usage: ["candle keys <subcommand> [flags]"],
|
|
39301
39505
|
rows: [
|
|
39302
39506
|
{
|
|
39303
39507
|
invocation: "list [--scopes]",
|
|
39304
|
-
description: "List API keys: name and Read or Read:Write access; --scopes adds the raw scopes"
|
|
39508
|
+
description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
|
|
39305
39509
|
},
|
|
39306
39510
|
{
|
|
39307
|
-
invocation: "create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
|
|
39308
|
-
description: "Create an API key"
|
|
39511
|
+
invocation: "create [--access read|read-write|read-write-transfer | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
|
|
39512
|
+
description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs)"
|
|
39309
39513
|
},
|
|
39310
39514
|
{ invocation: "revoke <prefix>", description: "Revoke an API key" },
|
|
39311
|
-
{
|
|
39312
|
-
|
|
39515
|
+
{
|
|
39516
|
+
invocation: "wallets <prefix>",
|
|
39517
|
+
description: "Wallets an agent profile can use. TEE wallets move between keys with candle tee rebind"
|
|
39518
|
+
},
|
|
39519
|
+
{
|
|
39520
|
+
invocation: " set <prefix> --wallets <id,id>",
|
|
39521
|
+
description: "Replace the profile's wallet set; from a key it can only narrow. To move TEE wallets to a key: candle tee rebind"
|
|
39522
|
+
},
|
|
39313
39523
|
{
|
|
39314
39524
|
invocation: " scope <prefix> --scope <all|selected>",
|
|
39315
39525
|
description: "Limit a profile to assigned wallets"
|
|
@@ -39317,8 +39527,10 @@ var HELP = {
|
|
|
39317
39527
|
],
|
|
39318
39528
|
examples: [
|
|
39319
39529
|
"candle keys list",
|
|
39530
|
+
"candle keys create --access read-write-transfer --label rebalancer",
|
|
39320
39531
|
"candle keys create --scopes trade:write --label agent-one",
|
|
39321
39532
|
"candle keys wallets ck_live_ab12",
|
|
39533
|
+
"candle tee rebind --label-prefix dest- --to-key ck_live_ab12",
|
|
39322
39534
|
"candle keys revoke ck_live_ab12"
|
|
39323
39535
|
],
|
|
39324
39536
|
env: ENV_API
|
|
@@ -39327,19 +39539,28 @@ var HELP = {
|
|
|
39327
39539
|
group: "Account",
|
|
39328
39540
|
display: "wallet",
|
|
39329
39541
|
summary: "Launch and linked wallets; import or revoke one (wallets is an alias)",
|
|
39330
|
-
description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
|
|
39542
|
+
description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key and a Trusted column saying whether it is yours: linked while signed in, or marked with wallet trust. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
|
|
39331
39543
|
usage: ["candle wallet [flags]", "candle wallet <subcommand> [flags]"],
|
|
39332
39544
|
rows: [
|
|
39333
39545
|
{
|
|
39334
39546
|
invocation: "import --chain <solana|evm> [options]",
|
|
39335
39547
|
description: "Import a wallet you own (key via --key-file or hidden prompt)"
|
|
39336
39548
|
},
|
|
39337
|
-
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" }
|
|
39549
|
+
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
|
|
39550
|
+
{
|
|
39551
|
+
invocation: "trust <label|address|id|prefix*>...",
|
|
39552
|
+
description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
|
|
39553
|
+
},
|
|
39554
|
+
{
|
|
39555
|
+
invocation: "untrust <label|address|id|prefix*>... [--yes]",
|
|
39556
|
+
description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
|
|
39557
|
+
}
|
|
39338
39558
|
],
|
|
39339
39559
|
examples: [
|
|
39340
39560
|
"candle wallet",
|
|
39341
39561
|
"candle wallet import --chain solana --key-file ./signer.json",
|
|
39342
|
-
"candle wallet revoke wal_123"
|
|
39562
|
+
"candle wallet revoke wal_123",
|
|
39563
|
+
"candle wallet trust 'tr-*' 'dest-*'"
|
|
39343
39564
|
],
|
|
39344
39565
|
env: ENV_API
|
|
39345
39566
|
},
|
|
@@ -39374,12 +39595,12 @@ var HELP = {
|
|
|
39374
39595
|
},
|
|
39375
39596
|
{ invocation: "status [--unlock]", description: "What the vault holds, and what opens it" },
|
|
39376
39597
|
{
|
|
39377
|
-
invocation: "list [<filter>] [--balances] [--rpc-url <url>]",
|
|
39378
|
-
description: "One line per key: address, label, role, derivation. Prompts for the passphrase"
|
|
39598
|
+
invocation: "list [<filter>] [--balances] [--rpc-url <url>] [--evm-rpc-url <url>]",
|
|
39599
|
+
description: "One line per key: address, label, role, derivation. Prompts for the passphrase. --balances reads SOL over your Solana RPC and ETH (plus USDG on Hood) for EVM keys over --evm-rpc-url, CANDLE_EVM_RPC_URL, or the built-in Hood RPC"
|
|
39379
39600
|
},
|
|
39380
39601
|
{
|
|
39381
|
-
invocation: "new-key --chain solana [--label <name>] [--count <n>] [--labels-from <file>]",
|
|
39382
|
-
description: "Derive the next Solana key, or n of them under one unlock; the name must be free"
|
|
39602
|
+
invocation: "new-key --chain solana|evm [--label <name>] [--count <n>] [--labels-from <file>]",
|
|
39603
|
+
description: "Derive the next Solana key (m/44'/501'/n'/0') or EVM key (m/44'/60'/n'/0/0), or n of them under one unlock; the name must be free"
|
|
39383
39604
|
},
|
|
39384
39605
|
{
|
|
39385
39606
|
invocation: "rename <label|address> <new-label> [--id <entry-id>]",
|
|
@@ -39387,8 +39608,8 @@ var HELP = {
|
|
|
39387
39608
|
},
|
|
39388
39609
|
{ invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
|
|
39389
39610
|
{
|
|
39390
|
-
invocation: "restore --phrase [--own-passphrase] [--count <n>] [--tee-count <k>] [--external-count <e>] [--rpc-url <url>]",
|
|
39391
|
-
description: "Rebuild a vault from the recovery phrase; it gets a new passphrase"
|
|
39611
|
+
invocation: "restore --phrase [--own-passphrase] [--count <n>] [--tee-count <k>] [--external-count <e>] [--evm-count <m>] [--rpc-url <url>]",
|
|
39612
|
+
description: "Rebuild a vault from the recovery phrase; it gets a new passphrase. --evm-count derives EVM indices 0..m-1 (default 0, never gap-scanned)"
|
|
39392
39613
|
},
|
|
39393
39614
|
{
|
|
39394
39615
|
invocation: "reconcile-exposure",
|
|
@@ -39413,8 +39634,8 @@ var HELP = {
|
|
|
39413
39634
|
description: "Rename the Phase 1 store after a verified backup"
|
|
39414
39635
|
},
|
|
39415
39636
|
{
|
|
39416
|
-
invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
|
|
39417
|
-
description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
|
|
39637
|
+
invocation: "transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]",
|
|
39638
|
+
description: "Sign a transfer locally from a vault key or a promoted TEE wallet. From an EVM key: ETH or an ERC-20, on Hood by default (--rpc-url for any EVM chain; the chain id is read from the RPC), exit 0 means depth-confirmed (1 block on Hood, 2 elsewhere), not finalized. A Solana key still needs --rpc-url"
|
|
39418
39639
|
},
|
|
39419
39640
|
{
|
|
39420
39641
|
invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
|
|
@@ -39443,10 +39664,17 @@ var HELP = {
|
|
|
39443
39664
|
invocation: "--factor <id|kind>",
|
|
39444
39665
|
description: "Unlock with this envelope: an id from factor list, or passphrase, security-key, touch-id, passkey; without it, a terminal lists the factors to pick by number"
|
|
39445
39666
|
},
|
|
39446
|
-
{
|
|
39667
|
+
{
|
|
39668
|
+
invocation: "--device <id>",
|
|
39669
|
+
description: "The security key to use when more than one is attached; on factor add security-key, the key to add (the vault opens with the passphrase or another key)"
|
|
39670
|
+
},
|
|
39447
39671
|
{
|
|
39448
39672
|
invocation: "--balances",
|
|
39449
|
-
description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; tokens are never read"
|
|
39673
|
+
description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; SPL tokens are never read. EVM keys are read over the EVM endpoint: ETH, and USDG on Hood"
|
|
39674
|
+
},
|
|
39675
|
+
{
|
|
39676
|
+
invocation: "--evm-rpc-url <url>",
|
|
39677
|
+
description: "list --balances: the EVM endpoint for EVM keys. Else CANDLE_EVM_RPC_URL, else the built-in Hood RPC; the Solana --rpc-url is never sent an EVM address. The host is printed on stderr before the read"
|
|
39450
39678
|
},
|
|
39451
39679
|
{
|
|
39452
39680
|
invocation: "--labels-from <file>",
|
|
@@ -39464,6 +39692,8 @@ var HELP = {
|
|
|
39464
39692
|
examples: [
|
|
39465
39693
|
"candle vault init",
|
|
39466
39694
|
"candle vault new-key --chain solana --label treasury",
|
|
39695
|
+
"candle vault new-key --chain evm --label hood-cold",
|
|
39696
|
+
"candle vault transfer 0x000000000000000000000000000000000000dEaD --amount 0.5 --asset USDG --from hood-cold",
|
|
39467
39697
|
"candle vault list cn-s",
|
|
39468
39698
|
"candle vault rename key-7 treasury-cold",
|
|
39469
39699
|
"candle vault new-key --chain solana --labels-from ./replacement-names.txt",
|
|
@@ -39474,7 +39704,14 @@ var HELP = {
|
|
|
39474
39704
|
"candle vault backup --to icloud",
|
|
39475
39705
|
"CANDLE_CONFIG_DIR=$HOME/t47 candle vault status"
|
|
39476
39706
|
],
|
|
39477
|
-
env: [
|
|
39707
|
+
env: [
|
|
39708
|
+
"CANDLE_CONFIG_DIR",
|
|
39709
|
+
"CANDLE_SOLANA_RPC_URL",
|
|
39710
|
+
"CANDLE_EVM_RPC_URL",
|
|
39711
|
+
"CANDLE_FIDO2_HELPER",
|
|
39712
|
+
"CANDLE_ENCLAVE_HELPER",
|
|
39713
|
+
"CANDLE_KEYSTORE_PASSPHRASE"
|
|
39714
|
+
]
|
|
39478
39715
|
},
|
|
39479
39716
|
tee: {
|
|
39480
39717
|
group: "Custody, on this machine",
|
|
@@ -39504,8 +39741,8 @@ var HELP = {
|
|
|
39504
39741
|
description: "Sign locally and move everything to the pinned vault; closes DAMM v2 LP positions after verifying each server-built close (--emergency moves the position NFT instead, with no API)"
|
|
39505
39742
|
},
|
|
39506
39743
|
{
|
|
39507
|
-
invocation: "rebind <wallet...> --to-key <prefix|label>",
|
|
39508
|
-
description: "Move TEE wallets to another key on this account (owner only; funds do not move)"
|
|
39744
|
+
invocation: "rebind <wallet...> --to-key <prefix|label> [--label-prefix <p>]",
|
|
39745
|
+
description: "Move TEE wallets to another key on this account, which is how a key gets TEE wallets. Name them, or --label-prefix for every wallet whose label starts with it (owner only; funds do not move)"
|
|
39509
39746
|
},
|
|
39510
39747
|
{ invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" }
|
|
39511
39748
|
],
|
|
@@ -39514,7 +39751,8 @@ var HELP = {
|
|
|
39514
39751
|
"candle tee new --label AgentOne",
|
|
39515
39752
|
"candle tee status AgentOneAddress",
|
|
39516
39753
|
"candle tee sweep AgentOneAddress --rpc-url https://api.mainnet-beta.solana.com",
|
|
39517
|
-
"candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h"
|
|
39754
|
+
"candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h",
|
|
39755
|
+
"candle tee rebind --label-prefix dest- --to-key Ab3dEf9h"
|
|
39518
39756
|
],
|
|
39519
39757
|
env: ENV_LOCAL_SIGNING
|
|
39520
39758
|
},
|
|
@@ -42460,6 +42698,1022 @@ zone
|
|
|
42460
42698
|
zoo`.split(`
|
|
42461
42699
|
`);
|
|
42462
42700
|
|
|
42701
|
+
// ../../node_modules/@noble/curves/esm/secp256k1.js
|
|
42702
|
+
init_sha2();
|
|
42703
|
+
init__shortw_utils();
|
|
42704
|
+
init_modular();
|
|
42705
|
+
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
42706
|
+
var secp256k1_CURVE = {
|
|
42707
|
+
p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),
|
|
42708
|
+
n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),
|
|
42709
|
+
h: BigInt(1),
|
|
42710
|
+
a: BigInt(0),
|
|
42711
|
+
b: BigInt(7),
|
|
42712
|
+
Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),
|
|
42713
|
+
Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")
|
|
42714
|
+
};
|
|
42715
|
+
var secp256k1_ENDO = {
|
|
42716
|
+
beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),
|
|
42717
|
+
basises: [
|
|
42718
|
+
[BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],
|
|
42719
|
+
[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")]
|
|
42720
|
+
]
|
|
42721
|
+
};
|
|
42722
|
+
var _2n5 = /* @__PURE__ */ BigInt(2);
|
|
42723
|
+
function sqrtMod(y) {
|
|
42724
|
+
const P2 = secp256k1_CURVE.p;
|
|
42725
|
+
const _3n4 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
|
|
42726
|
+
const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
|
|
42727
|
+
const b2 = y * y * y % P2;
|
|
42728
|
+
const b3 = b2 * b2 * y % P2;
|
|
42729
|
+
const b6 = pow2(b3, _3n4, P2) * b3 % P2;
|
|
42730
|
+
const b9 = pow2(b6, _3n4, P2) * b3 % P2;
|
|
42731
|
+
const b11 = pow2(b9, _2n5, P2) * b2 % P2;
|
|
42732
|
+
const b22 = pow2(b11, _11n, P2) * b11 % P2;
|
|
42733
|
+
const b44 = pow2(b22, _22n, P2) * b22 % P2;
|
|
42734
|
+
const b88 = pow2(b44, _44n, P2) * b44 % P2;
|
|
42735
|
+
const b176 = pow2(b88, _88n, P2) * b88 % P2;
|
|
42736
|
+
const b220 = pow2(b176, _44n, P2) * b44 % P2;
|
|
42737
|
+
const b223 = pow2(b220, _3n4, P2) * b3 % P2;
|
|
42738
|
+
const t1 = pow2(b223, _23n, P2) * b22 % P2;
|
|
42739
|
+
const t2 = pow2(t1, _6n, P2) * b2 % P2;
|
|
42740
|
+
const root = pow2(t2, _2n5, P2);
|
|
42741
|
+
if (!Fpk1.eql(Fpk1.sqr(root), y))
|
|
42742
|
+
throw new Error("Cannot find square root");
|
|
42743
|
+
return root;
|
|
42744
|
+
}
|
|
42745
|
+
var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod });
|
|
42746
|
+
var secp256k1 = createCurve({ ...secp256k1_CURVE, Fp: Fpk1, lowS: true, endo: secp256k1_ENDO }, sha256);
|
|
42747
|
+
|
|
42748
|
+
// ../../node_modules/@noble/hashes/esm/sha3.js
|
|
42749
|
+
init__u64();
|
|
42750
|
+
init_utils();
|
|
42751
|
+
var _0n7 = BigInt(0);
|
|
42752
|
+
var _1n7 = BigInt(1);
|
|
42753
|
+
var _2n6 = BigInt(2);
|
|
42754
|
+
var _7n2 = BigInt(7);
|
|
42755
|
+
var _256n = BigInt(256);
|
|
42756
|
+
var _0x71n = BigInt(113);
|
|
42757
|
+
var SHA3_PI = [];
|
|
42758
|
+
var SHA3_ROTL = [];
|
|
42759
|
+
var _SHA3_IOTA = [];
|
|
42760
|
+
for (let round = 0, R = _1n7, x = 1, y = 0;round < 24; round++) {
|
|
42761
|
+
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
42762
|
+
SHA3_PI.push(2 * (5 * y + x));
|
|
42763
|
+
SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
|
|
42764
|
+
let t = _0n7;
|
|
42765
|
+
for (let j = 0;j < 7; j++) {
|
|
42766
|
+
R = (R << _1n7 ^ (R >> _7n2) * _0x71n) % _256n;
|
|
42767
|
+
if (R & _2n6)
|
|
42768
|
+
t ^= _1n7 << (_1n7 << /* @__PURE__ */ BigInt(j)) - _1n7;
|
|
42769
|
+
}
|
|
42770
|
+
_SHA3_IOTA.push(t);
|
|
42771
|
+
}
|
|
42772
|
+
var IOTAS = split(_SHA3_IOTA, true);
|
|
42773
|
+
var SHA3_IOTA_H = IOTAS[0];
|
|
42774
|
+
var SHA3_IOTA_L = IOTAS[1];
|
|
42775
|
+
var rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
|
|
42776
|
+
var rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
|
|
42777
|
+
function keccakP(s, rounds = 24) {
|
|
42778
|
+
const B = new Uint32Array(5 * 2);
|
|
42779
|
+
for (let round = 24 - rounds;round < 24; round++) {
|
|
42780
|
+
for (let x = 0;x < 10; x++)
|
|
42781
|
+
B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
|
|
42782
|
+
for (let x = 0;x < 10; x += 2) {
|
|
42783
|
+
const idx1 = (x + 8) % 10;
|
|
42784
|
+
const idx0 = (x + 2) % 10;
|
|
42785
|
+
const B0 = B[idx0];
|
|
42786
|
+
const B1 = B[idx0 + 1];
|
|
42787
|
+
const Th = rotlH(B0, B1, 1) ^ B[idx1];
|
|
42788
|
+
const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
|
|
42789
|
+
for (let y = 0;y < 50; y += 10) {
|
|
42790
|
+
s[x + y] ^= Th;
|
|
42791
|
+
s[x + y + 1] ^= Tl;
|
|
42792
|
+
}
|
|
42793
|
+
}
|
|
42794
|
+
let curH = s[2];
|
|
42795
|
+
let curL = s[3];
|
|
42796
|
+
for (let t = 0;t < 24; t++) {
|
|
42797
|
+
const shift = SHA3_ROTL[t];
|
|
42798
|
+
const Th = rotlH(curH, curL, shift);
|
|
42799
|
+
const Tl = rotlL(curH, curL, shift);
|
|
42800
|
+
const PI = SHA3_PI[t];
|
|
42801
|
+
curH = s[PI];
|
|
42802
|
+
curL = s[PI + 1];
|
|
42803
|
+
s[PI] = Th;
|
|
42804
|
+
s[PI + 1] = Tl;
|
|
42805
|
+
}
|
|
42806
|
+
for (let y = 0;y < 50; y += 10) {
|
|
42807
|
+
for (let x = 0;x < 10; x++)
|
|
42808
|
+
B[x] = s[y + x];
|
|
42809
|
+
for (let x = 0;x < 10; x++)
|
|
42810
|
+
s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
|
|
42811
|
+
}
|
|
42812
|
+
s[0] ^= SHA3_IOTA_H[round];
|
|
42813
|
+
s[1] ^= SHA3_IOTA_L[round];
|
|
42814
|
+
}
|
|
42815
|
+
clean(B);
|
|
42816
|
+
}
|
|
42817
|
+
|
|
42818
|
+
class Keccak extends Hash {
|
|
42819
|
+
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
|
|
42820
|
+
super();
|
|
42821
|
+
this.pos = 0;
|
|
42822
|
+
this.posOut = 0;
|
|
42823
|
+
this.finished = false;
|
|
42824
|
+
this.destroyed = false;
|
|
42825
|
+
this.enableXOF = false;
|
|
42826
|
+
this.blockLen = blockLen;
|
|
42827
|
+
this.suffix = suffix;
|
|
42828
|
+
this.outputLen = outputLen;
|
|
42829
|
+
this.enableXOF = enableXOF;
|
|
42830
|
+
this.rounds = rounds;
|
|
42831
|
+
anumber(outputLen);
|
|
42832
|
+
if (!(0 < blockLen && blockLen < 200))
|
|
42833
|
+
throw new Error("only keccak-f1600 function is supported");
|
|
42834
|
+
this.state = new Uint8Array(200);
|
|
42835
|
+
this.state32 = u32(this.state);
|
|
42836
|
+
}
|
|
42837
|
+
clone() {
|
|
42838
|
+
return this._cloneInto();
|
|
42839
|
+
}
|
|
42840
|
+
keccak() {
|
|
42841
|
+
swap32IfBE(this.state32);
|
|
42842
|
+
keccakP(this.state32, this.rounds);
|
|
42843
|
+
swap32IfBE(this.state32);
|
|
42844
|
+
this.posOut = 0;
|
|
42845
|
+
this.pos = 0;
|
|
42846
|
+
}
|
|
42847
|
+
update(data) {
|
|
42848
|
+
aexists(this);
|
|
42849
|
+
data = toBytes(data);
|
|
42850
|
+
abytes(data);
|
|
42851
|
+
const { blockLen, state } = this;
|
|
42852
|
+
const len = data.length;
|
|
42853
|
+
for (let pos = 0;pos < len; ) {
|
|
42854
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
42855
|
+
for (let i = 0;i < take; i++)
|
|
42856
|
+
state[this.pos++] ^= data[pos++];
|
|
42857
|
+
if (this.pos === blockLen)
|
|
42858
|
+
this.keccak();
|
|
42859
|
+
}
|
|
42860
|
+
return this;
|
|
42861
|
+
}
|
|
42862
|
+
finish() {
|
|
42863
|
+
if (this.finished)
|
|
42864
|
+
return;
|
|
42865
|
+
this.finished = true;
|
|
42866
|
+
const { state, suffix, pos, blockLen } = this;
|
|
42867
|
+
state[pos] ^= suffix;
|
|
42868
|
+
if ((suffix & 128) !== 0 && pos === blockLen - 1)
|
|
42869
|
+
this.keccak();
|
|
42870
|
+
state[blockLen - 1] ^= 128;
|
|
42871
|
+
this.keccak();
|
|
42872
|
+
}
|
|
42873
|
+
writeInto(out) {
|
|
42874
|
+
aexists(this, false);
|
|
42875
|
+
abytes(out);
|
|
42876
|
+
this.finish();
|
|
42877
|
+
const bufferOut = this.state;
|
|
42878
|
+
const { blockLen } = this;
|
|
42879
|
+
for (let pos = 0, len = out.length;pos < len; ) {
|
|
42880
|
+
if (this.posOut >= blockLen)
|
|
42881
|
+
this.keccak();
|
|
42882
|
+
const take = Math.min(blockLen - this.posOut, len - pos);
|
|
42883
|
+
out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
|
|
42884
|
+
this.posOut += take;
|
|
42885
|
+
pos += take;
|
|
42886
|
+
}
|
|
42887
|
+
return out;
|
|
42888
|
+
}
|
|
42889
|
+
xofInto(out) {
|
|
42890
|
+
if (!this.enableXOF)
|
|
42891
|
+
throw new Error("XOF is not possible for this instance");
|
|
42892
|
+
return this.writeInto(out);
|
|
42893
|
+
}
|
|
42894
|
+
xof(bytes) {
|
|
42895
|
+
anumber(bytes);
|
|
42896
|
+
return this.xofInto(new Uint8Array(bytes));
|
|
42897
|
+
}
|
|
42898
|
+
digestInto(out) {
|
|
42899
|
+
aoutput(out, this);
|
|
42900
|
+
if (this.finished)
|
|
42901
|
+
throw new Error("digest() was already called");
|
|
42902
|
+
this.writeInto(out);
|
|
42903
|
+
this.destroy();
|
|
42904
|
+
return out;
|
|
42905
|
+
}
|
|
42906
|
+
digest() {
|
|
42907
|
+
return this.digestInto(new Uint8Array(this.outputLen));
|
|
42908
|
+
}
|
|
42909
|
+
destroy() {
|
|
42910
|
+
this.destroyed = true;
|
|
42911
|
+
clean(this.state);
|
|
42912
|
+
}
|
|
42913
|
+
_cloneInto(to) {
|
|
42914
|
+
const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
|
|
42915
|
+
to || (to = new Keccak(blockLen, suffix, outputLen, enableXOF, rounds));
|
|
42916
|
+
to.state32.set(this.state32);
|
|
42917
|
+
to.pos = this.pos;
|
|
42918
|
+
to.posOut = this.posOut;
|
|
42919
|
+
to.finished = this.finished;
|
|
42920
|
+
to.rounds = rounds;
|
|
42921
|
+
to.suffix = suffix;
|
|
42922
|
+
to.outputLen = outputLen;
|
|
42923
|
+
to.enableXOF = enableXOF;
|
|
42924
|
+
to.destroyed = this.destroyed;
|
|
42925
|
+
return to;
|
|
42926
|
+
}
|
|
42927
|
+
}
|
|
42928
|
+
var gen = (suffix, blockLen, outputLen) => createHasher(() => new Keccak(blockLen, suffix, outputLen));
|
|
42929
|
+
var keccak_256 = /* @__PURE__ */ (() => gen(1, 136, 256 / 8))();
|
|
42930
|
+
|
|
42931
|
+
// ../../node_modules/@scure/bip32/lib/esm/index.js
|
|
42932
|
+
init_modular();
|
|
42933
|
+
init_hmac();
|
|
42934
|
+
|
|
42935
|
+
// ../../node_modules/@noble/hashes/esm/legacy.js
|
|
42936
|
+
init__md();
|
|
42937
|
+
init_utils();
|
|
42938
|
+
var Rho160 = /* @__PURE__ */ Uint8Array.from([
|
|
42939
|
+
7,
|
|
42940
|
+
4,
|
|
42941
|
+
13,
|
|
42942
|
+
1,
|
|
42943
|
+
10,
|
|
42944
|
+
6,
|
|
42945
|
+
15,
|
|
42946
|
+
3,
|
|
42947
|
+
12,
|
|
42948
|
+
0,
|
|
42949
|
+
9,
|
|
42950
|
+
5,
|
|
42951
|
+
2,
|
|
42952
|
+
14,
|
|
42953
|
+
11,
|
|
42954
|
+
8
|
|
42955
|
+
]);
|
|
42956
|
+
var Id160 = /* @__PURE__ */ (() => Uint8Array.from(new Array(16).fill(0).map((_, i) => i)))();
|
|
42957
|
+
var Pi160 = /* @__PURE__ */ (() => Id160.map((i) => (9 * i + 5) % 16))();
|
|
42958
|
+
var idxLR = /* @__PURE__ */ (() => {
|
|
42959
|
+
const L = [Id160];
|
|
42960
|
+
const R = [Pi160];
|
|
42961
|
+
const res = [L, R];
|
|
42962
|
+
for (let i = 0;i < 4; i++)
|
|
42963
|
+
for (let j of res)
|
|
42964
|
+
j.push(j[i].map((k) => Rho160[k]));
|
|
42965
|
+
return res;
|
|
42966
|
+
})();
|
|
42967
|
+
var idxL = /* @__PURE__ */ (() => idxLR[0])();
|
|
42968
|
+
var idxR = /* @__PURE__ */ (() => idxLR[1])();
|
|
42969
|
+
var shifts160 = /* @__PURE__ */ [
|
|
42970
|
+
[11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8],
|
|
42971
|
+
[12, 13, 11, 15, 6, 9, 9, 7, 12, 15, 11, 13, 7, 8, 7, 7],
|
|
42972
|
+
[13, 15, 14, 11, 7, 7, 6, 8, 13, 14, 13, 12, 5, 5, 6, 9],
|
|
42973
|
+
[14, 11, 12, 14, 8, 6, 5, 5, 15, 12, 15, 14, 9, 9, 8, 6],
|
|
42974
|
+
[15, 12, 13, 13, 9, 5, 8, 6, 14, 11, 12, 11, 8, 6, 5, 5]
|
|
42975
|
+
].map((i) => Uint8Array.from(i));
|
|
42976
|
+
var shiftsL160 = /* @__PURE__ */ idxL.map((idx, i) => idx.map((j) => shifts160[i][j]));
|
|
42977
|
+
var shiftsR160 = /* @__PURE__ */ idxR.map((idx, i) => idx.map((j) => shifts160[i][j]));
|
|
42978
|
+
var Kl160 = /* @__PURE__ */ Uint32Array.from([
|
|
42979
|
+
0,
|
|
42980
|
+
1518500249,
|
|
42981
|
+
1859775393,
|
|
42982
|
+
2400959708,
|
|
42983
|
+
2840853838
|
|
42984
|
+
]);
|
|
42985
|
+
var Kr160 = /* @__PURE__ */ Uint32Array.from([
|
|
42986
|
+
1352829926,
|
|
42987
|
+
1548603684,
|
|
42988
|
+
1836072691,
|
|
42989
|
+
2053994217,
|
|
42990
|
+
0
|
|
42991
|
+
]);
|
|
42992
|
+
function ripemd_f(group, x, y, z) {
|
|
42993
|
+
if (group === 0)
|
|
42994
|
+
return x ^ y ^ z;
|
|
42995
|
+
if (group === 1)
|
|
42996
|
+
return x & y | ~x & z;
|
|
42997
|
+
if (group === 2)
|
|
42998
|
+
return (x | ~y) ^ z;
|
|
42999
|
+
if (group === 3)
|
|
43000
|
+
return x & z | y & ~z;
|
|
43001
|
+
return x ^ (y | ~z);
|
|
43002
|
+
}
|
|
43003
|
+
var BUF_160 = /* @__PURE__ */ new Uint32Array(16);
|
|
43004
|
+
|
|
43005
|
+
class RIPEMD160 extends HashMD {
|
|
43006
|
+
constructor() {
|
|
43007
|
+
super(64, 20, 8, true);
|
|
43008
|
+
this.h0 = 1732584193 | 0;
|
|
43009
|
+
this.h1 = 4023233417 | 0;
|
|
43010
|
+
this.h2 = 2562383102 | 0;
|
|
43011
|
+
this.h3 = 271733878 | 0;
|
|
43012
|
+
this.h4 = 3285377520 | 0;
|
|
43013
|
+
}
|
|
43014
|
+
get() {
|
|
43015
|
+
const { h0, h1, h2, h3, h4 } = this;
|
|
43016
|
+
return [h0, h1, h2, h3, h4];
|
|
43017
|
+
}
|
|
43018
|
+
set(h0, h1, h2, h3, h4) {
|
|
43019
|
+
this.h0 = h0 | 0;
|
|
43020
|
+
this.h1 = h1 | 0;
|
|
43021
|
+
this.h2 = h2 | 0;
|
|
43022
|
+
this.h3 = h3 | 0;
|
|
43023
|
+
this.h4 = h4 | 0;
|
|
43024
|
+
}
|
|
43025
|
+
process(view, offset) {
|
|
43026
|
+
for (let i = 0;i < 16; i++, offset += 4)
|
|
43027
|
+
BUF_160[i] = view.getUint32(offset, true);
|
|
43028
|
+
let al = this.h0 | 0, ar = al, bl = this.h1 | 0, br = bl, cl = this.h2 | 0, cr = cl, dl = this.h3 | 0, dr = dl, el = this.h4 | 0, er = el;
|
|
43029
|
+
for (let group = 0;group < 5; group++) {
|
|
43030
|
+
const rGroup = 4 - group;
|
|
43031
|
+
const hbl = Kl160[group], hbr = Kr160[group];
|
|
43032
|
+
const rl = idxL[group], rr = idxR[group];
|
|
43033
|
+
const sl = shiftsL160[group], sr = shiftsR160[group];
|
|
43034
|
+
for (let i = 0;i < 16; i++) {
|
|
43035
|
+
const tl = rotl(al + ripemd_f(group, bl, cl, dl) + BUF_160[rl[i]] + hbl, sl[i]) + el | 0;
|
|
43036
|
+
al = el, el = dl, dl = rotl(cl, 10) | 0, cl = bl, bl = tl;
|
|
43037
|
+
}
|
|
43038
|
+
for (let i = 0;i < 16; i++) {
|
|
43039
|
+
const tr = rotl(ar + ripemd_f(rGroup, br, cr, dr) + BUF_160[rr[i]] + hbr, sr[i]) + er | 0;
|
|
43040
|
+
ar = er, er = dr, dr = rotl(cr, 10) | 0, cr = br, br = tr;
|
|
43041
|
+
}
|
|
43042
|
+
}
|
|
43043
|
+
this.set(this.h1 + cl + dr | 0, this.h2 + dl + er | 0, this.h3 + el + ar | 0, this.h4 + al + br | 0, this.h0 + bl + cr | 0);
|
|
43044
|
+
}
|
|
43045
|
+
roundClean() {
|
|
43046
|
+
clean(BUF_160);
|
|
43047
|
+
}
|
|
43048
|
+
destroy() {
|
|
43049
|
+
this.destroyed = true;
|
|
43050
|
+
clean(this.buffer);
|
|
43051
|
+
this.set(0, 0, 0, 0, 0);
|
|
43052
|
+
}
|
|
43053
|
+
}
|
|
43054
|
+
var ripemd160 = /* @__PURE__ */ createHasher(() => new RIPEMD160);
|
|
43055
|
+
|
|
43056
|
+
// ../../node_modules/@scure/bip32/lib/esm/index.js
|
|
43057
|
+
init_sha2();
|
|
43058
|
+
init_utils();
|
|
43059
|
+
init_esm();
|
|
43060
|
+
/*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */
|
|
43061
|
+
var Point = secp256k1.ProjectivePoint;
|
|
43062
|
+
var base58check = createBase58check(sha256);
|
|
43063
|
+
function bytesToNumber(bytes) {
|
|
43064
|
+
abytes(bytes);
|
|
43065
|
+
const h = bytes.length === 0 ? "0" : bytesToHex(bytes);
|
|
43066
|
+
return BigInt("0x" + h);
|
|
43067
|
+
}
|
|
43068
|
+
function numberToBytes(num) {
|
|
43069
|
+
if (typeof num !== "bigint")
|
|
43070
|
+
throw new Error("bigint expected");
|
|
43071
|
+
return hexToBytes(num.toString(16).padStart(64, "0"));
|
|
43072
|
+
}
|
|
43073
|
+
var MASTER_SECRET = utf8ToBytes("Bitcoin seed");
|
|
43074
|
+
var BITCOIN_VERSIONS = { private: 76066276, public: 76067358 };
|
|
43075
|
+
var HARDENED_OFFSET = 2147483648;
|
|
43076
|
+
var hash160 = (data) => ripemd160(sha256(data));
|
|
43077
|
+
var fromU32 = (data) => createView(data).getUint32(0, false);
|
|
43078
|
+
var toU32 = (n) => {
|
|
43079
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
|
|
43080
|
+
throw new Error("invalid number, should be from 0 to 2**32-1, got " + n);
|
|
43081
|
+
}
|
|
43082
|
+
const buf = new Uint8Array(4);
|
|
43083
|
+
createView(buf).setUint32(0, n, false);
|
|
43084
|
+
return buf;
|
|
43085
|
+
};
|
|
43086
|
+
|
|
43087
|
+
class HDKey {
|
|
43088
|
+
get fingerprint() {
|
|
43089
|
+
if (!this.pubHash) {
|
|
43090
|
+
throw new Error("No publicKey set!");
|
|
43091
|
+
}
|
|
43092
|
+
return fromU32(this.pubHash);
|
|
43093
|
+
}
|
|
43094
|
+
get identifier() {
|
|
43095
|
+
return this.pubHash;
|
|
43096
|
+
}
|
|
43097
|
+
get pubKeyHash() {
|
|
43098
|
+
return this.pubHash;
|
|
43099
|
+
}
|
|
43100
|
+
get privateKey() {
|
|
43101
|
+
return this.privKeyBytes || null;
|
|
43102
|
+
}
|
|
43103
|
+
get publicKey() {
|
|
43104
|
+
return this.pubKey || null;
|
|
43105
|
+
}
|
|
43106
|
+
get privateExtendedKey() {
|
|
43107
|
+
const priv = this.privateKey;
|
|
43108
|
+
if (!priv) {
|
|
43109
|
+
throw new Error("No private key");
|
|
43110
|
+
}
|
|
43111
|
+
return base58check.encode(this.serialize(this.versions.private, concatBytes(new Uint8Array([0]), priv)));
|
|
43112
|
+
}
|
|
43113
|
+
get publicExtendedKey() {
|
|
43114
|
+
if (!this.pubKey) {
|
|
43115
|
+
throw new Error("No public key");
|
|
43116
|
+
}
|
|
43117
|
+
return base58check.encode(this.serialize(this.versions.public, this.pubKey));
|
|
43118
|
+
}
|
|
43119
|
+
static fromMasterSeed(seed, versions = BITCOIN_VERSIONS) {
|
|
43120
|
+
abytes(seed);
|
|
43121
|
+
if (8 * seed.length < 128 || 8 * seed.length > 512) {
|
|
43122
|
+
throw new Error("HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got " + seed.length);
|
|
43123
|
+
}
|
|
43124
|
+
const I = hmac(sha512, MASTER_SECRET, seed);
|
|
43125
|
+
return new HDKey({
|
|
43126
|
+
versions,
|
|
43127
|
+
chainCode: I.slice(32),
|
|
43128
|
+
privateKey: I.slice(0, 32)
|
|
43129
|
+
});
|
|
43130
|
+
}
|
|
43131
|
+
static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS) {
|
|
43132
|
+
const keyBuffer = base58check.decode(base58key);
|
|
43133
|
+
const keyView = createView(keyBuffer);
|
|
43134
|
+
const version = keyView.getUint32(0, false);
|
|
43135
|
+
const opt = {
|
|
43136
|
+
versions,
|
|
43137
|
+
depth: keyBuffer[4],
|
|
43138
|
+
parentFingerprint: keyView.getUint32(5, false),
|
|
43139
|
+
index: keyView.getUint32(9, false),
|
|
43140
|
+
chainCode: keyBuffer.slice(13, 45)
|
|
43141
|
+
};
|
|
43142
|
+
const key = keyBuffer.slice(45);
|
|
43143
|
+
const isPriv = key[0] === 0;
|
|
43144
|
+
if (version !== versions[isPriv ? "private" : "public"]) {
|
|
43145
|
+
throw new Error("Version mismatch");
|
|
43146
|
+
}
|
|
43147
|
+
if (isPriv) {
|
|
43148
|
+
return new HDKey({ ...opt, privateKey: key.slice(1) });
|
|
43149
|
+
} else {
|
|
43150
|
+
return new HDKey({ ...opt, publicKey: key });
|
|
43151
|
+
}
|
|
43152
|
+
}
|
|
43153
|
+
static fromJSON(json) {
|
|
43154
|
+
return HDKey.fromExtendedKey(json.xpriv);
|
|
43155
|
+
}
|
|
43156
|
+
constructor(opt) {
|
|
43157
|
+
this.depth = 0;
|
|
43158
|
+
this.index = 0;
|
|
43159
|
+
this.chainCode = null;
|
|
43160
|
+
this.parentFingerprint = 0;
|
|
43161
|
+
if (!opt || typeof opt !== "object") {
|
|
43162
|
+
throw new Error("HDKey.constructor must not be called directly");
|
|
43163
|
+
}
|
|
43164
|
+
this.versions = opt.versions || BITCOIN_VERSIONS;
|
|
43165
|
+
this.depth = opt.depth || 0;
|
|
43166
|
+
this.chainCode = opt.chainCode || null;
|
|
43167
|
+
this.index = opt.index || 0;
|
|
43168
|
+
this.parentFingerprint = opt.parentFingerprint || 0;
|
|
43169
|
+
if (!this.depth) {
|
|
43170
|
+
if (this.parentFingerprint || this.index) {
|
|
43171
|
+
throw new Error("HDKey: zero depth with non-zero index/parent fingerprint");
|
|
43172
|
+
}
|
|
43173
|
+
}
|
|
43174
|
+
if (opt.publicKey && opt.privateKey) {
|
|
43175
|
+
throw new Error("HDKey: publicKey and privateKey at same time.");
|
|
43176
|
+
}
|
|
43177
|
+
if (opt.privateKey) {
|
|
43178
|
+
if (!secp256k1.utils.isValidPrivateKey(opt.privateKey)) {
|
|
43179
|
+
throw new Error("Invalid private key");
|
|
43180
|
+
}
|
|
43181
|
+
this.privKey = typeof opt.privateKey === "bigint" ? opt.privateKey : bytesToNumber(opt.privateKey);
|
|
43182
|
+
this.privKeyBytes = numberToBytes(this.privKey);
|
|
43183
|
+
this.pubKey = secp256k1.getPublicKey(opt.privateKey, true);
|
|
43184
|
+
} else if (opt.publicKey) {
|
|
43185
|
+
this.pubKey = Point.fromHex(opt.publicKey).toRawBytes(true);
|
|
43186
|
+
} else {
|
|
43187
|
+
throw new Error("HDKey: no public or private key provided");
|
|
43188
|
+
}
|
|
43189
|
+
this.pubHash = hash160(this.pubKey);
|
|
43190
|
+
}
|
|
43191
|
+
derive(path) {
|
|
43192
|
+
if (!/^[mM]'?/.test(path)) {
|
|
43193
|
+
throw new Error('Path must start with "m" or "M"');
|
|
43194
|
+
}
|
|
43195
|
+
if (/^[mM]'?$/.test(path)) {
|
|
43196
|
+
return this;
|
|
43197
|
+
}
|
|
43198
|
+
const parts = path.replace(/^[mM]'?\//, "").split("/");
|
|
43199
|
+
let child = this;
|
|
43200
|
+
for (const c of parts) {
|
|
43201
|
+
const m = /^(\d+)('?)$/.exec(c);
|
|
43202
|
+
const m1 = m && m[1];
|
|
43203
|
+
if (!m || m.length !== 3 || typeof m1 !== "string")
|
|
43204
|
+
throw new Error("invalid child index: " + c);
|
|
43205
|
+
let idx = +m1;
|
|
43206
|
+
if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) {
|
|
43207
|
+
throw new Error("Invalid index");
|
|
43208
|
+
}
|
|
43209
|
+
if (m[2] === "'") {
|
|
43210
|
+
idx += HARDENED_OFFSET;
|
|
43211
|
+
}
|
|
43212
|
+
child = child.deriveChild(idx);
|
|
43213
|
+
}
|
|
43214
|
+
return child;
|
|
43215
|
+
}
|
|
43216
|
+
deriveChild(index) {
|
|
43217
|
+
if (!this.pubKey || !this.chainCode) {
|
|
43218
|
+
throw new Error("No publicKey or chainCode set");
|
|
43219
|
+
}
|
|
43220
|
+
let data = toU32(index);
|
|
43221
|
+
if (index >= HARDENED_OFFSET) {
|
|
43222
|
+
const priv = this.privateKey;
|
|
43223
|
+
if (!priv) {
|
|
43224
|
+
throw new Error("Could not derive hardened child key");
|
|
43225
|
+
}
|
|
43226
|
+
data = concatBytes(new Uint8Array([0]), priv, data);
|
|
43227
|
+
} else {
|
|
43228
|
+
data = concatBytes(this.pubKey, data);
|
|
43229
|
+
}
|
|
43230
|
+
const I = hmac(sha512, this.chainCode, data);
|
|
43231
|
+
const childTweak = bytesToNumber(I.slice(0, 32));
|
|
43232
|
+
const chainCode = I.slice(32);
|
|
43233
|
+
if (!secp256k1.utils.isValidPrivateKey(childTweak)) {
|
|
43234
|
+
throw new Error("Tweak bigger than curve order");
|
|
43235
|
+
}
|
|
43236
|
+
const opt = {
|
|
43237
|
+
versions: this.versions,
|
|
43238
|
+
chainCode,
|
|
43239
|
+
depth: this.depth + 1,
|
|
43240
|
+
parentFingerprint: this.fingerprint,
|
|
43241
|
+
index
|
|
43242
|
+
};
|
|
43243
|
+
try {
|
|
43244
|
+
if (this.privateKey) {
|
|
43245
|
+
const added = mod(this.privKey + childTweak, secp256k1.CURVE.n);
|
|
43246
|
+
if (!secp256k1.utils.isValidPrivateKey(added)) {
|
|
43247
|
+
throw new Error("The tweak was out of range or the resulted private key is invalid");
|
|
43248
|
+
}
|
|
43249
|
+
opt.privateKey = added;
|
|
43250
|
+
} else {
|
|
43251
|
+
const added = Point.fromHex(this.pubKey).add(Point.fromPrivateKey(childTweak));
|
|
43252
|
+
if (added.equals(Point.ZERO)) {
|
|
43253
|
+
throw new Error("The tweak was equal to negative P, which made the result key invalid");
|
|
43254
|
+
}
|
|
43255
|
+
opt.publicKey = added.toRawBytes(true);
|
|
43256
|
+
}
|
|
43257
|
+
return new HDKey(opt);
|
|
43258
|
+
} catch (err) {
|
|
43259
|
+
return this.deriveChild(index + 1);
|
|
43260
|
+
}
|
|
43261
|
+
}
|
|
43262
|
+
sign(hash) {
|
|
43263
|
+
if (!this.privateKey) {
|
|
43264
|
+
throw new Error("No privateKey set!");
|
|
43265
|
+
}
|
|
43266
|
+
abytes(hash, 32);
|
|
43267
|
+
return secp256k1.sign(hash, this.privKey).toCompactRawBytes();
|
|
43268
|
+
}
|
|
43269
|
+
verify(hash, signature) {
|
|
43270
|
+
abytes(hash, 32);
|
|
43271
|
+
abytes(signature, 64);
|
|
43272
|
+
if (!this.publicKey) {
|
|
43273
|
+
throw new Error("No publicKey set!");
|
|
43274
|
+
}
|
|
43275
|
+
let sig;
|
|
43276
|
+
try {
|
|
43277
|
+
sig = secp256k1.Signature.fromCompact(signature);
|
|
43278
|
+
} catch (error) {
|
|
43279
|
+
return false;
|
|
43280
|
+
}
|
|
43281
|
+
return secp256k1.verify(sig, hash, this.publicKey);
|
|
43282
|
+
}
|
|
43283
|
+
wipePrivateData() {
|
|
43284
|
+
this.privKey = undefined;
|
|
43285
|
+
if (this.privKeyBytes) {
|
|
43286
|
+
this.privKeyBytes.fill(0);
|
|
43287
|
+
this.privKeyBytes = undefined;
|
|
43288
|
+
}
|
|
43289
|
+
return this;
|
|
43290
|
+
}
|
|
43291
|
+
toJSON() {
|
|
43292
|
+
return {
|
|
43293
|
+
xpriv: this.privateExtendedKey,
|
|
43294
|
+
xpub: this.publicExtendedKey
|
|
43295
|
+
};
|
|
43296
|
+
}
|
|
43297
|
+
serialize(version, key) {
|
|
43298
|
+
if (!this.chainCode) {
|
|
43299
|
+
throw new Error("No chainCode set");
|
|
43300
|
+
}
|
|
43301
|
+
abytes(key, 33);
|
|
43302
|
+
return concatBytes(toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key);
|
|
43303
|
+
}
|
|
43304
|
+
}
|
|
43305
|
+
|
|
43306
|
+
// src/evm-lite.ts
|
|
43307
|
+
var HOOD_CHAIN_ID = 4663;
|
|
43308
|
+
var DEFAULT_HOOD_RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
|
|
43309
|
+
var EVM_RPC_URL_ENV = "CANDLE_EVM_RPC_URL";
|
|
43310
|
+
var HOOD_USDG_ADDRESS = "0x5fc5360d0400a0fd4f2af552add042d716f1d168";
|
|
43311
|
+
var HOOD_USDG_DECIMALS = 6;
|
|
43312
|
+
var NATIVE_DECIMALS = 18;
|
|
43313
|
+
var EVM_DERIVATION_SCHEME = "bip32-secp256k1";
|
|
43314
|
+
var EVM_SECRET_BYTES = 32;
|
|
43315
|
+
function bytesToHex2(bytes) {
|
|
43316
|
+
let out = "";
|
|
43317
|
+
for (const b of bytes)
|
|
43318
|
+
out += b.toString(16).padStart(2, "0");
|
|
43319
|
+
return `0x${out}`;
|
|
43320
|
+
}
|
|
43321
|
+
function hexToBytes2(hex2) {
|
|
43322
|
+
const body = hex2.startsWith("0x") ? hex2.slice(2) : hex2;
|
|
43323
|
+
if (body.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(body))
|
|
43324
|
+
throw new Error(`not a hex string: ${hex2}`);
|
|
43325
|
+
const out = new Uint8Array(body.length / 2);
|
|
43326
|
+
for (let i = 0;i < out.length; i++)
|
|
43327
|
+
out[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
|
|
43328
|
+
return out;
|
|
43329
|
+
}
|
|
43330
|
+
function hexToBigInt(hex2) {
|
|
43331
|
+
const body = hex2.startsWith("0x") ? hex2.slice(2) : hex2;
|
|
43332
|
+
if (body === "")
|
|
43333
|
+
return 0n;
|
|
43334
|
+
if (!/^[0-9a-fA-F]+$/.test(body))
|
|
43335
|
+
throw new Error(`not a hex quantity: ${hex2}`);
|
|
43336
|
+
return BigInt(`0x${body}`);
|
|
43337
|
+
}
|
|
43338
|
+
function quantity(value) {
|
|
43339
|
+
return `0x${BigInt(value).toString(16)}`;
|
|
43340
|
+
}
|
|
43341
|
+
function uintToMinimalBytes(value) {
|
|
43342
|
+
if (value < 0n)
|
|
43343
|
+
throw new Error("RLP integers are non-negative");
|
|
43344
|
+
if (value === 0n)
|
|
43345
|
+
return new Uint8Array(0);
|
|
43346
|
+
let hex2 = value.toString(16);
|
|
43347
|
+
if (hex2.length % 2 !== 0)
|
|
43348
|
+
hex2 = `0${hex2}`;
|
|
43349
|
+
return hexToBytes2(hex2);
|
|
43350
|
+
}
|
|
43351
|
+
function rlpLength(length, offset) {
|
|
43352
|
+
if (length < 56)
|
|
43353
|
+
return Uint8Array.of(offset + length);
|
|
43354
|
+
const lengthBytes = uintToMinimalBytes(BigInt(length));
|
|
43355
|
+
return concat2(Uint8Array.of(offset + 55 + lengthBytes.length), lengthBytes);
|
|
43356
|
+
}
|
|
43357
|
+
function concat2(...parts) {
|
|
43358
|
+
const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
|
|
43359
|
+
let at = 0;
|
|
43360
|
+
for (const part of parts) {
|
|
43361
|
+
out.set(part, at);
|
|
43362
|
+
at += part.length;
|
|
43363
|
+
}
|
|
43364
|
+
return out;
|
|
43365
|
+
}
|
|
43366
|
+
function rlpEncode(item) {
|
|
43367
|
+
if (item instanceof Uint8Array) {
|
|
43368
|
+
if (item.length === 1 && item[0] < 128)
|
|
43369
|
+
return item;
|
|
43370
|
+
return concat2(rlpLength(item.length, 128), item);
|
|
43371
|
+
}
|
|
43372
|
+
const body = concat2(...item.map(rlpEncode));
|
|
43373
|
+
return concat2(rlpLength(body.length, 192), body);
|
|
43374
|
+
}
|
|
43375
|
+
function toChecksumAddress(address) {
|
|
43376
|
+
const lower = (address.startsWith("0x") ? address.slice(2) : address).toLowerCase();
|
|
43377
|
+
if (!/^[0-9a-f]{40}$/.test(lower))
|
|
43378
|
+
throw new Error(`not an EVM address: ${address}`);
|
|
43379
|
+
const digest = bytesToHex2(keccak_256(new TextEncoder().encode(lower))).slice(2);
|
|
43380
|
+
let out = "0x";
|
|
43381
|
+
for (let i = 0;i < lower.length; i++) {
|
|
43382
|
+
const c = lower[i];
|
|
43383
|
+
out += Number.parseInt(digest[i], 16) >= 8 ? c.toUpperCase() : c;
|
|
43384
|
+
}
|
|
43385
|
+
return out;
|
|
43386
|
+
}
|
|
43387
|
+
function looksLikeEvmAddress(value) {
|
|
43388
|
+
return /^0x[0-9a-fA-F]{40}$/.test(value);
|
|
43389
|
+
}
|
|
43390
|
+
function checkEvmAddress(value) {
|
|
43391
|
+
if (!looksLikeEvmAddress(value))
|
|
43392
|
+
return { ok: false, reason: "not 0x followed by 40 hex characters" };
|
|
43393
|
+
const body = value.slice(2);
|
|
43394
|
+
const checksummed = toChecksumAddress(value);
|
|
43395
|
+
const hasLower = /[a-f]/.test(body);
|
|
43396
|
+
const hasUpper = /[A-F]/.test(body);
|
|
43397
|
+
if (hasLower && hasUpper && checksummed !== value) {
|
|
43398
|
+
return { ok: false, reason: "its mixed-case EIP-55 checksum does not match" };
|
|
43399
|
+
}
|
|
43400
|
+
return { ok: true, address: checksummed };
|
|
43401
|
+
}
|
|
43402
|
+
function sameEvmAddress(a, b) {
|
|
43403
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
43404
|
+
}
|
|
43405
|
+
function evmAddressFromSecret(secret) {
|
|
43406
|
+
if (secret.length !== EVM_SECRET_BYTES) {
|
|
43407
|
+
throw new Error(`expected a ${EVM_SECRET_BYTES}-byte secp256k1 scalar, got ${secret.length}`);
|
|
43408
|
+
}
|
|
43409
|
+
const pub = secp256k1.getPublicKey(secret, false).slice(1);
|
|
43410
|
+
return toChecksumAddress(bytesToHex2(keccak_256(pub)).slice(-40));
|
|
43411
|
+
}
|
|
43412
|
+
function evmDerivationPath(index) {
|
|
43413
|
+
if (!Number.isInteger(index) || index < 0 || index >= 2147483648)
|
|
43414
|
+
throw new Error(`index out of range: ${index}`);
|
|
43415
|
+
return `m/44'/60'/${index}'/0/0`;
|
|
43416
|
+
}
|
|
43417
|
+
function deriveEvmKey(seed, index) {
|
|
43418
|
+
const path = evmDerivationPath(index);
|
|
43419
|
+
const root = HDKey.fromMasterSeed(seed);
|
|
43420
|
+
try {
|
|
43421
|
+
const leaf = root.derive(path);
|
|
43422
|
+
try {
|
|
43423
|
+
if (leaf.privateKey === null)
|
|
43424
|
+
throw new Error("BIP-32 derivation produced no private key");
|
|
43425
|
+
const secret = Uint8Array.from(leaf.privateKey);
|
|
43426
|
+
return { secret, address: evmAddressFromSecret(secret), path };
|
|
43427
|
+
} finally {
|
|
43428
|
+
leaf.wipePrivateData();
|
|
43429
|
+
}
|
|
43430
|
+
} finally {
|
|
43431
|
+
root.wipePrivateData();
|
|
43432
|
+
}
|
|
43433
|
+
}
|
|
43434
|
+
function buildNativeTransfer(input) {
|
|
43435
|
+
return { ...input, to: toChecksumAddress(input.to), data: new Uint8Array(0) };
|
|
43436
|
+
}
|
|
43437
|
+
function buildErc20Transfer(input) {
|
|
43438
|
+
const { token, recipient, amount, ...fees } = input;
|
|
43439
|
+
return { ...fees, to: toChecksumAddress(token), value: 0n, data: encodeErc20Transfer(recipient, amount) };
|
|
43440
|
+
}
|
|
43441
|
+
var ERC20_TRANSFER_SELECTOR = "0xa9059cbb";
|
|
43442
|
+
var ERC20_DECIMALS_SELECTOR = "0x313ce567";
|
|
43443
|
+
var ERC20_SYMBOL_SELECTOR = "0x95d89b41";
|
|
43444
|
+
var ERC20_BALANCE_OF_SELECTOR = "0x70a08231";
|
|
43445
|
+
function abiWord(value) {
|
|
43446
|
+
if (value < 0n || value >= 1n << 256n)
|
|
43447
|
+
throw new Error("uint256 out of range");
|
|
43448
|
+
const out = new Uint8Array(32);
|
|
43449
|
+
out.set(uintToMinimalBytes(value), 32 - uintToMinimalBytes(value).length);
|
|
43450
|
+
return out;
|
|
43451
|
+
}
|
|
43452
|
+
function abiAddress(address) {
|
|
43453
|
+
const out = new Uint8Array(32);
|
|
43454
|
+
out.set(hexToBytes2(address), 12);
|
|
43455
|
+
return out;
|
|
43456
|
+
}
|
|
43457
|
+
function encodeErc20Transfer(recipient, amount) {
|
|
43458
|
+
return concat2(hexToBytes2(ERC20_TRANSFER_SELECTOR), abiAddress(recipient), abiWord(amount));
|
|
43459
|
+
}
|
|
43460
|
+
var TYPE_2 = Uint8Array.of(2);
|
|
43461
|
+
function unsignedFields(tx) {
|
|
43462
|
+
return [
|
|
43463
|
+
uintToMinimalBytes(tx.chainId),
|
|
43464
|
+
uintToMinimalBytes(tx.nonce),
|
|
43465
|
+
uintToMinimalBytes(tx.maxPriorityFeePerGas),
|
|
43466
|
+
uintToMinimalBytes(tx.maxFeePerGas),
|
|
43467
|
+
uintToMinimalBytes(tx.gas),
|
|
43468
|
+
hexToBytes2(tx.to),
|
|
43469
|
+
uintToMinimalBytes(tx.value),
|
|
43470
|
+
tx.data,
|
|
43471
|
+
[]
|
|
43472
|
+
];
|
|
43473
|
+
}
|
|
43474
|
+
function signingPayload(tx) {
|
|
43475
|
+
return keccak_256(concat2(TYPE_2, rlpEncode(unsignedFields(tx))));
|
|
43476
|
+
}
|
|
43477
|
+
function signTransaction(tx, secret) {
|
|
43478
|
+
if (secret.length !== EVM_SECRET_BYTES) {
|
|
43479
|
+
throw new Error(`expected a ${EVM_SECRET_BYTES}-byte secp256k1 scalar, got ${secret.length}`);
|
|
43480
|
+
}
|
|
43481
|
+
const signature = secp256k1.sign(signingPayload(tx), secret, { lowS: true, prehash: false });
|
|
43482
|
+
const yParity = signature.recovery === 1 ? 1 : 0;
|
|
43483
|
+
const raw = concat2(TYPE_2, rlpEncode([
|
|
43484
|
+
...unsignedFields(tx),
|
|
43485
|
+
uintToMinimalBytes(BigInt(yParity)),
|
|
43486
|
+
uintToMinimalBytes(signature.r),
|
|
43487
|
+
uintToMinimalBytes(signature.s)
|
|
43488
|
+
]));
|
|
43489
|
+
return { raw, hash: bytesToHex2(keccak_256(raw)), yParity, r: signature.r, s: signature.s };
|
|
43490
|
+
}
|
|
43491
|
+
function formatUnits(raw, decimals) {
|
|
43492
|
+
const negative = raw < 0n;
|
|
43493
|
+
const magnitude = negative ? -raw : raw;
|
|
43494
|
+
const base = 10n ** BigInt(decimals);
|
|
43495
|
+
const whole = magnitude / base;
|
|
43496
|
+
const fraction = decimals === 0 ? "" : (magnitude % base).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
43497
|
+
const text = fraction === "" ? whole.toString() : `${whole}.${fraction}`;
|
|
43498
|
+
return negative ? `-${text}` : text;
|
|
43499
|
+
}
|
|
43500
|
+
function parseUnits(decimal, decimals) {
|
|
43501
|
+
if (!/^\d+(\.\d+)?$/.test(decimal))
|
|
43502
|
+
return { ok: false, reason: "not-a-number" };
|
|
43503
|
+
const [whole, fraction = ""] = decimal.split(".");
|
|
43504
|
+
if (fraction.length > decimals)
|
|
43505
|
+
return { ok: false, reason: "precision" };
|
|
43506
|
+
const raw = BigInt((whole ?? "0") + fraction.padEnd(decimals, "0"));
|
|
43507
|
+
if (raw === 0n)
|
|
43508
|
+
return { ok: false, reason: "zero" };
|
|
43509
|
+
return { ok: true, raw };
|
|
43510
|
+
}
|
|
43511
|
+
|
|
43512
|
+
class EvmRpcError extends Error {
|
|
43513
|
+
kind;
|
|
43514
|
+
method;
|
|
43515
|
+
rpcCode;
|
|
43516
|
+
constructor(kind, method, message, rpcCode) {
|
|
43517
|
+
super(message);
|
|
43518
|
+
this.name = "EvmRpcError";
|
|
43519
|
+
this.kind = kind;
|
|
43520
|
+
this.method = method;
|
|
43521
|
+
this.rpcCode = rpcCode;
|
|
43522
|
+
}
|
|
43523
|
+
}
|
|
43524
|
+
function decodeAbiString(bytes) {
|
|
43525
|
+
if (bytes.length === 0)
|
|
43526
|
+
return;
|
|
43527
|
+
if (bytes.length >= 64) {
|
|
43528
|
+
const offset = Number(hexToBigInt(bytesToHex2(bytes.subarray(0, 32))));
|
|
43529
|
+
if (offset + 32 <= bytes.length) {
|
|
43530
|
+
const length = Number(hexToBigInt(bytesToHex2(bytes.subarray(offset, offset + 32))));
|
|
43531
|
+
if (offset + 32 + length <= bytes.length) {
|
|
43532
|
+
return new TextDecoder().decode(bytes.subarray(offset + 32, offset + 32 + length));
|
|
43533
|
+
}
|
|
43534
|
+
}
|
|
43535
|
+
}
|
|
43536
|
+
if (bytes.length === 32) {
|
|
43537
|
+
let end = 32;
|
|
43538
|
+
while (end > 0 && bytes[end - 1] === 0)
|
|
43539
|
+
end--;
|
|
43540
|
+
const text = new TextDecoder().decode(bytes.subarray(0, end));
|
|
43541
|
+
return /^[\x20-\x7e]+$/.test(text) ? text : undefined;
|
|
43542
|
+
}
|
|
43543
|
+
return;
|
|
43544
|
+
}
|
|
43545
|
+
function createEvmRpc(url, fetchFn) {
|
|
43546
|
+
let id = 0;
|
|
43547
|
+
async function call(method, params) {
|
|
43548
|
+
id += 1;
|
|
43549
|
+
let res;
|
|
43550
|
+
try {
|
|
43551
|
+
res = await fetchFn(url, {
|
|
43552
|
+
method: "POST",
|
|
43553
|
+
headers: { "content-type": "application/json" },
|
|
43554
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
43555
|
+
});
|
|
43556
|
+
} catch (error) {
|
|
43557
|
+
throw new EvmRpcError("transport", method, `RPC ${method} failed: ${error instanceof Error ? error.message : error}`);
|
|
43558
|
+
}
|
|
43559
|
+
if (!res.ok)
|
|
43560
|
+
throw new EvmRpcError("transport", method, `RPC ${method} failed: HTTP ${res.status}`);
|
|
43561
|
+
let json;
|
|
43562
|
+
try {
|
|
43563
|
+
json = await res.json();
|
|
43564
|
+
} catch {
|
|
43565
|
+
throw new EvmRpcError("transport", method, `RPC ${method} failed: the answer was not JSON`);
|
|
43566
|
+
}
|
|
43567
|
+
if (json.error) {
|
|
43568
|
+
throw new EvmRpcError("rpc", method, `${json.error.message ?? "RPC error"}`, typeof json.error.code === "number" ? json.error.code : undefined);
|
|
43569
|
+
}
|
|
43570
|
+
return json.result;
|
|
43571
|
+
}
|
|
43572
|
+
const asQuantity = (value, method) => {
|
|
43573
|
+
if (typeof value !== "string")
|
|
43574
|
+
throw new EvmRpcError("rpc", method, `RPC ${method} answered without a quantity`);
|
|
43575
|
+
return hexToBigInt(value);
|
|
43576
|
+
};
|
|
43577
|
+
return {
|
|
43578
|
+
async chainId() {
|
|
43579
|
+
return asQuantity(await call("eth_chainId", []), "eth_chainId");
|
|
43580
|
+
},
|
|
43581
|
+
async getTransactionCount(address, tag) {
|
|
43582
|
+
return asQuantity(await call("eth_getTransactionCount", [address, tag]), "eth_getTransactionCount");
|
|
43583
|
+
},
|
|
43584
|
+
async estimateGas(input) {
|
|
43585
|
+
return asQuantity(await call("eth_estimateGas", [
|
|
43586
|
+
{
|
|
43587
|
+
from: input.from,
|
|
43588
|
+
to: input.to,
|
|
43589
|
+
value: quantity(input.value),
|
|
43590
|
+
...input.data.length > 0 ? { data: bytesToHex2(input.data) } : {}
|
|
43591
|
+
}
|
|
43592
|
+
]), "eth_estimateGas");
|
|
43593
|
+
},
|
|
43594
|
+
async feeHistory(blockCount, newestBlock, rewardPercentiles) {
|
|
43595
|
+
const r = await call("eth_feeHistory", [
|
|
43596
|
+
quantity(blockCount),
|
|
43597
|
+
newestBlock,
|
|
43598
|
+
rewardPercentiles
|
|
43599
|
+
]);
|
|
43600
|
+
const base = Array.isArray(r?.baseFeePerGas) ? r.baseFeePerGas : [];
|
|
43601
|
+
if (base.length === 0)
|
|
43602
|
+
throw new EvmRpcError("rpc", "eth_feeHistory", "RPC eth_feeHistory answered no baseFeePerGas");
|
|
43603
|
+
return {
|
|
43604
|
+
baseFeePerGas: base.map((value) => asQuantity(value, "eth_feeHistory")),
|
|
43605
|
+
reward: (Array.isArray(r.reward) ? r.reward : []).map((row) => (Array.isArray(row) ? row : []).map((value) => asQuantity(value, "eth_feeHistory")))
|
|
43606
|
+
};
|
|
43607
|
+
},
|
|
43608
|
+
async maxPriorityFeePerGas() {
|
|
43609
|
+
return asQuantity(await call("eth_maxPriorityFeePerGas", []), "eth_maxPriorityFeePerGas");
|
|
43610
|
+
},
|
|
43611
|
+
async getBalance(address) {
|
|
43612
|
+
return asQuantity(await call("eth_getBalance", [address, "latest"]), "eth_getBalance");
|
|
43613
|
+
},
|
|
43614
|
+
async call(input) {
|
|
43615
|
+
const r = await call("eth_call", [{ to: input.to, data: bytesToHex2(input.data) }, "latest"]);
|
|
43616
|
+
if (typeof r !== "string")
|
|
43617
|
+
throw new EvmRpcError("rpc", "eth_call", "RPC eth_call answered without data");
|
|
43618
|
+
return hexToBytes2(r);
|
|
43619
|
+
},
|
|
43620
|
+
async sendRawTransaction(raw) {
|
|
43621
|
+
const r = await call("eth_sendRawTransaction", [bytesToHex2(raw)]);
|
|
43622
|
+
if (typeof r !== "string") {
|
|
43623
|
+
throw new EvmRpcError("rpc", "eth_sendRawTransaction", "RPC eth_sendRawTransaction answered without a hash");
|
|
43624
|
+
}
|
|
43625
|
+
return r;
|
|
43626
|
+
},
|
|
43627
|
+
async getTransactionReceipt(hash) {
|
|
43628
|
+
const r = await call("eth_getTransactionReceipt", [hash]);
|
|
43629
|
+
if (r === null || r === undefined)
|
|
43630
|
+
return null;
|
|
43631
|
+
const status = asQuantity(r.status, "eth_getTransactionReceipt");
|
|
43632
|
+
return {
|
|
43633
|
+
status: status === 1n ? 1 : 0,
|
|
43634
|
+
blockNumber: asQuantity(r.blockNumber, "eth_getTransactionReceipt"),
|
|
43635
|
+
transactionHash: typeof r.transactionHash === "string" ? r.transactionHash : hash
|
|
43636
|
+
};
|
|
43637
|
+
},
|
|
43638
|
+
async blockNumber() {
|
|
43639
|
+
return asQuantity(await call("eth_blockNumber", []), "eth_blockNumber");
|
|
43640
|
+
},
|
|
43641
|
+
async erc20Decimals(token) {
|
|
43642
|
+
const answer = await this.call({ to: token, data: hexToBytes2(ERC20_DECIMALS_SELECTOR) });
|
|
43643
|
+
if (answer.length !== 32)
|
|
43644
|
+
throw new EvmRpcError("rpc", "eth_call", "the contract did not answer decimals()");
|
|
43645
|
+
const value = hexToBigInt(bytesToHex2(answer));
|
|
43646
|
+
if (value > 255n)
|
|
43647
|
+
throw new EvmRpcError("rpc", "eth_call", "the contract's decimals() is not a uint8");
|
|
43648
|
+
return Number(value);
|
|
43649
|
+
},
|
|
43650
|
+
async erc20Symbol(token) {
|
|
43651
|
+
try {
|
|
43652
|
+
return decodeAbiString(await this.call({ to: token, data: hexToBytes2(ERC20_SYMBOL_SELECTOR) }));
|
|
43653
|
+
} catch (error) {
|
|
43654
|
+
if (error instanceof EvmRpcError && error.kind === "rpc")
|
|
43655
|
+
return;
|
|
43656
|
+
throw error;
|
|
43657
|
+
}
|
|
43658
|
+
},
|
|
43659
|
+
async erc20BalanceOf(token, owner) {
|
|
43660
|
+
const answer = await this.call({
|
|
43661
|
+
to: token,
|
|
43662
|
+
data: concat2(hexToBytes2(ERC20_BALANCE_OF_SELECTOR), abiAddress(owner))
|
|
43663
|
+
});
|
|
43664
|
+
if (answer.length !== 32)
|
|
43665
|
+
throw new EvmRpcError("rpc", "eth_call", "the contract did not answer balanceOf()");
|
|
43666
|
+
return hexToBigInt(bytesToHex2(answer));
|
|
43667
|
+
}
|
|
43668
|
+
};
|
|
43669
|
+
}
|
|
43670
|
+
var FEE_HISTORY_BLOCKS = 10;
|
|
43671
|
+
async function quoteFees(rpc) {
|
|
43672
|
+
const history = await rpc.feeHistory(FEE_HISTORY_BLOCKS, "latest", [50]);
|
|
43673
|
+
const baseFee = history.baseFeePerGas[history.baseFeePerGas.length - 1];
|
|
43674
|
+
let tip;
|
|
43675
|
+
let tipSource;
|
|
43676
|
+
try {
|
|
43677
|
+
tip = await rpc.maxPriorityFeePerGas();
|
|
43678
|
+
tipSource = "eth_maxPriorityFeePerGas";
|
|
43679
|
+
} catch (error) {
|
|
43680
|
+
if (!(error instanceof EvmRpcError) || error.kind !== "rpc")
|
|
43681
|
+
throw error;
|
|
43682
|
+
const rewards = history.reward.map((row) => row[0] ?? 0n).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
43683
|
+
tip = rewards.length === 0 ? 0n : rewards[Math.floor(rewards.length / 2)];
|
|
43684
|
+
tipSource = "eth_feeHistory";
|
|
43685
|
+
}
|
|
43686
|
+
return { baseFee, maxPriorityFeePerGas: tip, maxFeePerGas: 2n * baseFee + tip, tipSource };
|
|
43687
|
+
}
|
|
43688
|
+
function gasWithHeadroom(estimate) {
|
|
43689
|
+
return (estimate * 12n + 9n) / 10n;
|
|
43690
|
+
}
|
|
43691
|
+
function requiredDepth(chainId) {
|
|
43692
|
+
return chainId === BigInt(HOOD_CHAIN_ID) ? 1 : 2;
|
|
43693
|
+
}
|
|
43694
|
+
function rpcHostOf(url) {
|
|
43695
|
+
return new URL(url).host;
|
|
43696
|
+
}
|
|
43697
|
+
function resolveEvmRpcUrl(flag, envValue, flagName) {
|
|
43698
|
+
const fromFlag = flag?.trim() || undefined;
|
|
43699
|
+
const fromEnv = envValue?.trim() || undefined;
|
|
43700
|
+
const url = fromFlag ?? fromEnv ?? DEFAULT_HOOD_RPC_URL;
|
|
43701
|
+
const source = fromFlag !== undefined ? flagName : fromEnv !== undefined ? EVM_RPC_URL_ENV : undefined;
|
|
43702
|
+
let parsed;
|
|
43703
|
+
try {
|
|
43704
|
+
parsed = new URL(url);
|
|
43705
|
+
} catch {
|
|
43706
|
+
return { error: `${source ?? flagName} is not a valid URL: ${url}` };
|
|
43707
|
+
}
|
|
43708
|
+
const local = parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost";
|
|
43709
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
|
|
43710
|
+
return {
|
|
43711
|
+
error: `${source ?? flagName} must be https:// (plain http is allowed only for 127.0.0.1 / localhost).`
|
|
43712
|
+
};
|
|
43713
|
+
}
|
|
43714
|
+
return { url, builtIn: url === DEFAULT_HOOD_RPC_URL };
|
|
43715
|
+
}
|
|
43716
|
+
|
|
42463
43717
|
// src/vault/ed25519.ts
|
|
42464
43718
|
init_ed25519();
|
|
42465
43719
|
init_esm();
|
|
@@ -42495,7 +43749,7 @@ var sha5122 = sha512;
|
|
|
42495
43749
|
|
|
42496
43750
|
// src/vault/slip10.ts
|
|
42497
43751
|
init_errors();
|
|
42498
|
-
var
|
|
43752
|
+
var HARDENED_OFFSET2 = 2147483648;
|
|
42499
43753
|
var ED25519_SEED_KEY = new TextEncoder().encode("ed25519 seed");
|
|
42500
43754
|
function masterFromSeed(seed) {
|
|
42501
43755
|
const i = hmac(sha5122, ED25519_SEED_KEY, seed);
|
|
@@ -42506,7 +43760,7 @@ function masterFromSeed(seed) {
|
|
|
42506
43760
|
}
|
|
42507
43761
|
}
|
|
42508
43762
|
function deriveChild(node, index) {
|
|
42509
|
-
if (!Number.isInteger(index) || index <
|
|
43763
|
+
if (!Number.isInteger(index) || index < HARDENED_OFFSET2 || index > 4294967295) {
|
|
42510
43764
|
throw new VaultError("VAULT_INDEX_INVALID", `SLIP-0010 Ed25519 has no non-hardened children; refusing index ${index}.`);
|
|
42511
43765
|
}
|
|
42512
43766
|
const data = new Uint8Array(1 + 32 + 4);
|
|
@@ -42533,9 +43787,9 @@ function parsePath(path) {
|
|
|
42533
43787
|
if (!hardened) {
|
|
42534
43788
|
throw new VaultError("VAULT_INDEX_INVALID", `SLIP-0010 Ed25519 has no non-hardened children; ${path} asks for one.`);
|
|
42535
43789
|
}
|
|
42536
|
-
if (index >=
|
|
43790
|
+
if (index >= HARDENED_OFFSET2)
|
|
42537
43791
|
throw new VaultError("VAULT_INDEX_INVALID", `Derivation index out of range: ${part}`);
|
|
42538
|
-
return index +
|
|
43792
|
+
return index + HARDENED_OFFSET2;
|
|
42539
43793
|
});
|
|
42540
43794
|
}
|
|
42541
43795
|
function derivePath(seed, path) {
|
|
@@ -42570,6 +43824,10 @@ function solanaExternalPath(index) {
|
|
|
42570
43824
|
function evmPath(index) {
|
|
42571
43825
|
return `m/44'/60'/${assertIndex(index)}'/0/0`;
|
|
42572
43826
|
}
|
|
43827
|
+
function evmIndexOfPath(path) {
|
|
43828
|
+
const match = /^m\/44'\/60'\/(\d+)'\/0\/0$/.exec(path);
|
|
43829
|
+
return match?.[1] === undefined ? undefined : Number(match[1]);
|
|
43830
|
+
}
|
|
42573
43831
|
function pathForBranch(branch, index) {
|
|
42574
43832
|
if (branch === "solanaVault")
|
|
42575
43833
|
return solanaVaultPath(index);
|
|
@@ -42627,6 +43885,15 @@ async function deriveSolanaKey(entropy, path) {
|
|
|
42627
43885
|
wipe(seed);
|
|
42628
43886
|
}
|
|
42629
43887
|
}
|
|
43888
|
+
async function deriveEvmKeyFromRoot(entropy, index) {
|
|
43889
|
+
const seed = await seedFromEntropy(entropy);
|
|
43890
|
+
try {
|
|
43891
|
+
const derived = deriveEvmKey(seed, index);
|
|
43892
|
+
return { ...derived, secret: ownSecret(derived.secret) };
|
|
43893
|
+
} finally {
|
|
43894
|
+
wipe(seed);
|
|
43895
|
+
}
|
|
43896
|
+
}
|
|
42630
43897
|
// src/vault/local-sweep.ts
|
|
42631
43898
|
init_esm();
|
|
42632
43899
|
init_solana_lite();
|
|
@@ -43316,12 +44583,11 @@ async function vaultNewKey(args, ctx) {
|
|
|
43316
44583
|
return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
43317
44584
|
const chain2 = parsed.values["--chain"];
|
|
43318
44585
|
if (chain2 === undefined)
|
|
43319
|
-
return usage(ctx, "--chain solana is required.");
|
|
43320
|
-
if (chain2
|
|
43321
|
-
return usage(ctx,
|
|
44586
|
+
return usage(ctx, "--chain solana|evm is required.");
|
|
44587
|
+
if (chain2 !== "solana" && chain2 !== "evm") {
|
|
44588
|
+
return usage(ctx, `Unknown chain: ${chain2}. This release derives Solana and EVM keys (--chain solana|evm).`);
|
|
43322
44589
|
}
|
|
43323
|
-
|
|
43324
|
-
return usage(ctx, `Unknown chain: ${chain2}. This release derives Solana keys only.`);
|
|
44590
|
+
const branch = chain2 === "evm" ? "evm" : "solanaVault";
|
|
43325
44591
|
if (!refuseEnvPassphrase(ctx))
|
|
43326
44592
|
return 1;
|
|
43327
44593
|
if (!requireTty(ctx, "vault new-key"))
|
|
@@ -43356,7 +44622,7 @@ async function vaultNewKey(args, ctx) {
|
|
|
43356
44622
|
suggestion: "Create a second vault with a fresh root (`candle vault init`) and move the funds across with `candle vault transfer`. There is no flag for this: no fact you could assert would make the old boundary known."
|
|
43357
44623
|
});
|
|
43358
44624
|
}
|
|
43359
|
-
const clash = labelClash(vault.index, plannedLabels(vault.index.hd, batch, parsed.values["--label"]));
|
|
44625
|
+
const clash = labelClash(vault.index, plannedLabels(vault.index.hd, batch, parsed.values["--label"], branch));
|
|
43360
44626
|
if (clash !== undefined) {
|
|
43361
44627
|
return usage(ctx, batch.labels !== undefined ? `A key labelled ${clash} already exists in this vault; every --labels-from name must be new.` : parsed.values["--label"] !== undefined ? `A key labelled ${clash} already exists in this vault; choose another --label.` : `A key labelled ${clash} already exists in this vault; pass --label <name> to choose a different name for this key.`);
|
|
43362
44628
|
}
|
|
@@ -43366,41 +44632,52 @@ async function vaultNewKey(args, ctx) {
|
|
|
43366
44632
|
const root = await decryptRoot(vault);
|
|
43367
44633
|
try {
|
|
43368
44634
|
for (let made = 0;made < batch.count; made++) {
|
|
43369
|
-
const index = nextAllocatableIndex(current.index.hd.nextIndex
|
|
43370
|
-
const derivationPath = solanaVaultPath(index);
|
|
43371
|
-
const label = batch.labels?.[made] ?? parsed.values["--label"] ??
|
|
44635
|
+
const index = nextAllocatableIndex(current.index.hd.nextIndex[branch], current.index.hd.exposedIndexes[branch]);
|
|
44636
|
+
const derivationPath = branch === "evm" ? evmPath(index) : solanaVaultPath(index);
|
|
44637
|
+
const label = batch.labels?.[made] ?? parsed.values["--label"] ?? defaultLabel(branch, index);
|
|
43372
44638
|
let address;
|
|
43373
44639
|
let keyId;
|
|
43374
44640
|
let blob;
|
|
43375
|
-
|
|
43376
|
-
|
|
43377
|
-
|
|
43378
|
-
|
|
43379
|
-
|
|
43380
|
-
|
|
43381
|
-
|
|
44641
|
+
if (branch === "evm") {
|
|
44642
|
+
const key = await deriveEvmKeyFromRoot(root, index);
|
|
44643
|
+
try {
|
|
44644
|
+
address = key.address;
|
|
44645
|
+
keyId = freshKeyId();
|
|
44646
|
+
blob = await sealKeyBlob(current, keyId, key.secret);
|
|
44647
|
+
} finally {
|
|
44648
|
+
wipe(key.secret);
|
|
44649
|
+
}
|
|
44650
|
+
} else {
|
|
44651
|
+
const key = await deriveSolanaKey(root, derivationPath);
|
|
44652
|
+
try {
|
|
44653
|
+
address = key.address;
|
|
44654
|
+
keyId = freshKeyId();
|
|
44655
|
+
blob = await sealKeyBlob(current, keyId, key.secret64);
|
|
44656
|
+
} finally {
|
|
44657
|
+
wipe(key.secret64);
|
|
44658
|
+
}
|
|
43382
44659
|
}
|
|
43383
44660
|
const entry = {
|
|
43384
44661
|
id: keyId,
|
|
43385
|
-
chain:
|
|
43386
|
-
curve: "ed25519",
|
|
44662
|
+
chain: chain2,
|
|
44663
|
+
curve: chain2 === "evm" ? "secp256k1" : "ed25519",
|
|
43387
44664
|
address,
|
|
43388
44665
|
label,
|
|
43389
44666
|
createdAt: new Date(deps.now()).toISOString(),
|
|
43390
44667
|
role: "vault",
|
|
43391
44668
|
origin: "derived",
|
|
43392
|
-
derivation: { scheme: DERIVATION_SCHEME, path: derivationPath },
|
|
44669
|
+
derivation: { scheme: chain2 === "evm" ? EVM_DERIVATION_SCHEME : DERIVATION_SCHEME, path: derivationPath },
|
|
43393
44670
|
exposure: { everRemoteExposed: false, everExported: false }
|
|
43394
44671
|
};
|
|
43395
44672
|
current = await commitVault(current, {
|
|
43396
44673
|
index: {
|
|
43397
|
-
hd: { ...current.index.hd, nextIndex: { ...current.index.hd.nextIndex,
|
|
44674
|
+
hd: { ...current.index.hd, nextIndex: { ...current.index.hd.nextIndex, [branch]: index + 1 } },
|
|
43398
44675
|
entries: [...current.index.entries, entry]
|
|
43399
44676
|
},
|
|
43400
44677
|
addKeys: [blob]
|
|
43401
44678
|
}, deps);
|
|
43402
44679
|
derived.push({ address, label, path: derivationPath, index, keyId });
|
|
43403
|
-
await verifyWrittenFromDisk(current, address, keyId);
|
|
44680
|
+
await verifyWrittenFromDisk(current, address, keyId, chain2);
|
|
43404
44681
|
last = { address, keyId };
|
|
43405
44682
|
if (!ctx.json && batchRequested)
|
|
43406
44683
|
deps.stdout.write(`${address} ${label} ${derivationPath}
|
|
@@ -43415,7 +44692,7 @@ async function vaultNewKey(args, ctx) {
|
|
|
43415
44692
|
const only = derived[0];
|
|
43416
44693
|
if (only === undefined || last === undefined)
|
|
43417
44694
|
throw new VaultError("VAULT_WRITE_FAILED", "No key was derived.");
|
|
43418
|
-
await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
|
|
44695
|
+
await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx, chain2);
|
|
43419
44696
|
if (ctx.json) {
|
|
43420
44697
|
if (batchRequested)
|
|
43421
44698
|
writeJson(deps, { ok: true, count: derived.length, keys: derived });
|
|
@@ -43454,16 +44731,25 @@ ${derived.length} of ${requested} keys were created and ARE in the vault; the va
|
|
|
43454
44731
|
` + `Re-run for the remaining ${requested - derived.length}${derived.length > 0 ? " (with a --labels-from file holding the names that did not land)" : ""}.
|
|
43455
44732
|
`);
|
|
43456
44733
|
}
|
|
43457
|
-
function plannedLabels(hd, batch, labelFlag) {
|
|
44734
|
+
function plannedLabels(hd, batch, labelFlag, branch = "solanaVault") {
|
|
43458
44735
|
const labels = [];
|
|
43459
|
-
let counter = hd.nextIndex
|
|
44736
|
+
let counter = hd.nextIndex[branch];
|
|
43460
44737
|
for (let made = 0;made < batch.count; made++) {
|
|
43461
|
-
const index = nextAllocatableIndex(counter, hd.exposedIndexes
|
|
43462
|
-
labels.push(batch.labels?.[made] ?? labelFlag ??
|
|
44738
|
+
const index = nextAllocatableIndex(counter, hd.exposedIndexes[branch]);
|
|
44739
|
+
labels.push(batch.labels?.[made] ?? labelFlag ?? defaultLabel(branch, index));
|
|
43463
44740
|
counter = index + 1;
|
|
43464
44741
|
}
|
|
43465
44742
|
return labels;
|
|
43466
44743
|
}
|
|
44744
|
+
function defaultLabel(branch, index) {
|
|
44745
|
+
return branch === "evm" ? `evm-${index}` : `key-${index}`;
|
|
44746
|
+
}
|
|
44747
|
+
function addressOfSecret(chain2, secret) {
|
|
44748
|
+
return chain2 === "evm" ? evmAddressFromSecret(secret) : addressFromSecret64(secret);
|
|
44749
|
+
}
|
|
44750
|
+
function sameAddress(chain2, a, b) {
|
|
44751
|
+
return chain2 === "evm" ? sameEvmAddress(a, b) : a === b;
|
|
44752
|
+
}
|
|
43467
44753
|
function labelClash(index, planned) {
|
|
43468
44754
|
const taken = new Set(index.entries.map((entry) => entry.label));
|
|
43469
44755
|
const seen = new Set;
|
|
@@ -43480,7 +44766,7 @@ function nextAllocatableIndex(counter, exposed) {
|
|
|
43480
44766
|
index++;
|
|
43481
44767
|
return index;
|
|
43482
44768
|
}
|
|
43483
|
-
async function verifyWrittenFromDisk(vault, address, keyId) {
|
|
44769
|
+
async function verifyWrittenFromDisk(vault, address, keyId, chain2 = "solana") {
|
|
43484
44770
|
const raw = await readVaultRaw(vault.path);
|
|
43485
44771
|
if (raw === null) {
|
|
43486
44772
|
throw new VaultError("VAULT_WRITE_FAILED", `The vault at ${vault.path} could not be read back after the write.`);
|
|
@@ -43488,14 +44774,14 @@ async function verifyWrittenFromDisk(vault, address, keyId) {
|
|
|
43488
44774
|
const onDisk = { ...vault, raw, file: parseVaultFile(raw) };
|
|
43489
44775
|
const secret = await decryptKey(onDisk, keyId);
|
|
43490
44776
|
try {
|
|
43491
|
-
if (
|
|
44777
|
+
if (!sameAddress(chain2, addressOfSecret(chain2, secret), address)) {
|
|
43492
44778
|
throw new VaultError("VAULT_VERIFY_FAILED", "The key written to the vault does not produce the address just derived.");
|
|
43493
44779
|
}
|
|
43494
44780
|
} finally {
|
|
43495
44781
|
wipe(secret);
|
|
43496
44782
|
}
|
|
43497
44783
|
}
|
|
43498
|
-
async function verifyWritten(path, address, keyId, reopen, _ctx) {
|
|
44784
|
+
async function verifyWritten(path, address, keyId, reopen, _ctx, chain2 = "solana") {
|
|
43499
44785
|
const raw = await readVaultRaw(path);
|
|
43500
44786
|
if (raw === null)
|
|
43501
44787
|
throw new VaultError("VAULT_WRITE_FAILED", `The vault at ${path} could not be read back after the write.`);
|
|
@@ -43503,7 +44789,7 @@ async function verifyWritten(path, address, keyId, reopen, _ctx) {
|
|
|
43503
44789
|
try {
|
|
43504
44790
|
const secret = await decryptKey(reopened, keyId);
|
|
43505
44791
|
try {
|
|
43506
|
-
if (
|
|
44792
|
+
if (!sameAddress(chain2, addressOfSecret(chain2, secret), address)) {
|
|
43507
44793
|
throw new VaultError("VAULT_VERIFY_FAILED", "The key written to the vault does not produce the address just derived.");
|
|
43508
44794
|
}
|
|
43509
44795
|
} finally {
|
|
@@ -43718,6 +45004,8 @@ async function externalSweep(args, ctx) {
|
|
|
43718
45004
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
43719
45005
|
});
|
|
43720
45006
|
const vault = hold(opened.vault);
|
|
45007
|
+
assertNotEvmEntry(vault.index, source, "external sweep");
|
|
45008
|
+
assertNotEvmEntry(vault.index, to, "external sweep");
|
|
43721
45009
|
const sourceEntry = requireExternalEntry(vault.index, source);
|
|
43722
45010
|
const destination = findVaultRoleEntry(vault.index, to);
|
|
43723
45011
|
if (destination === undefined || destination.role !== "vault") {
|
|
@@ -43821,6 +45109,10 @@ var NO_API_KEY = {
|
|
|
43821
45109
|
message: "No API key for this profile.",
|
|
43822
45110
|
suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
|
|
43823
45111
|
};
|
|
45112
|
+
function widenRefusedHint(prefix, walletIds) {
|
|
45113
|
+
const wallets = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
|
|
45114
|
+
return `To move TEE wallets to this key, run: candle tee rebind ${wallets} --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).";
|
|
45115
|
+
}
|
|
43824
45116
|
function formatTimestamp2(ms) {
|
|
43825
45117
|
return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
|
|
43826
45118
|
}
|
|
@@ -43916,7 +45208,15 @@ async function keysWalletsSet(args, ctx) {
|
|
|
43916
45208
|
env: deps.env
|
|
43917
45209
|
});
|
|
43918
45210
|
if (!result.ok) {
|
|
43919
|
-
|
|
45211
|
+
if (result.code !== "LOOSEN_REQUIRES_SESSION") {
|
|
45212
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
45213
|
+
return 1;
|
|
45214
|
+
}
|
|
45215
|
+
const hint = widenRefusedHint(prefix, walletIds);
|
|
45216
|
+
writeFailure(deps, { ...result, uiHint: hint }, { apiUrl, authType: "key" }, json);
|
|
45217
|
+
if (!json)
|
|
45218
|
+
deps.stderr.write(`${hint}
|
|
45219
|
+
`);
|
|
43920
45220
|
return 1;
|
|
43921
45221
|
}
|
|
43922
45222
|
if (json) {
|
|
@@ -46487,6 +47787,7 @@ function programNameOf(programId) {
|
|
|
46487
47787
|
// src/commands/sign.ts
|
|
46488
47788
|
init_solana_lite();
|
|
46489
47789
|
init_errors();
|
|
47790
|
+
init_promote_support();
|
|
46490
47791
|
init_store();
|
|
46491
47792
|
init_vault_support();
|
|
46492
47793
|
async function readInput(ctx, file) {
|
|
@@ -46665,6 +47966,7 @@ async function sign2(args, ctx) {
|
|
|
46665
47966
|
const vault = hold(opened.vault);
|
|
46666
47967
|
const named = [];
|
|
46667
47968
|
for (const requested of lifted.values) {
|
|
47969
|
+
assertNotEvmEntry(vault.index, requested, "candle sign");
|
|
46668
47970
|
const entry = findExternalEntry(vault.index, requested);
|
|
46669
47971
|
if (entry === undefined) {
|
|
46670
47972
|
const other = vault.index.entries.find((candidate) => candidate.label === requested || candidate.address === requested);
|
|
@@ -46820,6 +48122,7 @@ async function signMessage2(args, ctx) {
|
|
|
46820
48122
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
46821
48123
|
});
|
|
46822
48124
|
const vault = hold(opened.vault);
|
|
48125
|
+
assertNotEvmEntry(vault.index, requested, "candle sign message");
|
|
46823
48126
|
const entry = findExternalEntry(vault.index, requested);
|
|
46824
48127
|
if (entry === undefined) {
|
|
46825
48128
|
const other = vault.index.entries.find((candidate) => candidate.label === requested || candidate.address === requested);
|
|
@@ -47469,11 +48772,11 @@ async function resolveTeeAddress(ctx, _parsed, address, openLegacy, access3 = "r
|
|
|
47469
48772
|
} catch (error) {
|
|
47470
48773
|
if (isUsageError(error)) {
|
|
47471
48774
|
writeUsageFailure(ctx.deps, error.message, ctx.json);
|
|
47472
|
-
return { ok: false, code: 2 };
|
|
48775
|
+
return { ok: false, code: 2, reported: true };
|
|
47473
48776
|
}
|
|
47474
48777
|
if (isVaultError(error)) {
|
|
47475
48778
|
writeLocalFailure(ctx.deps, { code: error.code, message: error.message, ...error.suggestion ? { suggestion: error.suggestion } : {} }, ctx.json);
|
|
47476
|
-
return { ok: false, code: error.exitCode };
|
|
48779
|
+
return { ok: false, code: error.exitCode, reported: true };
|
|
47477
48780
|
}
|
|
47478
48781
|
throw error;
|
|
47479
48782
|
}
|
|
@@ -47823,7 +49126,7 @@ var sigma16 = _utf8ToBytes("expand 16-byte k");
|
|
|
47823
49126
|
var sigma32 = _utf8ToBytes("expand 32-byte k");
|
|
47824
49127
|
var sigma16_32 = u322(sigma16);
|
|
47825
49128
|
var sigma32_32 = u322(sigma32);
|
|
47826
|
-
function
|
|
49129
|
+
function rotl2(a, b) {
|
|
47827
49130
|
return a << b | a >>> 32 - b;
|
|
47828
49131
|
}
|
|
47829
49132
|
function isAligned322(b) {
|
|
@@ -48238,69 +49541,69 @@ function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
|
|
48238
49541
|
let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
|
48239
49542
|
for (let r = 0;r < rounds; r += 2) {
|
|
48240
49543
|
x00 = x00 + x04 | 0;
|
|
48241
|
-
x12 =
|
|
49544
|
+
x12 = rotl2(x12 ^ x00, 16);
|
|
48242
49545
|
x08 = x08 + x12 | 0;
|
|
48243
|
-
x04 =
|
|
49546
|
+
x04 = rotl2(x04 ^ x08, 12);
|
|
48244
49547
|
x00 = x00 + x04 | 0;
|
|
48245
|
-
x12 =
|
|
49548
|
+
x12 = rotl2(x12 ^ x00, 8);
|
|
48246
49549
|
x08 = x08 + x12 | 0;
|
|
48247
|
-
x04 =
|
|
49550
|
+
x04 = rotl2(x04 ^ x08, 7);
|
|
48248
49551
|
x01 = x01 + x05 | 0;
|
|
48249
|
-
x13 =
|
|
49552
|
+
x13 = rotl2(x13 ^ x01, 16);
|
|
48250
49553
|
x09 = x09 + x13 | 0;
|
|
48251
|
-
x05 =
|
|
49554
|
+
x05 = rotl2(x05 ^ x09, 12);
|
|
48252
49555
|
x01 = x01 + x05 | 0;
|
|
48253
|
-
x13 =
|
|
49556
|
+
x13 = rotl2(x13 ^ x01, 8);
|
|
48254
49557
|
x09 = x09 + x13 | 0;
|
|
48255
|
-
x05 =
|
|
49558
|
+
x05 = rotl2(x05 ^ x09, 7);
|
|
48256
49559
|
x02 = x02 + x06 | 0;
|
|
48257
|
-
x14 =
|
|
49560
|
+
x14 = rotl2(x14 ^ x02, 16);
|
|
48258
49561
|
x10 = x10 + x14 | 0;
|
|
48259
|
-
x06 =
|
|
49562
|
+
x06 = rotl2(x06 ^ x10, 12);
|
|
48260
49563
|
x02 = x02 + x06 | 0;
|
|
48261
|
-
x14 =
|
|
49564
|
+
x14 = rotl2(x14 ^ x02, 8);
|
|
48262
49565
|
x10 = x10 + x14 | 0;
|
|
48263
|
-
x06 =
|
|
49566
|
+
x06 = rotl2(x06 ^ x10, 7);
|
|
48264
49567
|
x03 = x03 + x07 | 0;
|
|
48265
|
-
x15 =
|
|
49568
|
+
x15 = rotl2(x15 ^ x03, 16);
|
|
48266
49569
|
x11 = x11 + x15 | 0;
|
|
48267
|
-
x07 =
|
|
49570
|
+
x07 = rotl2(x07 ^ x11, 12);
|
|
48268
49571
|
x03 = x03 + x07 | 0;
|
|
48269
|
-
x15 =
|
|
49572
|
+
x15 = rotl2(x15 ^ x03, 8);
|
|
48270
49573
|
x11 = x11 + x15 | 0;
|
|
48271
|
-
x07 =
|
|
49574
|
+
x07 = rotl2(x07 ^ x11, 7);
|
|
48272
49575
|
x00 = x00 + x05 | 0;
|
|
48273
|
-
x15 =
|
|
49576
|
+
x15 = rotl2(x15 ^ x00, 16);
|
|
48274
49577
|
x10 = x10 + x15 | 0;
|
|
48275
|
-
x05 =
|
|
49578
|
+
x05 = rotl2(x05 ^ x10, 12);
|
|
48276
49579
|
x00 = x00 + x05 | 0;
|
|
48277
|
-
x15 =
|
|
49580
|
+
x15 = rotl2(x15 ^ x00, 8);
|
|
48278
49581
|
x10 = x10 + x15 | 0;
|
|
48279
|
-
x05 =
|
|
49582
|
+
x05 = rotl2(x05 ^ x10, 7);
|
|
48280
49583
|
x01 = x01 + x06 | 0;
|
|
48281
|
-
x12 =
|
|
49584
|
+
x12 = rotl2(x12 ^ x01, 16);
|
|
48282
49585
|
x11 = x11 + x12 | 0;
|
|
48283
|
-
x06 =
|
|
49586
|
+
x06 = rotl2(x06 ^ x11, 12);
|
|
48284
49587
|
x01 = x01 + x06 | 0;
|
|
48285
|
-
x12 =
|
|
49588
|
+
x12 = rotl2(x12 ^ x01, 8);
|
|
48286
49589
|
x11 = x11 + x12 | 0;
|
|
48287
|
-
x06 =
|
|
49590
|
+
x06 = rotl2(x06 ^ x11, 7);
|
|
48288
49591
|
x02 = x02 + x07 | 0;
|
|
48289
|
-
x13 =
|
|
49592
|
+
x13 = rotl2(x13 ^ x02, 16);
|
|
48290
49593
|
x08 = x08 + x13 | 0;
|
|
48291
|
-
x07 =
|
|
49594
|
+
x07 = rotl2(x07 ^ x08, 12);
|
|
48292
49595
|
x02 = x02 + x07 | 0;
|
|
48293
|
-
x13 =
|
|
49596
|
+
x13 = rotl2(x13 ^ x02, 8);
|
|
48294
49597
|
x08 = x08 + x13 | 0;
|
|
48295
|
-
x07 =
|
|
49598
|
+
x07 = rotl2(x07 ^ x08, 7);
|
|
48296
49599
|
x03 = x03 + x04 | 0;
|
|
48297
|
-
x14 =
|
|
49600
|
+
x14 = rotl2(x14 ^ x03, 16);
|
|
48298
49601
|
x09 = x09 + x14 | 0;
|
|
48299
|
-
x04 =
|
|
49602
|
+
x04 = rotl2(x04 ^ x09, 12);
|
|
48300
49603
|
x03 = x03 + x04 | 0;
|
|
48301
|
-
x14 =
|
|
49604
|
+
x14 = rotl2(x14 ^ x03, 8);
|
|
48302
49605
|
x09 = x09 + x14 | 0;
|
|
48303
|
-
x04 =
|
|
49606
|
+
x04 = rotl2(x04 ^ x09, 7);
|
|
48304
49607
|
}
|
|
48305
49608
|
let oi = 0;
|
|
48306
49609
|
out[oi++] = y00 + x00 | 0;
|
|
@@ -48573,7 +49876,7 @@ function i2Osp(n, w) {
|
|
|
48573
49876
|
}
|
|
48574
49877
|
return ret;
|
|
48575
49878
|
}
|
|
48576
|
-
function
|
|
49879
|
+
function concat4(a, b) {
|
|
48577
49880
|
const ret = new Uint8Array(a.length + b.length);
|
|
48578
49881
|
ret.set(a, 0);
|
|
48579
49882
|
ret.set(b, a.length);
|
|
@@ -48718,11 +50021,11 @@ class Dhkem {
|
|
|
48718
50021
|
const sks = isCryptoKeyPair(params.senderKey) ? params.senderKey.privateKey : params.senderKey;
|
|
48719
50022
|
const dh1 = new Uint8Array(await this._prim.dh(ke.privateKey, params.recipientPublicKey));
|
|
48720
50023
|
const dh2 = new Uint8Array(await this._prim.dh(sks, params.recipientPublicKey));
|
|
48721
|
-
dh =
|
|
50024
|
+
dh = concat4(dh1, dh2);
|
|
48722
50025
|
}
|
|
48723
50026
|
let kemContext;
|
|
48724
50027
|
if (params.senderKey === undefined) {
|
|
48725
|
-
kemContext =
|
|
50028
|
+
kemContext = concat4(new Uint8Array(enc), new Uint8Array(pkrm));
|
|
48726
50029
|
} else {
|
|
48727
50030
|
const pks = isCryptoKeyPair(params.senderKey) ? params.senderKey.publicKey : await this._prim.derivePublicKey(params.senderKey);
|
|
48728
50031
|
const pksm = await this._prim.serializePublicKey(pks);
|
|
@@ -48749,11 +50052,11 @@ class Dhkem {
|
|
|
48749
50052
|
} else {
|
|
48750
50053
|
const dh1 = new Uint8Array(await this._prim.dh(skr, pke));
|
|
48751
50054
|
const dh2 = new Uint8Array(await this._prim.dh(skr, params.senderPublicKey));
|
|
48752
|
-
dh =
|
|
50055
|
+
dh = concat4(dh1, dh2);
|
|
48753
50056
|
}
|
|
48754
50057
|
let kemContext;
|
|
48755
50058
|
if (params.senderPublicKey === undefined) {
|
|
48756
|
-
kemContext =
|
|
50059
|
+
kemContext = concat4(new Uint8Array(params.enc), new Uint8Array(pkrm));
|
|
48757
50060
|
} else {
|
|
48758
50061
|
const pksm = await this._prim.serializePublicKey(params.senderPublicKey);
|
|
48759
50062
|
kemContext = new Uint8Array(params.enc.byteLength + pkrm.byteLength + pksm.byteLength);
|
|
@@ -50194,241 +51497,9 @@ async function runImportFlow(params) {
|
|
|
50194
51497
|
|
|
50195
51498
|
// src/wallet-keygen.ts
|
|
50196
51499
|
import { generateKeyPairSync } from "node:crypto";
|
|
50197
|
-
|
|
50198
|
-
// ../../node_modules/@noble/curves/esm/secp256k1.js
|
|
50199
|
-
init_sha2();
|
|
50200
|
-
init__shortw_utils();
|
|
50201
|
-
init_modular();
|
|
50202
|
-
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
50203
|
-
var secp256k1_CURVE = {
|
|
50204
|
-
p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),
|
|
50205
|
-
n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),
|
|
50206
|
-
h: BigInt(1),
|
|
50207
|
-
a: BigInt(0),
|
|
50208
|
-
b: BigInt(7),
|
|
50209
|
-
Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),
|
|
50210
|
-
Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")
|
|
50211
|
-
};
|
|
50212
|
-
var secp256k1_ENDO = {
|
|
50213
|
-
beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),
|
|
50214
|
-
basises: [
|
|
50215
|
-
[BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],
|
|
50216
|
-
[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")]
|
|
50217
|
-
]
|
|
50218
|
-
};
|
|
50219
|
-
var _2n5 = /* @__PURE__ */ BigInt(2);
|
|
50220
|
-
function sqrtMod(y) {
|
|
50221
|
-
const P2 = secp256k1_CURVE.p;
|
|
50222
|
-
const _3n4 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
|
|
50223
|
-
const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
|
|
50224
|
-
const b2 = y * y * y % P2;
|
|
50225
|
-
const b3 = b2 * b2 * y % P2;
|
|
50226
|
-
const b6 = pow2(b3, _3n4, P2) * b3 % P2;
|
|
50227
|
-
const b9 = pow2(b6, _3n4, P2) * b3 % P2;
|
|
50228
|
-
const b11 = pow2(b9, _2n5, P2) * b2 % P2;
|
|
50229
|
-
const b22 = pow2(b11, _11n, P2) * b11 % P2;
|
|
50230
|
-
const b44 = pow2(b22, _22n, P2) * b22 % P2;
|
|
50231
|
-
const b88 = pow2(b44, _44n, P2) * b44 % P2;
|
|
50232
|
-
const b176 = pow2(b88, _88n, P2) * b88 % P2;
|
|
50233
|
-
const b220 = pow2(b176, _44n, P2) * b44 % P2;
|
|
50234
|
-
const b223 = pow2(b220, _3n4, P2) * b3 % P2;
|
|
50235
|
-
const t1 = pow2(b223, _23n, P2) * b22 % P2;
|
|
50236
|
-
const t2 = pow2(t1, _6n, P2) * b2 % P2;
|
|
50237
|
-
const root = pow2(t2, _2n5, P2);
|
|
50238
|
-
if (!Fpk1.eql(Fpk1.sqr(root), y))
|
|
50239
|
-
throw new Error("Cannot find square root");
|
|
50240
|
-
return root;
|
|
50241
|
-
}
|
|
50242
|
-
var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod });
|
|
50243
|
-
var secp256k1 = createCurve({ ...secp256k1_CURVE, Fp: Fpk1, lowS: true, endo: secp256k1_ENDO }, sha256);
|
|
50244
|
-
|
|
50245
|
-
// ../../node_modules/@noble/hashes/esm/sha3.js
|
|
50246
|
-
init__u64();
|
|
50247
|
-
init_utils();
|
|
50248
|
-
var _0n7 = BigInt(0);
|
|
50249
|
-
var _1n7 = BigInt(1);
|
|
50250
|
-
var _2n6 = BigInt(2);
|
|
50251
|
-
var _7n2 = BigInt(7);
|
|
50252
|
-
var _256n = BigInt(256);
|
|
50253
|
-
var _0x71n = BigInt(113);
|
|
50254
|
-
var SHA3_PI = [];
|
|
50255
|
-
var SHA3_ROTL = [];
|
|
50256
|
-
var _SHA3_IOTA = [];
|
|
50257
|
-
for (let round = 0, R = _1n7, x = 1, y = 0;round < 24; round++) {
|
|
50258
|
-
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
50259
|
-
SHA3_PI.push(2 * (5 * y + x));
|
|
50260
|
-
SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
|
|
50261
|
-
let t = _0n7;
|
|
50262
|
-
for (let j = 0;j < 7; j++) {
|
|
50263
|
-
R = (R << _1n7 ^ (R >> _7n2) * _0x71n) % _256n;
|
|
50264
|
-
if (R & _2n6)
|
|
50265
|
-
t ^= _1n7 << (_1n7 << /* @__PURE__ */ BigInt(j)) - _1n7;
|
|
50266
|
-
}
|
|
50267
|
-
_SHA3_IOTA.push(t);
|
|
50268
|
-
}
|
|
50269
|
-
var IOTAS = split(_SHA3_IOTA, true);
|
|
50270
|
-
var SHA3_IOTA_H = IOTAS[0];
|
|
50271
|
-
var SHA3_IOTA_L = IOTAS[1];
|
|
50272
|
-
var rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
|
|
50273
|
-
var rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
|
|
50274
|
-
function keccakP(s, rounds = 24) {
|
|
50275
|
-
const B = new Uint32Array(5 * 2);
|
|
50276
|
-
for (let round = 24 - rounds;round < 24; round++) {
|
|
50277
|
-
for (let x = 0;x < 10; x++)
|
|
50278
|
-
B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
|
|
50279
|
-
for (let x = 0;x < 10; x += 2) {
|
|
50280
|
-
const idx1 = (x + 8) % 10;
|
|
50281
|
-
const idx0 = (x + 2) % 10;
|
|
50282
|
-
const B0 = B[idx0];
|
|
50283
|
-
const B1 = B[idx0 + 1];
|
|
50284
|
-
const Th = rotlH(B0, B1, 1) ^ B[idx1];
|
|
50285
|
-
const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
|
|
50286
|
-
for (let y = 0;y < 50; y += 10) {
|
|
50287
|
-
s[x + y] ^= Th;
|
|
50288
|
-
s[x + y + 1] ^= Tl;
|
|
50289
|
-
}
|
|
50290
|
-
}
|
|
50291
|
-
let curH = s[2];
|
|
50292
|
-
let curL = s[3];
|
|
50293
|
-
for (let t = 0;t < 24; t++) {
|
|
50294
|
-
const shift = SHA3_ROTL[t];
|
|
50295
|
-
const Th = rotlH(curH, curL, shift);
|
|
50296
|
-
const Tl = rotlL(curH, curL, shift);
|
|
50297
|
-
const PI = SHA3_PI[t];
|
|
50298
|
-
curH = s[PI];
|
|
50299
|
-
curL = s[PI + 1];
|
|
50300
|
-
s[PI] = Th;
|
|
50301
|
-
s[PI + 1] = Tl;
|
|
50302
|
-
}
|
|
50303
|
-
for (let y = 0;y < 50; y += 10) {
|
|
50304
|
-
for (let x = 0;x < 10; x++)
|
|
50305
|
-
B[x] = s[y + x];
|
|
50306
|
-
for (let x = 0;x < 10; x++)
|
|
50307
|
-
s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
|
|
50308
|
-
}
|
|
50309
|
-
s[0] ^= SHA3_IOTA_H[round];
|
|
50310
|
-
s[1] ^= SHA3_IOTA_L[round];
|
|
50311
|
-
}
|
|
50312
|
-
clean(B);
|
|
50313
|
-
}
|
|
50314
|
-
|
|
50315
|
-
class Keccak extends Hash {
|
|
50316
|
-
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
|
|
50317
|
-
super();
|
|
50318
|
-
this.pos = 0;
|
|
50319
|
-
this.posOut = 0;
|
|
50320
|
-
this.finished = false;
|
|
50321
|
-
this.destroyed = false;
|
|
50322
|
-
this.enableXOF = false;
|
|
50323
|
-
this.blockLen = blockLen;
|
|
50324
|
-
this.suffix = suffix;
|
|
50325
|
-
this.outputLen = outputLen;
|
|
50326
|
-
this.enableXOF = enableXOF;
|
|
50327
|
-
this.rounds = rounds;
|
|
50328
|
-
anumber(outputLen);
|
|
50329
|
-
if (!(0 < blockLen && blockLen < 200))
|
|
50330
|
-
throw new Error("only keccak-f1600 function is supported");
|
|
50331
|
-
this.state = new Uint8Array(200);
|
|
50332
|
-
this.state32 = u32(this.state);
|
|
50333
|
-
}
|
|
50334
|
-
clone() {
|
|
50335
|
-
return this._cloneInto();
|
|
50336
|
-
}
|
|
50337
|
-
keccak() {
|
|
50338
|
-
swap32IfBE(this.state32);
|
|
50339
|
-
keccakP(this.state32, this.rounds);
|
|
50340
|
-
swap32IfBE(this.state32);
|
|
50341
|
-
this.posOut = 0;
|
|
50342
|
-
this.pos = 0;
|
|
50343
|
-
}
|
|
50344
|
-
update(data) {
|
|
50345
|
-
aexists(this);
|
|
50346
|
-
data = toBytes(data);
|
|
50347
|
-
abytes(data);
|
|
50348
|
-
const { blockLen, state } = this;
|
|
50349
|
-
const len = data.length;
|
|
50350
|
-
for (let pos = 0;pos < len; ) {
|
|
50351
|
-
const take2 = Math.min(blockLen - this.pos, len - pos);
|
|
50352
|
-
for (let i = 0;i < take2; i++)
|
|
50353
|
-
state[this.pos++] ^= data[pos++];
|
|
50354
|
-
if (this.pos === blockLen)
|
|
50355
|
-
this.keccak();
|
|
50356
|
-
}
|
|
50357
|
-
return this;
|
|
50358
|
-
}
|
|
50359
|
-
finish() {
|
|
50360
|
-
if (this.finished)
|
|
50361
|
-
return;
|
|
50362
|
-
this.finished = true;
|
|
50363
|
-
const { state, suffix, pos, blockLen } = this;
|
|
50364
|
-
state[pos] ^= suffix;
|
|
50365
|
-
if ((suffix & 128) !== 0 && pos === blockLen - 1)
|
|
50366
|
-
this.keccak();
|
|
50367
|
-
state[blockLen - 1] ^= 128;
|
|
50368
|
-
this.keccak();
|
|
50369
|
-
}
|
|
50370
|
-
writeInto(out) {
|
|
50371
|
-
aexists(this, false);
|
|
50372
|
-
abytes(out);
|
|
50373
|
-
this.finish();
|
|
50374
|
-
const bufferOut = this.state;
|
|
50375
|
-
const { blockLen } = this;
|
|
50376
|
-
for (let pos = 0, len = out.length;pos < len; ) {
|
|
50377
|
-
if (this.posOut >= blockLen)
|
|
50378
|
-
this.keccak();
|
|
50379
|
-
const take2 = Math.min(blockLen - this.posOut, len - pos);
|
|
50380
|
-
out.set(bufferOut.subarray(this.posOut, this.posOut + take2), pos);
|
|
50381
|
-
this.posOut += take2;
|
|
50382
|
-
pos += take2;
|
|
50383
|
-
}
|
|
50384
|
-
return out;
|
|
50385
|
-
}
|
|
50386
|
-
xofInto(out) {
|
|
50387
|
-
if (!this.enableXOF)
|
|
50388
|
-
throw new Error("XOF is not possible for this instance");
|
|
50389
|
-
return this.writeInto(out);
|
|
50390
|
-
}
|
|
50391
|
-
xof(bytes) {
|
|
50392
|
-
anumber(bytes);
|
|
50393
|
-
return this.xofInto(new Uint8Array(bytes));
|
|
50394
|
-
}
|
|
50395
|
-
digestInto(out) {
|
|
50396
|
-
aoutput(out, this);
|
|
50397
|
-
if (this.finished)
|
|
50398
|
-
throw new Error("digest() was already called");
|
|
50399
|
-
this.writeInto(out);
|
|
50400
|
-
this.destroy();
|
|
50401
|
-
return out;
|
|
50402
|
-
}
|
|
50403
|
-
digest() {
|
|
50404
|
-
return this.digestInto(new Uint8Array(this.outputLen));
|
|
50405
|
-
}
|
|
50406
|
-
destroy() {
|
|
50407
|
-
this.destroyed = true;
|
|
50408
|
-
clean(this.state);
|
|
50409
|
-
}
|
|
50410
|
-
_cloneInto(to) {
|
|
50411
|
-
const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
|
|
50412
|
-
to || (to = new Keccak(blockLen, suffix, outputLen, enableXOF, rounds));
|
|
50413
|
-
to.state32.set(this.state32);
|
|
50414
|
-
to.pos = this.pos;
|
|
50415
|
-
to.posOut = this.posOut;
|
|
50416
|
-
to.finished = this.finished;
|
|
50417
|
-
to.rounds = rounds;
|
|
50418
|
-
to.suffix = suffix;
|
|
50419
|
-
to.outputLen = outputLen;
|
|
50420
|
-
to.enableXOF = enableXOF;
|
|
50421
|
-
to.destroyed = this.destroyed;
|
|
50422
|
-
return to;
|
|
50423
|
-
}
|
|
50424
|
-
}
|
|
50425
|
-
var gen = (suffix, blockLen, outputLen) => createHasher(() => new Keccak(blockLen, suffix, outputLen));
|
|
50426
|
-
var keccak_256 = /* @__PURE__ */ (() => gen(1, 136, 256 / 8))();
|
|
50427
|
-
|
|
50428
|
-
// src/wallet-keygen.ts
|
|
50429
51500
|
init_esm();
|
|
50430
51501
|
var hex3 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
50431
|
-
function
|
|
51502
|
+
function toChecksumAddress2(lowercaseBody) {
|
|
50432
51503
|
const digest = hex3(keccak_256(new TextEncoder().encode(lowercaseBody)));
|
|
50433
51504
|
let out = "0x";
|
|
50434
51505
|
for (let i = 0;i < lowercaseBody.length; i++) {
|
|
@@ -50449,7 +51520,7 @@ function generateSolana() {
|
|
|
50449
51520
|
function generateEvm() {
|
|
50450
51521
|
const priv = secp256k1.utils.randomPrivateKey();
|
|
50451
51522
|
const pub = secp256k1.getPublicKey(priv, false).slice(1);
|
|
50452
|
-
return { address:
|
|
51523
|
+
return { address: toChecksumAddress2(hex3(keccak_256(pub)).slice(-40)), privateKey: `0x${hex3(priv)}` };
|
|
50453
51524
|
}
|
|
50454
51525
|
function generateWallet(chain2) {
|
|
50455
51526
|
return chain2 === "solana" ? generateSolana() : generateEvm();
|
|
@@ -50465,6 +51536,11 @@ init_deps();
|
|
|
50465
51536
|
init_profiles();
|
|
50466
51537
|
init_render();
|
|
50467
51538
|
init_secret_store();
|
|
51539
|
+
function trustedCell(row) {
|
|
51540
|
+
if (row.revokedAt)
|
|
51541
|
+
return "-";
|
|
51542
|
+
return row.addedVia === "session" || typeof row.trustedAt === "number" ? "yes" : "no";
|
|
51543
|
+
}
|
|
50468
51544
|
var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
|
|
50469
51545
|
function isTeeRow(row) {
|
|
50470
51546
|
return row.profile !== undefined && TEE_PROFILES.has(row.profile);
|
|
@@ -50515,6 +51591,9 @@ var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade
|
|
|
50515
51591
|
var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
|
|
50516
51592
|
` + `This account's embedded wallet, shown above, can pay for a token trade too.
|
|
50517
51593
|
`;
|
|
51594
|
+
var UNTRUSTED_HINT = `A wallet marked Trusted no was linked by an API key: agents can move funds into it only once you mark it.
|
|
51595
|
+
` + `Run: candle wallets trust <label|address|prefix*> (the device token, never an API key)
|
|
51596
|
+
`;
|
|
50518
51597
|
var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
|
|
50519
51598
|
`;
|
|
50520
51599
|
async function probeSignerStates(rows, store) {
|
|
@@ -50621,24 +51700,28 @@ Linked wallets (${linkedRows.length}):
|
|
|
50621
51700
|
`);
|
|
50622
51701
|
} else {
|
|
50623
51702
|
const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
|
|
50624
|
-
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
|
|
51703
|
+
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Trusted", "Signer"], linkedRows.map((wallet, index) => [
|
|
50625
51704
|
wallet._id,
|
|
50626
51705
|
wallet.chain,
|
|
50627
51706
|
wallet.address,
|
|
50628
51707
|
wallet.label ?? "-",
|
|
50629
51708
|
isTeeRow(wallet) ? "tee" : "linked",
|
|
50630
51709
|
wallet.revokedAt ? "yes" : "no",
|
|
51710
|
+
trustedCell(wallet),
|
|
50631
51711
|
cells[index] ?? "-"
|
|
50632
51712
|
]))}
|
|
50633
51713
|
`);
|
|
50634
51714
|
const anyNone = cells.includes("none");
|
|
50635
51715
|
const anyStale = cells.includes("stale");
|
|
50636
51716
|
const anyTee = linkedRows.some(isTeeRow);
|
|
50637
|
-
|
|
51717
|
+
const anyUntrusted = linkedRows.some((row) => trustedCell(row) === "no");
|
|
51718
|
+
if (anyNone || anyStale || anyTee || anyUntrusted)
|
|
50638
51719
|
deps.stdout.write(`
|
|
50639
51720
|
`);
|
|
50640
51721
|
if (anyTee)
|
|
50641
51722
|
deps.stdout.write(TEE_HINT);
|
|
51723
|
+
if (anyUntrusted)
|
|
51724
|
+
deps.stdout.write(UNTRUSTED_HINT);
|
|
50642
51725
|
if (anyNone)
|
|
50643
51726
|
deps.stdout.write(NONE_HINT);
|
|
50644
51727
|
if (anyStale)
|
|
@@ -51180,10 +52263,8 @@ async function addressOwnedByVault(ctx, address) {
|
|
|
51180
52263
|
writeUsageFailure(ctx.deps, error.message, ctx.json);
|
|
51181
52264
|
return "usage";
|
|
51182
52265
|
}
|
|
51183
|
-
|
|
51184
|
-
|
|
51185
|
-
message: error instanceof Error ? error.message : String(error)
|
|
51186
|
-
}, ctx.json);
|
|
52266
|
+
const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
|
|
52267
|
+
writeLocalFailure(ctx.deps, isVaultError2(error) ? { code: error.code, message: error.message, ...error.suggestion ? { suggestion: error.suggestion } : {} } : { code: "VAULT_UNLOCK_FAILED", message: error instanceof Error ? error.message : String(error) }, ctx.json);
|
|
51187
52268
|
return "error";
|
|
51188
52269
|
}
|
|
51189
52270
|
}
|
|
@@ -52834,6 +53915,150 @@ async function teeRebinds(args, ctx) {
|
|
|
52834
53915
|
return 0;
|
|
52835
53916
|
}
|
|
52836
53917
|
|
|
53918
|
+
// src/commands/transfer.ts
|
|
53919
|
+
init_args();
|
|
53920
|
+
init_render();
|
|
53921
|
+
init_trading();
|
|
53922
|
+
var USAGE2 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
|
|
53923
|
+
var TRANSFER_ASSETS = Object.keys(BASES);
|
|
53924
|
+
var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
53925
|
+
async function readLinkedWallets(ctx, key) {
|
|
53926
|
+
const rows = [];
|
|
53927
|
+
let cursor = null;
|
|
53928
|
+
const seen = new Set;
|
|
53929
|
+
for (let page = 0;page < 25; page++) {
|
|
53930
|
+
const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
|
|
53931
|
+
const body = await request(ctx, key, `/api/v1/agent/wallets${query}`);
|
|
53932
|
+
if (!Array.isArray(body.page))
|
|
53933
|
+
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
53934
|
+
rows.push(...body.page);
|
|
53935
|
+
if (body.isDone !== false || typeof body.continueCursor !== "string" || seen.has(body.continueCursor))
|
|
53936
|
+
return rows;
|
|
53937
|
+
seen.add(body.continueCursor);
|
|
53938
|
+
cursor = body.continueCursor;
|
|
53939
|
+
}
|
|
53940
|
+
return rows;
|
|
53941
|
+
}
|
|
53942
|
+
async function resolveDestination(ctx, key, source, to) {
|
|
53943
|
+
if (to === "vault") {
|
|
53944
|
+
const lifecycle = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/lifecycle`);
|
|
53945
|
+
const vault = lifecycle.vaultDestination;
|
|
53946
|
+
if (typeof vault !== "string" || vault.length === 0)
|
|
53947
|
+
throw new TradingError("VAULT_NOT_PINNED", "This wallet has no pinned vault recorded on Candle, so --to vault names nothing. Pass the address instead.");
|
|
53948
|
+
return { kind: "vault", address: vault };
|
|
53949
|
+
}
|
|
53950
|
+
const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === "solana" && row.revokedAt === undefined && row.address !== source.address);
|
|
53951
|
+
const matches = rows.filter((row) => row.label === to || row._id === to || row.address === to);
|
|
53952
|
+
if (matches.length > 1)
|
|
53953
|
+
throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches.length} linked wallets: ${matches.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
|
|
53954
|
+
const match = matches[0];
|
|
53955
|
+
if (match)
|
|
53956
|
+
return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
|
|
53957
|
+
if (to === source.address)
|
|
53958
|
+
throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
|
|
53959
|
+
if (BASE58_ADDRESS.test(to))
|
|
53960
|
+
return { kind: "address", address: to };
|
|
53961
|
+
throw new TradingError("DESTINATION_UNKNOWN", rows.length === 0 ? `"${to}" is not vault, a Solana address, or a linked wallet on this account (it has no other active Solana linked wallets).` : `"${to}" is not vault, a Solana address, or a linked wallet on this account. Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`);
|
|
53962
|
+
}
|
|
53963
|
+
function describeDestination(destination) {
|
|
53964
|
+
switch (destination.kind) {
|
|
53965
|
+
case "vault":
|
|
53966
|
+
return `this wallet's pinned vault ${destination.address}`;
|
|
53967
|
+
case "linked":
|
|
53968
|
+
return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
|
|
53969
|
+
case "address":
|
|
53970
|
+
return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
|
|
53971
|
+
}
|
|
53972
|
+
}
|
|
53973
|
+
async function confirmTransfer(ctx, lines, yes) {
|
|
53974
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
53975
|
+
for (const line of lines)
|
|
53976
|
+
output.write(`${safeText(line)}
|
|
53977
|
+
`);
|
|
53978
|
+
if (yes)
|
|
53979
|
+
return true;
|
|
53980
|
+
if (!ctx.deps.isTTY.stdin)
|
|
53981
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary transfer prompt.");
|
|
53982
|
+
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
53983
|
+
}
|
|
53984
|
+
async function transfer(args, ctx) {
|
|
53985
|
+
const parsed = parseArgs(args, {
|
|
53986
|
+
valueFlags: ["--wallet", "--to", "--asset", "--mint", "--amount", "--rpc-url"],
|
|
53987
|
+
booleanFlags: ["--yes"]
|
|
53988
|
+
});
|
|
53989
|
+
if ("error" in parsed) {
|
|
53990
|
+
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
53991
|
+
return 2;
|
|
53992
|
+
}
|
|
53993
|
+
const flags = parsed.values;
|
|
53994
|
+
const asset = flags["--asset"]?.toUpperCase();
|
|
53995
|
+
if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || Boolean(flags["--asset"]) === Boolean(flags["--mint"]) || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"])) {
|
|
53996
|
+
writeUsageFailure(ctx.deps, USAGE2, ctx.json);
|
|
53997
|
+
return 2;
|
|
53998
|
+
}
|
|
53999
|
+
const to = flags["--to"];
|
|
54000
|
+
const isMax = flags["--amount"] === "max";
|
|
54001
|
+
try {
|
|
54002
|
+
if (!isMax)
|
|
54003
|
+
rawAmount(flags["--amount"], 18);
|
|
54004
|
+
const key = await tradingKey(ctx);
|
|
54005
|
+
const payer = await tradingPayer(ctx, key, flags["--wallet"], "transfer:write");
|
|
54006
|
+
if (payer.kind === "embedded")
|
|
54007
|
+
throw new TradingError("PAYER_UNSUPPORTED", "This command moves a linked wallet this machine can sign for. The embedded wallet transfers through the agent transfer rail (MCP candle_transfer), not from here. Name a TEE wallet with --wallet.");
|
|
54008
|
+
if (!payer.scopes.includes("transfer:bound"))
|
|
54009
|
+
throw new TradingError("SCOPE_MISSING", "Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind");
|
|
54010
|
+
const wallet = payer.wallet;
|
|
54011
|
+
const destination = await resolveDestination(ctx, key, wallet, to);
|
|
54012
|
+
const label = asset ?? flags["--mint"];
|
|
54013
|
+
let amountRaw;
|
|
54014
|
+
if (isMax)
|
|
54015
|
+
amountRaw = "max";
|
|
54016
|
+
else {
|
|
54017
|
+
const decimals = asset ? BASES[asset]?.decimals ?? 9 : await decimalsFor(ctx, flags["--mint"], flags["--rpc-url"]);
|
|
54018
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
54019
|
+
}
|
|
54020
|
+
const amountText = isMax ? `the full spendable balance of ${label}` : `${flags["--amount"]} ${label}`;
|
|
54021
|
+
const confirmed = await confirmTransfer(ctx, [
|
|
54022
|
+
`Transfer ${amountText} from ${wallet.address} (${wallet.id}) to ${describeDestination(destination)}`,
|
|
54023
|
+
destination.kind === "vault" ? "Destination: this wallet's own vault, pinned when it was imported." : destination.kind === "linked" ? "Destination: a linked wallet on this account. Candle allows it only if you linked it while signed in or marked it trusted, and the amount counts against this key's spend caps." : "Destination: an address Candle will classify at build; from a TEE wallet only its vault or a trusted linked wallet is allowed."
|
|
54024
|
+
], parsed.booleans.has("--yes"));
|
|
54025
|
+
if (!confirmed)
|
|
54026
|
+
return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet.id, destination });
|
|
54027
|
+
const built = await request(ctx, key, "/api/v1/agent/transfer/build", {
|
|
54028
|
+
walletId: wallet.id,
|
|
54029
|
+
chain: "solana",
|
|
54030
|
+
...asset ? { asset } : { mint: flags["--mint"] },
|
|
54031
|
+
amountRaw,
|
|
54032
|
+
to: destination.address
|
|
54033
|
+
});
|
|
54034
|
+
const transferId = built.transferId;
|
|
54035
|
+
const unsigned = built.unsignedTransactionsBase64;
|
|
54036
|
+
if (typeof transferId !== "string" || !Array.isArray(unsigned) || unsigned.length !== 1 || typeof unsigned[0] !== "string")
|
|
54037
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return one unsigned transfer transaction; nothing was signed.");
|
|
54038
|
+
if (typeof built.payerAddress === "string" && built.payerAddress !== wallet.address)
|
|
54039
|
+
throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the requested payer; nothing was signed.");
|
|
54040
|
+
const builtAmount = typeof built.amountRaw === "string" ? built.amountRaw : amountRaw;
|
|
54041
|
+
ctx.deps.stderr.write(`Built transfer ${transferId}: ${asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`}${typeof built.destinationKind === "string" ? ` to ${built.destinationKind === "vault" ? "the vault" : "a linked wallet"}` : ""}.
|
|
54042
|
+
`);
|
|
54043
|
+
const signed = await relaySign(ctx, key, wallet, unsigned[0]);
|
|
54044
|
+
const result = await request(ctx, key, "/api/v1/agent/transfer/submit", {
|
|
54045
|
+
transferId,
|
|
54046
|
+
signedTransactionsBase64: [signed]
|
|
54047
|
+
});
|
|
54048
|
+
const receipt = {
|
|
54049
|
+
...result,
|
|
54050
|
+
transferId,
|
|
54051
|
+
walletId: wallet.id,
|
|
54052
|
+
wallet: safeText(wallet.address),
|
|
54053
|
+
destination,
|
|
54054
|
+
...typeof built.destinationKind === "string" ? { destinationKind: built.destinationKind } : {}
|
|
54055
|
+
};
|
|
54056
|
+
return printTradingResult(ctx, receipt);
|
|
54057
|
+
} catch (error) {
|
|
54058
|
+
return tradingFailure(ctx, error);
|
|
54059
|
+
}
|
|
54060
|
+
}
|
|
54061
|
+
|
|
52837
54062
|
// src/commands/update.ts
|
|
52838
54063
|
init_args();
|
|
52839
54064
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -53563,12 +54788,32 @@ async function verifyEntry(copy, entry, root, report, observer) {
|
|
|
53563
54788
|
if (addressFromSecret64(secret) !== entry.address) {
|
|
53564
54789
|
fail2(5, "its stored secret does not produce the address the index records", entry.id);
|
|
53565
54790
|
}
|
|
54791
|
+
} else if (!sameEvmAddress(evmAddressFromSecret(secret), entry.address)) {
|
|
54792
|
+
fail2(5, "its stored secret does not produce the address the index records", entry.id);
|
|
53566
54793
|
}
|
|
53567
54794
|
report.addressChecked.push(entry.id);
|
|
53568
54795
|
if (entry.origin !== "derived" || entry.derivation === undefined) {
|
|
53569
54796
|
report.notRederived.push(entry.id);
|
|
53570
54797
|
return;
|
|
53571
54798
|
}
|
|
54799
|
+
if (entry.derivation.scheme === "bip32-secp256k1") {
|
|
54800
|
+
const index = evmIndexOfPath(entry.derivation.path);
|
|
54801
|
+
if (index === undefined) {
|
|
54802
|
+
fail2(5, `its recorded path ${entry.derivation.path} is not on the EVM branch`, entry.id);
|
|
54803
|
+
}
|
|
54804
|
+
const derivedEvm = await deriveEvmKeyFromRoot(root, index);
|
|
54805
|
+
observer?.onLeafLive?.(2);
|
|
54806
|
+
try {
|
|
54807
|
+
if (!bytesEqual(derivedEvm.secret, secret)) {
|
|
54808
|
+
fail2(5, `it does not re-derive from the root along its recorded path ${entry.derivation.path}`, entry.id);
|
|
54809
|
+
}
|
|
54810
|
+
report.rederived.push(entry.id);
|
|
54811
|
+
} finally {
|
|
54812
|
+
wipe(derivedEvm.secret);
|
|
54813
|
+
observer?.onLeafLive?.(1);
|
|
54814
|
+
}
|
|
54815
|
+
return;
|
|
54816
|
+
}
|
|
53572
54817
|
if (entry.derivation.scheme !== "slip10-ed25519") {
|
|
53573
54818
|
report.notRederived.push(entry.id);
|
|
53574
54819
|
return;
|
|
@@ -54104,6 +55349,8 @@ async function vaultDemote(args, ctx) {
|
|
|
54104
55349
|
code: 1
|
|
54105
55350
|
}));
|
|
54106
55351
|
if (!resolved.ok) {
|
|
55352
|
+
if (resolved.reported)
|
|
55353
|
+
return resolved.code;
|
|
54107
55354
|
writeLocalFailure(ctx.deps, {
|
|
54108
55355
|
code: "TEE_WALLET_UNKNOWN",
|
|
54109
55356
|
message: `${address} is not a TEE wallet in the vault.`,
|
|
@@ -54406,10 +55653,15 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54406
55653
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54407
55654
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54408
55655
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54409
|
-
|
|
54410
|
-
passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
|
|
55656
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
54411
55657
|
});
|
|
54412
55658
|
const vault = hold(opened.vault);
|
|
55659
|
+
const openedWith = {
|
|
55660
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55661
|
+
envelopeId: opened.factor.envelopeId
|
|
55662
|
+
};
|
|
55663
|
+
if (opened.factor.kind === "security-key")
|
|
55664
|
+
opened.factor.session.pin = undefined;
|
|
54413
55665
|
const vaultId = vault.file.vaultId;
|
|
54414
55666
|
let registered;
|
|
54415
55667
|
try {
|
|
@@ -54507,7 +55759,8 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54507
55759
|
saltDerivation: "platform",
|
|
54508
55760
|
helper: envelope.helper,
|
|
54509
55761
|
recoverableFactors: recoverable,
|
|
54510
|
-
verified: true
|
|
55762
|
+
verified: true,
|
|
55763
|
+
openedWith
|
|
54511
55764
|
});
|
|
54512
55765
|
return 0;
|
|
54513
55766
|
}
|
|
@@ -54530,6 +55783,7 @@ ${PASSKEY_NOTE}
|
|
|
54530
55783
|
}
|
|
54531
55784
|
|
|
54532
55785
|
// src/commands/vault-factor-security-key.ts
|
|
55786
|
+
init_esm();
|
|
54533
55787
|
init_crypto();
|
|
54534
55788
|
init_errors();
|
|
54535
55789
|
init_fido2();
|
|
@@ -54653,20 +55907,84 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54653
55907
|
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
54654
55908
|
const file = parseVaultFile(raw);
|
|
54655
55909
|
const envelopeId = freshEnvelopeId();
|
|
54656
|
-
const
|
|
54657
|
-
|
|
54658
|
-
|
|
54659
|
-
|
|
54660
|
-
|
|
54661
|
-
|
|
55910
|
+
const vaultId = file.vaultId;
|
|
55911
|
+
const keysInHeader = file.envelopes.filter(isCtap2Envelope);
|
|
55912
|
+
const { helperPath, info } = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55913
|
+
assertHelperExcludeList(info, helperPath, keysInHeader.length);
|
|
55914
|
+
const attached = keysInHeader.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: keysInHeader }) : undefined;
|
|
55915
|
+
const excludeCountNow = keysInHeader.length;
|
|
55916
|
+
const addingLine = (device) => deps.stderr.write(`Adding: ${device.product || "security key"} (--device ${device.deviceId}), a different key from the one that opened the vault.
|
|
55917
|
+
`);
|
|
55918
|
+
let session;
|
|
55919
|
+
let enrolling;
|
|
55920
|
+
let deferredAfter;
|
|
54662
55921
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54663
55922
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54664
55923
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54665
|
-
|
|
54666
|
-
|
|
55924
|
+
among: factorAddAmong(file.envelopes),
|
|
55925
|
+
attached,
|
|
55926
|
+
onFactorChosen: async (chosen) => {
|
|
55927
|
+
if (chosen.kind === "passphrase") {
|
|
55928
|
+
session = await openSecurityKeySession(deps, {
|
|
55929
|
+
vaultId,
|
|
55930
|
+
envelopeId,
|
|
55931
|
+
deviceFlag: ctx.vaultDevice,
|
|
55932
|
+
requireFeatures: true,
|
|
55933
|
+
beforePin: (device) => {
|
|
55934
|
+
refuseIfEnrolled(device, attached);
|
|
55935
|
+
assertExcludeListFits(device, excludeCountNow);
|
|
55936
|
+
}
|
|
55937
|
+
});
|
|
55938
|
+
return;
|
|
55939
|
+
}
|
|
55940
|
+
if (chosen.kind !== "security-key")
|
|
55941
|
+
return;
|
|
55942
|
+
const plan = planDevices(ctx, chosen.envelope, info.devices, attached);
|
|
55943
|
+
if (plan.enrolling !== undefined) {
|
|
55944
|
+
refuseIfEnrolled(plan.enrolling, attached);
|
|
55945
|
+
assertDeviceServesFactor(plan.enrolling);
|
|
55946
|
+
assertExcludeListFits(plan.enrolling, excludeCountNow);
|
|
55947
|
+
enrolling = plan.enrolling;
|
|
55948
|
+
addingLine(plan.enrolling);
|
|
55949
|
+
} else {
|
|
55950
|
+
deferredAfter = plan.opening;
|
|
55951
|
+
}
|
|
55952
|
+
return { preferDevice: plan.opening.deviceId };
|
|
55953
|
+
}
|
|
54667
55954
|
});
|
|
54668
55955
|
const vault = hold(opened.vault);
|
|
54669
|
-
const
|
|
55956
|
+
const openedWith = {
|
|
55957
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55958
|
+
envelopeId: opened.factor.envelopeId
|
|
55959
|
+
};
|
|
55960
|
+
if (opened.factor.kind === "security-key")
|
|
55961
|
+
opened.factor.session.pin = undefined;
|
|
55962
|
+
const enrolledKeys = vault.file.envelopes.filter(isCtap2Envelope);
|
|
55963
|
+
const excludeCredentialIds = enrolledKeys.map((envelope2) => base64.encode(unb64u(envelope2.credentialId, "credentialId")));
|
|
55964
|
+
if (session === undefined) {
|
|
55965
|
+
if (deferredAfter !== undefined) {
|
|
55966
|
+
await deps.promptLine(`The vault is open. Unplug ${deferredAfter.product || "the security key that opened it"}, insert the security key to add, then press Enter.`);
|
|
55967
|
+
const again = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55968
|
+
const probeAgain = enrolledKeys.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: enrolledKeys }) : undefined;
|
|
55969
|
+
const inserted = chooseAfterInsert(ctx, again.info.devices, probeAgain);
|
|
55970
|
+
refuseIfEnrolled(inserted, probeAgain);
|
|
55971
|
+
assertDeviceServesFactor(inserted);
|
|
55972
|
+
assertExcludeListFits(inserted, excludeCredentialIds.length);
|
|
55973
|
+
addingLine(inserted);
|
|
55974
|
+
enrolling = inserted;
|
|
55975
|
+
}
|
|
55976
|
+
if (enrolling === undefined)
|
|
55977
|
+
throw new Error("factor add security-key: no enrolling device was decided");
|
|
55978
|
+
const target = enrolling;
|
|
55979
|
+
session = await openSecurityKeySession(deps, {
|
|
55980
|
+
vaultId,
|
|
55981
|
+
envelopeId,
|
|
55982
|
+
deviceFlag: target.deviceId,
|
|
55983
|
+
requireFeatures: true,
|
|
55984
|
+
pinPrompt: (device) => `PIN for the key being added, ${device.product || "the security key"} (input hidden): `
|
|
55985
|
+
});
|
|
55986
|
+
}
|
|
55987
|
+
const registered = await registerCredential(deps, session, { vaultId, envelopeId, excludeCredentialIds });
|
|
54670
55988
|
if (registered.backupEligible) {
|
|
54671
55989
|
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${session.device.product || "This authenticator"} reports the credential it created as backup-eligible (synced), so it is not a hardware-bound security key credential and cannot be recorded as a hardware-token factor.`, {
|
|
54672
55990
|
suggestion: "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Use a hardware security key for this factor."
|
|
@@ -54723,7 +56041,8 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54723
56041
|
backupState: envelope.backupState,
|
|
54724
56042
|
userVerification: "required",
|
|
54725
56043
|
recoverableFactors: recoverable,
|
|
54726
|
-
verified: true
|
|
56044
|
+
verified: true,
|
|
56045
|
+
openedWith
|
|
54727
56046
|
});
|
|
54728
56047
|
return 0;
|
|
54729
56048
|
}
|
|
@@ -54742,6 +56061,76 @@ ${SECURITY_KEY_PAIR_NOTE}
|
|
|
54742
56061
|
`);
|
|
54743
56062
|
return 0;
|
|
54744
56063
|
}
|
|
56064
|
+
function refuseIfEnrolled(device, attached) {
|
|
56065
|
+
const entry = attached?.devices?.find((candidate) => candidate.deviceId === device.deviceId);
|
|
56066
|
+
if (entry === undefined || attached?.credentials === undefined)
|
|
56067
|
+
return;
|
|
56068
|
+
for (const [envelopeId, credentialId] of attached.credentials) {
|
|
56069
|
+
if (entry.present.includes(credentialId))
|
|
56070
|
+
throw alreadyEnrolled(device, envelopeId);
|
|
56071
|
+
}
|
|
56072
|
+
}
|
|
56073
|
+
function planDevices(ctx, opener, devices, attached) {
|
|
56074
|
+
const platform = ctx.deps.platform;
|
|
56075
|
+
if (devices.length === 0) {
|
|
56076
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key is attached.", {
|
|
56077
|
+
suggestion: "Plug the key in and run the command again. No other factor is substituted."
|
|
56078
|
+
});
|
|
56079
|
+
}
|
|
56080
|
+
const named = ctx.vaultDevice;
|
|
56081
|
+
const openerCredential = attached?.credentials?.get(opener.id);
|
|
56082
|
+
const holders = openerCredential === undefined ? [] : (attached?.devices ?? []).filter((device) => device.present.includes(openerCredential));
|
|
56083
|
+
let opening;
|
|
56084
|
+
if (holders.length === 1) {
|
|
56085
|
+
opening = devices.find((device) => device.deviceId === holders[0].deviceId);
|
|
56086
|
+
}
|
|
56087
|
+
if (opening === undefined) {
|
|
56088
|
+
const candidates = devices.filter((device) => device.deviceId !== named);
|
|
56089
|
+
if (candidates.length === 1)
|
|
56090
|
+
opening = candidates[0];
|
|
56091
|
+
else if (devices.length === 1)
|
|
56092
|
+
opening = devices[0];
|
|
56093
|
+
}
|
|
56094
|
+
if (opening === undefined) {
|
|
56095
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${devices.length} security keys are attached and the one that opens the vault could not be told apart. Leave only the key that opens the vault attached; this command asks for the key to add after the vault is open.`, { suggestion: "Nothing was sent to any key. Run again with only the key that opens the vault attached." });
|
|
56096
|
+
}
|
|
56097
|
+
if (!opening.readable)
|
|
56098
|
+
selectDevice([opening], undefined, platform);
|
|
56099
|
+
const others = devices.filter((device) => device.deviceId !== opening.deviceId);
|
|
56100
|
+
if (named !== undefined) {
|
|
56101
|
+
if (named === opening.deviceId)
|
|
56102
|
+
throw alreadyEnrolled(opening, opener.id);
|
|
56103
|
+
return { opening, enrolling: selectDevice(others, named, platform) };
|
|
56104
|
+
}
|
|
56105
|
+
if (others.length === 1)
|
|
56106
|
+
return { opening, enrolling: selectDevice(others, undefined, platform) };
|
|
56107
|
+
if (others.length > 1) {
|
|
56108
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
56109
|
+
`);
|
|
56110
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opens the vault, and none was named, so nothing was sent to any of them:
|
|
56111
|
+
${listing}`, { suggestion: "Run again with --device <id> naming the key to add." });
|
|
56112
|
+
}
|
|
56113
|
+
return { opening, enrolling: undefined };
|
|
56114
|
+
}
|
|
56115
|
+
function chooseAfterInsert(ctx, devices, probe) {
|
|
56116
|
+
const platform = ctx.deps.platform;
|
|
56117
|
+
if (devices.length === 0) {
|
|
56118
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key other than the one that opened the vault is attached. Nothing was written.", { suggestion: "Run the command again with the key to add inserted when asked." });
|
|
56119
|
+
}
|
|
56120
|
+
if (devices.length === 1)
|
|
56121
|
+
return selectDevice(devices, undefined, platform);
|
|
56122
|
+
const holdsVault = (device) => (probe?.devices?.find((entry) => entry.deviceId === device.deviceId)?.present.length ?? 0) > 0;
|
|
56123
|
+
const others = devices.filter((device) => !holdsVault(device));
|
|
56124
|
+
if (others.length === 1)
|
|
56125
|
+
return selectDevice(others, undefined, platform);
|
|
56126
|
+
if (others.length === 0) {
|
|
56127
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "Every attached security key already holds one of this vault's credentials, so there is no key to add. Nothing was written.", { suggestion: "Run the command again with a security key this vault does not have inserted when asked." });
|
|
56128
|
+
}
|
|
56129
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
56130
|
+
`);
|
|
56131
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opened the vault, so nothing was sent to any of them:
|
|
56132
|
+
${listing}`, { suggestion: "Run the command again with only the key to add inserted when asked. Nothing was written." });
|
|
56133
|
+
}
|
|
54745
56134
|
|
|
54746
56135
|
// src/commands/vault-factor-touch-id.ts
|
|
54747
56136
|
init_enclave();
|
|
@@ -54780,10 +56169,15 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54780
56169
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54781
56170
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54782
56171
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54783
|
-
|
|
54784
|
-
passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
|
|
56172
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
54785
56173
|
});
|
|
54786
56174
|
const vault = hold(opened.vault);
|
|
56175
|
+
const openedWith = {
|
|
56176
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
56177
|
+
envelopeId: opened.factor.envelopeId
|
|
56178
|
+
};
|
|
56179
|
+
if (opened.factor.kind === "security-key")
|
|
56180
|
+
opened.factor.session.pin = undefined;
|
|
54787
56181
|
const vaultId = vault.file.vaultId;
|
|
54788
56182
|
const keyTag = keyTagFor(vaultId, envelopeId);
|
|
54789
56183
|
let created;
|
|
@@ -54867,7 +56261,8 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54867
56261
|
helper: { teamId: session.identity.teamId, bundleId: session.identity.bundleId, minVersion: session.version },
|
|
54868
56262
|
accessControl: "biometryCurrentSet",
|
|
54869
56263
|
recoverableFactors: recoverable,
|
|
54870
|
-
verified: true
|
|
56264
|
+
verified: true,
|
|
56265
|
+
openedWith
|
|
54871
56266
|
});
|
|
54872
56267
|
return 0;
|
|
54873
56268
|
}
|
|
@@ -55556,6 +56951,7 @@ async function reconcileFundingReceipts(vault, addresses, rpcUrl2, ctx) {
|
|
|
55556
56951
|
}
|
|
55557
56952
|
|
|
55558
56953
|
// src/commands/vault-fund.ts
|
|
56954
|
+
init_promote_support();
|
|
55559
56955
|
init_store();
|
|
55560
56956
|
|
|
55561
56957
|
// src/vault/vault-transfer-sign.ts
|
|
@@ -55773,10 +57169,11 @@ async function signAndBroadcastTransfer(input) {
|
|
|
55773
57169
|
// src/commands/vault-fund.ts
|
|
55774
57170
|
init_vault_support();
|
|
55775
57171
|
async function fundExternal(ctx, opened, vault, external2, input) {
|
|
55776
|
-
const vaultKeys = vault.index.entries.filter((entry) => entry.role === "vault");
|
|
57172
|
+
const vaultKeys = vault.index.entries.filter((entry) => entry.role === "vault" && entry.chain === "solana");
|
|
55777
57173
|
const fromFlag = input.parsed.values["--from"];
|
|
55778
57174
|
let fromEntry;
|
|
55779
57175
|
if (fromFlag !== undefined) {
|
|
57176
|
+
assertNotEvmEntry(vault.index, fromFlag, "vault fund");
|
|
55780
57177
|
fromEntry = vaultKeys.find((entry) => entry.label === fromFlag) ?? vaultKeys.find((entry) => entry.address === fromFlag);
|
|
55781
57178
|
if (fromEntry === undefined)
|
|
55782
57179
|
return usage(ctx, `No vault key matches --from ${fromFlag}.`);
|
|
@@ -55866,6 +57263,9 @@ async function vaultFund(args, ctx) {
|
|
|
55866
57263
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
55867
57264
|
});
|
|
55868
57265
|
const vault = hold(opened.vault);
|
|
57266
|
+
assertNotEvmEntry(vault.index, teeAddress, "vault fund");
|
|
57267
|
+
if (parsed.values["--from"] !== undefined)
|
|
57268
|
+
assertNotEvmEntry(vault.index, parsed.values["--from"], "vault fund");
|
|
55869
57269
|
const teeEntry = vault.index.entries.find((entry) => entry.address === teeAddress && entry.role === "tee-wallet");
|
|
55870
57270
|
if (teeEntry === undefined) {
|
|
55871
57271
|
const external2 = findExternalEntry(vault.index, teeAddress);
|
|
@@ -55897,7 +57297,7 @@ async function vaultFund(args, ctx) {
|
|
|
55897
57297
|
suggestion: "Nothing was signed. Name the source key with --from <label>, or pin one: candle tee enable <address> --vault <address>"
|
|
55898
57298
|
});
|
|
55899
57299
|
}
|
|
55900
|
-
const fromEntry = vault.index.entries.find((entry) => entry.address === destination && entry.role === "vault");
|
|
57300
|
+
const fromEntry = vault.index.entries.find((entry) => entry.address === destination && entry.role === "vault" && entry.chain === "solana");
|
|
55901
57301
|
if (fromEntry === undefined) {
|
|
55902
57302
|
throw new VaultError("GRANT_DESTINATION_UNRESOLVED", `Pinned destination ${destination} is not a vault key in this vault.`, {
|
|
55903
57303
|
suggestion: "Nothing was signed. Name the source key with --from <label>; candle vault status lists this vault's keys."
|
|
@@ -56354,9 +57754,48 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
|
|
|
56354
57754
|
}
|
|
56355
57755
|
return { lamports, unavailable, ...failure === undefined ? {} : { failure } };
|
|
56356
57756
|
}
|
|
57757
|
+
async function readEvmBalances(addresses, rpcUrl2, fetchFn) {
|
|
57758
|
+
const rpc2 = createEvmRpc(rpcUrl2, fetchFn);
|
|
57759
|
+
const read = {
|
|
57760
|
+
host: rpcHostOf(rpcUrl2),
|
|
57761
|
+
chainId: undefined,
|
|
57762
|
+
requests: 0,
|
|
57763
|
+
wei: new Map,
|
|
57764
|
+
usdg: new Map,
|
|
57765
|
+
unavailable: []
|
|
57766
|
+
};
|
|
57767
|
+
try {
|
|
57768
|
+
read.requests += 1;
|
|
57769
|
+
read.chainId = await rpc2.chainId();
|
|
57770
|
+
} catch (error) {
|
|
57771
|
+
read.unavailable.push(...addresses);
|
|
57772
|
+
read.failure = error instanceof Error ? error.message : String(error);
|
|
57773
|
+
return read;
|
|
57774
|
+
}
|
|
57775
|
+
const hood = read.chainId === BigInt(HOOD_CHAIN_ID);
|
|
57776
|
+
for (const address of addresses) {
|
|
57777
|
+
try {
|
|
57778
|
+
read.requests += 1;
|
|
57779
|
+
read.wei.set(address, await rpc2.getBalance(address));
|
|
57780
|
+
if (hood) {
|
|
57781
|
+
read.requests += 1;
|
|
57782
|
+
read.usdg.set(address, await rpc2.erc20BalanceOf(HOOD_USDG_ADDRESS, address));
|
|
57783
|
+
}
|
|
57784
|
+
} catch (error) {
|
|
57785
|
+
read.wei.delete(address);
|
|
57786
|
+
read.usdg.delete(address);
|
|
57787
|
+
read.unavailable.push(address);
|
|
57788
|
+
read.failure ??= error instanceof Error ? error.message : String(error);
|
|
57789
|
+
}
|
|
57790
|
+
}
|
|
57791
|
+
return read;
|
|
57792
|
+
}
|
|
57793
|
+
function evmRequestsPlanned(count) {
|
|
57794
|
+
return `${1 + count} to ${1 + 2 * count}`;
|
|
57795
|
+
}
|
|
56357
57796
|
async function vaultList(args, ctx) {
|
|
56358
57797
|
const parsed = parseArgs(args, {
|
|
56359
|
-
valueFlags: ["--keystore", "--rpc-url"],
|
|
57798
|
+
valueFlags: ["--keystore", "--rpc-url", "--evm-rpc-url"],
|
|
56360
57799
|
booleanFlags: ["--balances", "--accept-older-copy"],
|
|
56361
57800
|
pathFlags: ["--keystore"]
|
|
56362
57801
|
});
|
|
@@ -56372,6 +57811,9 @@ async function vaultList(args, ctx) {
|
|
|
56372
57811
|
if (!balances && parsed.values["--rpc-url"] !== undefined) {
|
|
56373
57812
|
return usage(ctx, "--rpc-url has no effect without --balances; vault list is offline by default.");
|
|
56374
57813
|
}
|
|
57814
|
+
if (!balances && parsed.values["--evm-rpc-url"] !== undefined) {
|
|
57815
|
+
return usage(ctx, "--evm-rpc-url has no effect without --balances; vault list is offline by default.");
|
|
57816
|
+
}
|
|
56375
57817
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
56376
57818
|
if ("error" in resolvedVault)
|
|
56377
57819
|
return usage(ctx, resolvedVault.error);
|
|
@@ -56383,6 +57825,10 @@ async function vaultList(args, ctx) {
|
|
|
56383
57825
|
return usage(ctx, resolved.error);
|
|
56384
57826
|
rpcUrl2 = resolved;
|
|
56385
57827
|
}
|
|
57828
|
+
const evmRpc = resolveEvmRpcUrl(parsed.values["--evm-rpc-url"], deps.env[EVM_RPC_URL_ENV], "--evm-rpc-url");
|
|
57829
|
+
if (balances && "error" in evmRpc)
|
|
57830
|
+
return usage(ctx, evmRpc.error);
|
|
57831
|
+
const evmRpcUrl = "url" in evmRpc ? evmRpc.url : DEFAULT_HOOD_RPC_URL;
|
|
56386
57832
|
if (!requirePromptStreams(ctx, "vault list"))
|
|
56387
57833
|
return 1;
|
|
56388
57834
|
return runVaultCommand(ctx, async ({ hold }) => {
|
|
@@ -56408,12 +57854,26 @@ async function vaultList(args, ctx) {
|
|
|
56408
57854
|
unavailable = outcome.unavailable;
|
|
56409
57855
|
failure = outcome.failure;
|
|
56410
57856
|
}
|
|
56411
|
-
const
|
|
57857
|
+
const solanaComplete = unavailable.length === 0;
|
|
56412
57858
|
const totalLamports = [...lamports.values()].reduce((sum, value) => sum + value, 0n);
|
|
56413
|
-
if (!
|
|
57859
|
+
if (!solanaComplete) {
|
|
56414
57860
|
deps.stderr.write(`${unavailable.length} ${unavailable.length === 1 ? "address" : "addresses"} could not be read: ${failure ?? "the RPC did not answer"}. Narrow with a filter, or use your own endpoint with --rpc-url.
|
|
56415
57861
|
`);
|
|
56416
57862
|
}
|
|
57863
|
+
const evm = balances ? matched.filter((entry) => entry.chain === "evm") : [];
|
|
57864
|
+
let evmRead;
|
|
57865
|
+
if (evm.length > 0) {
|
|
57866
|
+
const host = rpcHostOf(evmRpcUrl);
|
|
57867
|
+
deps.stderr.write(`Reading ETH (and USDG when the chain is Hood) for ${evm.length} EVM ${evm.length === 1 ? "address" : "addresses"} from ${host}, in ${evmRequestsPlanned(evm.length)} requests. That endpoint sees all ${evm.length} together.
|
|
57868
|
+
`);
|
|
57869
|
+
evmRead = await readEvmBalances(evm.map((entry) => entry.address), evmRpcUrl, deps.fetch);
|
|
57870
|
+
if (evmRead.unavailable.length > 0) {
|
|
57871
|
+
deps.stderr.write(`${evmRead.unavailable.length} EVM ${evmRead.unavailable.length === 1 ? "address" : "addresses"} could not be read: ${evmRead.failure ?? "the RPC did not answer"}. Narrow with a filter, or use your own endpoint with --evm-rpc-url.
|
|
57872
|
+
`);
|
|
57873
|
+
}
|
|
57874
|
+
}
|
|
57875
|
+
const hood = evmRead?.chainId === BigInt(HOOD_CHAIN_ID);
|
|
57876
|
+
const complete = solanaComplete && (evmRead === undefined || evmRead.unavailable.length === 0);
|
|
56417
57877
|
if (ctx.json) {
|
|
56418
57878
|
writeJson(deps, {
|
|
56419
57879
|
ok: true,
|
|
@@ -56425,17 +57885,30 @@ async function vaultList(args, ctx) {
|
|
|
56425
57885
|
const held = lamports.get(entry.address);
|
|
56426
57886
|
return {
|
|
56427
57887
|
...describeEntry(entry),
|
|
56428
|
-
...balances && entry.chain === "solana" ? { lamports: held === undefined ? null : held.toString() } : {}
|
|
57888
|
+
...balances && entry.chain === "solana" ? { lamports: held === undefined ? null : held.toString() } : {},
|
|
57889
|
+
...balances && entry.chain === "evm" ? {
|
|
57890
|
+
wei: evmRead?.wei.get(entry.address)?.toString() ?? null,
|
|
57891
|
+
...hood ? { usdgRaw: evmRead?.usdg.get(entry.address)?.toString() ?? null } : {}
|
|
57892
|
+
} : {}
|
|
56429
57893
|
};
|
|
56430
57894
|
}),
|
|
56431
57895
|
...balances && rpcHost !== undefined ? {
|
|
56432
57896
|
balances: {
|
|
56433
57897
|
rpcHost,
|
|
56434
57898
|
requests,
|
|
56435
|
-
complete,
|
|
57899
|
+
complete: solanaComplete,
|
|
56436
57900
|
totalLamports: totalLamports.toString(),
|
|
56437
57901
|
unavailable
|
|
56438
57902
|
}
|
|
57903
|
+
} : {},
|
|
57904
|
+
...evmRead !== undefined ? {
|
|
57905
|
+
evmBalances: {
|
|
57906
|
+
rpcHost: evmRead.host,
|
|
57907
|
+
chainId: evmRead.chainId === undefined ? null : Number(evmRead.chainId),
|
|
57908
|
+
requests: evmRead.requests,
|
|
57909
|
+
complete: evmRead.unavailable.length === 0,
|
|
57910
|
+
unavailable: evmRead.unavailable
|
|
57911
|
+
}
|
|
56439
57912
|
} : {}
|
|
56440
57913
|
});
|
|
56441
57914
|
return complete ? 0 : 3;
|
|
@@ -56451,24 +57924,50 @@ async function vaultList(args, ctx) {
|
|
|
56451
57924
|
const headers = ["ADDRESS", "LABEL", "ROLE", "DERIVATION"];
|
|
56452
57925
|
if (balances)
|
|
56453
57926
|
headers.push("SOL");
|
|
57927
|
+
const evmColumns = evmRead !== undefined;
|
|
57928
|
+
const nativeHeader = hood || evmRead?.chainId === undefined ? "ETH" : `ETH@${evmRead.chainId}`;
|
|
57929
|
+
if (evmColumns)
|
|
57930
|
+
headers.push(nativeHeader);
|
|
57931
|
+
if (evmColumns && hood)
|
|
57932
|
+
headers.push("USDG");
|
|
56454
57933
|
const rows = matched.map((entry) => {
|
|
56455
57934
|
const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
|
|
56456
57935
|
if (balances) {
|
|
56457
57936
|
const held = lamports.get(entry.address);
|
|
56458
57937
|
row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
|
|
56459
57938
|
}
|
|
57939
|
+
if (evmColumns) {
|
|
57940
|
+
const wei = evmRead?.wei.get(entry.address);
|
|
57941
|
+
row.push(entry.chain !== "evm" ? "-" : wei === undefined ? "?" : formatUnits(wei, NATIVE_DECIMALS));
|
|
57942
|
+
if (hood) {
|
|
57943
|
+
const usdg = evmRead?.usdg.get(entry.address);
|
|
57944
|
+
row.push(entry.chain !== "evm" ? "-" : usdg === undefined ? "?" : formatUnits(usdg, HOOD_USDG_DECIMALS));
|
|
57945
|
+
}
|
|
57946
|
+
}
|
|
56460
57947
|
return row;
|
|
56461
57948
|
});
|
|
56462
57949
|
deps.stdout.write(`
|
|
56463
57950
|
${renderTable(headers, rows)}
|
|
56464
57951
|
`);
|
|
56465
57952
|
if (balances) {
|
|
56466
|
-
deps.stdout.write(
|
|
57953
|
+
deps.stdout.write(solanaComplete ? `
|
|
56467
57954
|
total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
|
|
56468
57955
|
` : `
|
|
56469
57956
|
total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
|
|
56470
57957
|
`);
|
|
56471
57958
|
}
|
|
57959
|
+
if (evmRead !== undefined) {
|
|
57960
|
+
const totalWei = [...evmRead.wei.values()].reduce((sum, value) => sum + value, 0n);
|
|
57961
|
+
const readCount = evm.length - evmRead.unavailable.length;
|
|
57962
|
+
const chain2 = evmRead.chainId === undefined ? "an unread chain" : hood ? "Hood" : `chain id ${evmRead.chainId}`;
|
|
57963
|
+
deps.stdout.write(`total ${formatUnits(totalWei, NATIVE_DECIMALS)} ${nativeHeader} across ${readCount}${readCount === evm.length ? "" : ` of ${evm.length}`} EVM keys${readCount === evm.length ? "" : " read"} on ${chain2}
|
|
57964
|
+
`);
|
|
57965
|
+
if (hood) {
|
|
57966
|
+
const totalUsdg = [...evmRead.usdg.values()].reduce((sum, value) => sum + value, 0n);
|
|
57967
|
+
deps.stdout.write(`total ${formatUnits(totalUsdg, HOOD_USDG_DECIMALS)} USDG across those keys
|
|
57968
|
+
`);
|
|
57969
|
+
}
|
|
57970
|
+
}
|
|
56472
57971
|
return complete ? 0 : 3;
|
|
56473
57972
|
});
|
|
56474
57973
|
}
|
|
@@ -57148,6 +58647,7 @@ async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
|
|
|
57148
58647
|
});
|
|
57149
58648
|
let vault = hold(opened.vault);
|
|
57150
58649
|
assertRecoverableFactorExists(vault.file.envelopes);
|
|
58650
|
+
assertNotEvmEntry(vault.index, subjectLabel, "vault promote");
|
|
57151
58651
|
const existing = findEntryByLabelOrAddress(vault.index, subjectLabel);
|
|
57152
58652
|
if (existing === undefined) {
|
|
57153
58653
|
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `No entry matches ${subjectLabel}.`, {
|
|
@@ -58778,8 +60278,8 @@ function resolveTarget(index, old, id) {
|
|
|
58778
60278
|
// src/commands/vault-restore.ts
|
|
58779
60279
|
init_args();
|
|
58780
60280
|
init_deps();
|
|
58781
|
-
init_solana_lite();
|
|
58782
60281
|
import { rm as rm3 } from "node:fs/promises";
|
|
60282
|
+
init_solana_lite();
|
|
58783
60283
|
init_crypto();
|
|
58784
60284
|
init_errors();
|
|
58785
60285
|
init_format();
|
|
@@ -58791,7 +60291,7 @@ var GAP_LIMIT = 20;
|
|
|
58791
60291
|
var SCAN_CEILING = 500;
|
|
58792
60292
|
async function vaultRestore(args, ctx) {
|
|
58793
60293
|
const parsed = parseArgs(args, {
|
|
58794
|
-
valueFlags: ["--keystore", "--count", "--tee-count", "--external-count", "--rpc-url"],
|
|
60294
|
+
valueFlags: ["--keystore", "--count", "--tee-count", "--external-count", "--evm-count", "--rpc-url"],
|
|
58795
60295
|
booleanFlags: ["--phrase", "--own-passphrase"],
|
|
58796
60296
|
pathFlags: ["--keystore"]
|
|
58797
60297
|
});
|
|
@@ -58808,7 +60308,7 @@ async function vaultRestore(args, ctx) {
|
|
|
58808
60308
|
}
|
|
58809
60309
|
if (!requireTty(ctx, "vault restore"))
|
|
58810
60310
|
return 1;
|
|
58811
|
-
const counts = parseCounts(parsed.values["--count"], parsed.values["--tee-count"], parsed.values["--external-count"], parsed.values["--rpc-url"]);
|
|
60311
|
+
const counts = parseCounts(parsed.values["--count"], parsed.values["--tee-count"], parsed.values["--external-count"], parsed.values["--rpc-url"], parsed.values["--evm-count"]);
|
|
58812
60312
|
if ("error" in counts)
|
|
58813
60313
|
return usage(ctx, counts.error);
|
|
58814
60314
|
const { deps } = ctx;
|
|
@@ -58908,7 +60408,7 @@ async function discardIncompleteRestore(ctx, path, sidecarExisted) {
|
|
|
58908
60408
|
}
|
|
58909
60409
|
}
|
|
58910
60410
|
var RESTORED_BRANCHES = ["solanaVault", "solanaTee", "solanaExternal"];
|
|
58911
|
-
function parseCounts(count, teeCount, externalCount, rpcUrl2) {
|
|
60411
|
+
function parseCounts(count, teeCount, externalCount, rpcUrl2, evmCount) {
|
|
58912
60412
|
const parse = (raw, flag) => {
|
|
58913
60413
|
if (raw === undefined)
|
|
58914
60414
|
return;
|
|
@@ -58926,6 +60426,9 @@ function parseCounts(count, teeCount, externalCount, rpcUrl2) {
|
|
|
58926
60426
|
const externalParsed = parse(externalCount, "--external-count");
|
|
58927
60427
|
if (typeof externalParsed === "object" && externalParsed !== null)
|
|
58928
60428
|
return externalParsed;
|
|
60429
|
+
const evmParsed = parse(evmCount, "--evm-count");
|
|
60430
|
+
if (typeof evmParsed === "object" && evmParsed !== null)
|
|
60431
|
+
return evmParsed;
|
|
58929
60432
|
const allOmitted = vaultCount === undefined && teeParsed === undefined && externalParsed === undefined;
|
|
58930
60433
|
const scan = rpcUrl2 !== undefined;
|
|
58931
60434
|
const resolve4 = (value) => {
|
|
@@ -58940,6 +60443,7 @@ function parseCounts(count, teeCount, externalCount, rpcUrl2) {
|
|
|
58940
60443
|
solanaVault,
|
|
58941
60444
|
solanaTee,
|
|
58942
60445
|
solanaExternal,
|
|
60446
|
+
evm: evmParsed ?? 0,
|
|
58943
60447
|
requested: { solanaVault: solanaVault ?? -1, solanaTee: solanaTee ?? -1, solanaExternal: solanaExternal ?? -1 }
|
|
58944
60448
|
};
|
|
58945
60449
|
}
|
|
@@ -58969,17 +60473,41 @@ async function deriveWithinBounds(ctx, vault, counts, rpc2) {
|
|
|
58969
60473
|
ctx.deps.stdout.write(`Gap scan on ${branch} stopped at index ${index - 1} after ${GAP_LIMIT} consecutive indices with no balance, no token account and no signature history.
|
|
58970
60474
|
`);
|
|
58971
60475
|
}
|
|
60476
|
+
for (let index = 0;index < counts.evm; index++) {
|
|
60477
|
+
set.entries.push(await deriveOneEvm(vault, root, index));
|
|
60478
|
+
}
|
|
58972
60479
|
} finally {
|
|
58973
60480
|
wipe(root);
|
|
58974
60481
|
}
|
|
58975
60482
|
for (const entry of set.entries) {
|
|
58976
60483
|
if (entry.branch === "solanaExternal")
|
|
58977
60484
|
set.externalByAddress.set(entry.address, entry);
|
|
60485
|
+
else if (entry.branch === "evm")
|
|
60486
|
+
set.byAddress.set(entry.address.toLowerCase(), entry);
|
|
58978
60487
|
else
|
|
58979
60488
|
set.byAddress.set(entry.address, entry);
|
|
58980
60489
|
}
|
|
58981
60490
|
return set;
|
|
58982
60491
|
}
|
|
60492
|
+
async function deriveOneEvm(vault, root, index) {
|
|
60493
|
+
const derived = await deriveEvmKeyFromRoot(root, index);
|
|
60494
|
+
try {
|
|
60495
|
+
const keyId = freshKeyId();
|
|
60496
|
+
return {
|
|
60497
|
+
branch: "evm",
|
|
60498
|
+
index,
|
|
60499
|
+
address: derived.address,
|
|
60500
|
+
path: derived.path,
|
|
60501
|
+
keyId,
|
|
60502
|
+
blob: await sealKeyBlob(vault, keyId, derived.secret)
|
|
60503
|
+
};
|
|
60504
|
+
} finally {
|
|
60505
|
+
wipe(derived.secret);
|
|
60506
|
+
}
|
|
60507
|
+
}
|
|
60508
|
+
function lookupKey(address) {
|
|
60509
|
+
return looksLikeEvmAddress(address) ? address.toLowerCase() : address;
|
|
60510
|
+
}
|
|
58983
60511
|
async function deriveOne(vault, root, branch, index) {
|
|
58984
60512
|
const path = pathForBranch(branch, index);
|
|
58985
60513
|
const derived = await deriveSolanaKey(root, path);
|
|
@@ -59037,7 +60565,7 @@ This profile acts as account ${read.account}.
|
|
|
59037
60565
|
for (const row of read.rows) {
|
|
59038
60566
|
if (typeof row.address !== "string")
|
|
59039
60567
|
continue;
|
|
59040
|
-
const entry = derived.byAddress.get(row.address);
|
|
60568
|
+
const entry = derived.byAddress.get(lookupKey(row.address));
|
|
59041
60569
|
if (entry)
|
|
59042
60570
|
matched.push({ entry, row });
|
|
59043
60571
|
else if (derived.externalByAddress.has(row.address))
|
|
@@ -59052,6 +60580,20 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
59052
60580
|
const matchByAddress = new Map(matches2.matched.map((match) => [match.entry.address, match.row]));
|
|
59053
60581
|
const entries = derived.entries.map((entry) => {
|
|
59054
60582
|
const row = matchByAddress.get(entry.address);
|
|
60583
|
+
if (entry.branch === "evm") {
|
|
60584
|
+
return {
|
|
60585
|
+
id: entry.keyId,
|
|
60586
|
+
chain: "evm",
|
|
60587
|
+
curve: "secp256k1",
|
|
60588
|
+
address: entry.address,
|
|
60589
|
+
label: row?.label ?? `evm-${entry.index}`,
|
|
60590
|
+
createdAt: now,
|
|
60591
|
+
role: "vault",
|
|
60592
|
+
origin: "derived",
|
|
60593
|
+
derivation: { scheme: EVM_DERIVATION_SCHEME, path: entry.path },
|
|
60594
|
+
exposure: { everRemoteExposed: row !== undefined, everExported: false, exposureUnknown: true }
|
|
60595
|
+
};
|
|
60596
|
+
}
|
|
59055
60597
|
const external2 = entry.branch === "solanaExternal";
|
|
59056
60598
|
const base = {
|
|
59057
60599
|
id: entry.keyId,
|
|
@@ -59092,14 +60634,14 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
59092
60634
|
solanaVault: highest.solanaVault + 1,
|
|
59093
60635
|
solanaTee: highest.solanaTee + 1,
|
|
59094
60636
|
solanaExternal: highest.solanaExternal + 1,
|
|
59095
|
-
evm:
|
|
60637
|
+
evm: highest.evm + 1
|
|
59096
60638
|
},
|
|
59097
60639
|
rootExported: false,
|
|
59098
60640
|
exposedIndexes: {
|
|
59099
60641
|
solanaVault: exposed.solanaVault.sort((a, b) => a - b),
|
|
59100
60642
|
solanaTee: exposed.solanaTee.sort((a, b) => a - b),
|
|
59101
60643
|
solanaExternal: exposed.solanaExternal.sort((a, b) => a - b),
|
|
59102
|
-
evm:
|
|
60644
|
+
evm: exposed.evm.sort((a, b) => a - b)
|
|
59103
60645
|
},
|
|
59104
60646
|
discovery: {
|
|
59105
60647
|
restoredAt: now,
|
|
@@ -59162,6 +60704,13 @@ Every one of them is recorded with an unknown history and stays that way: this p
|
|
|
59162
60704
|
`);
|
|
59163
60705
|
}
|
|
59164
60706
|
}
|
|
60707
|
+
if (counts.evm === 0) {
|
|
60708
|
+
deps.stdout.write(` evm: none. If this root has EVM keys, re-run with --evm-count <n>; the EVM branch is never gap-scanned.
|
|
60709
|
+
`);
|
|
60710
|
+
} else {
|
|
60711
|
+
deps.stdout.write(` evm: indices 0 to ${counts.evm - 1}, on m/44'/60'/n'/0/0.
|
|
60712
|
+
`);
|
|
60713
|
+
}
|
|
59165
60714
|
if ((matches2.externalListed?.length ?? 0) > 0) {
|
|
59166
60715
|
deps.stdout.write(`
|
|
59167
60716
|
${matches2.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.
|
|
@@ -59238,8 +60787,8 @@ async function vaultReconcileExposure(args, ctx) {
|
|
|
59238
60787
|
if (recorded !== undefined && recorded !== "" && recorded !== read.account) {
|
|
59239
60788
|
throw new VaultError("EXPOSURE_ACCOUNT_MISMATCH", `This vault recorded its discovery against account ${recorded}, and this profile acts as ${read.account}.`, { suggestion: "Switch profile and run it again. Nothing was flagged." });
|
|
59240
60789
|
}
|
|
59241
|
-
const listed = new Set(read.rows.map((row) => row.address));
|
|
59242
|
-
const entries = vault.index.entries.map((entry) => entry.role !== "external" && listed.has(entry.address) && !entry.exposure.everRemoteExposed ? { ...entry, exposure: { ...entry.exposure, everRemoteExposed: true } } : entry);
|
|
60790
|
+
const listed = new Set(read.rows.map((row) => lookupKey(row.address)));
|
|
60791
|
+
const entries = vault.index.entries.map((entry) => entry.role !== "external" && listed.has(lookupKey(entry.address)) && !entry.exposure.everRemoteExposed ? { ...entry, exposure: { ...entry.exposure, everRemoteExposed: true } } : entry);
|
|
59243
60792
|
const added = entries.filter((entry, i) => entry !== vault.index.entries[i]);
|
|
59244
60793
|
const exposedIndexes = { ...vault.index.hd.exposedIndexes };
|
|
59245
60794
|
for (const entry of entries) {
|
|
@@ -59706,6 +61255,375 @@ function describeEnvelope(envelope, facts) {
|
|
|
59706
61255
|
// src/commands/vault-transfer.ts
|
|
59707
61256
|
init_args();
|
|
59708
61257
|
init_deps();
|
|
61258
|
+
init_errors();
|
|
61259
|
+
|
|
61260
|
+
// src/vault/evm-transfer.ts
|
|
61261
|
+
init_errors();
|
|
61262
|
+
var EVM_RECEIPT_WAIT_MS = 120000;
|
|
61263
|
+
var EVM_RECEIPT_POLL_MS = 2000;
|
|
61264
|
+
function refuse2(code, message, opts = {}) {
|
|
61265
|
+
return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
|
|
61266
|
+
}
|
|
61267
|
+
function checkEvmDestination(to, from) {
|
|
61268
|
+
if (!to.startsWith("0x")) {
|
|
61269
|
+
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." });
|
|
61270
|
+
}
|
|
61271
|
+
const checked = checkEvmAddress(to);
|
|
61272
|
+
if (!checked.ok) {
|
|
61273
|
+
throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
|
|
61274
|
+
}
|
|
61275
|
+
if (sameEvmAddress(checked.address, from.address)) {
|
|
61276
|
+
throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
|
|
61277
|
+
}
|
|
61278
|
+
return checked.address;
|
|
61279
|
+
}
|
|
61280
|
+
function assertSolanaDestination(to, from) {
|
|
61281
|
+
if (!looksLikeEvmAddress(to))
|
|
61282
|
+
return;
|
|
61283
|
+
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." });
|
|
61284
|
+
}
|
|
61285
|
+
async function resolveEvmAsset(rpc2, asset, chainId) {
|
|
61286
|
+
const upper = asset.toUpperCase();
|
|
61287
|
+
if (upper === "ETH")
|
|
61288
|
+
return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
|
|
61289
|
+
let token;
|
|
61290
|
+
if (upper === "USDG") {
|
|
61291
|
+
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
61292
|
+
return {
|
|
61293
|
+
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.`
|
|
61294
|
+
};
|
|
61295
|
+
}
|
|
61296
|
+
token = toChecksumAddress(HOOD_USDG_ADDRESS);
|
|
61297
|
+
} else {
|
|
61298
|
+
const checked = checkEvmAddress(asset);
|
|
61299
|
+
if (!checked.ok)
|
|
61300
|
+
return {
|
|
61301
|
+
usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
|
|
61302
|
+
};
|
|
61303
|
+
token = checked.address;
|
|
61304
|
+
}
|
|
61305
|
+
let decimals;
|
|
61306
|
+
try {
|
|
61307
|
+
decimals = await rpc2.erc20Decimals(token);
|
|
61308
|
+
} catch (error) {
|
|
61309
|
+
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." });
|
|
61310
|
+
}
|
|
61311
|
+
const symbol = upper === "USDG" ? "USDG" : await rpc2.erc20Symbol(token) ?? `${token.slice(0, 10)}…`;
|
|
61312
|
+
return { kind: "erc20", symbol, decimals, token };
|
|
61313
|
+
}
|
|
61314
|
+
function nativeName(hood, chainId) {
|
|
61315
|
+
return hood ? "ETH" : `ETH on chain ${chainId}`;
|
|
61316
|
+
}
|
|
61317
|
+
function evmDisplayLines(plan) {
|
|
61318
|
+
const { tx, asset, hood, chainId } = plan;
|
|
61319
|
+
const native = nativeName(hood, chainId);
|
|
61320
|
+
const lines = [
|
|
61321
|
+
`chain ${chainId}${hood ? " (Hood)" : ""}`,
|
|
61322
|
+
`from ${plan.from.label} ${plan.from.address}`
|
|
61323
|
+
];
|
|
61324
|
+
if (asset.kind === "native") {
|
|
61325
|
+
lines.push(`to ${tx.to}`);
|
|
61326
|
+
lines.push(`amount ${plan.amount} ${native} = ${plan.amountRaw} wei`);
|
|
61327
|
+
} else {
|
|
61328
|
+
lines.push(`to ${tx.to} (the ${asset.symbol} contract, ${asset.decimals} dp)`);
|
|
61329
|
+
lines.push(`recipient ${plan.recipient} (decoded from transfer(address,uint256))`);
|
|
61330
|
+
lines.push(`amount ${plan.amount} ${asset.symbol} = ${plan.amountRaw} raw`);
|
|
61331
|
+
}
|
|
61332
|
+
lines.push(`gas limit ${tx.gas}`);
|
|
61333
|
+
lines.push(`max fee ${tx.maxFeePerGas} wei/gas (priority ${tx.maxPriorityFeePerGas} wei/gas)`);
|
|
61334
|
+
lines.push(`fee cap ${formatUnits(plan.feeCap, NATIVE_DECIMALS)} ${native} (gas × max fee)`);
|
|
61335
|
+
lines.push(`nonce ${tx.nonce}`);
|
|
61336
|
+
return lines;
|
|
61337
|
+
}
|
|
61338
|
+
async function planEvmTransfer(rpc2, input) {
|
|
61339
|
+
const to = checkEvmDestination(input.to, input.from);
|
|
61340
|
+
const chainId = await rpc2.chainId();
|
|
61341
|
+
if (input.builtIn && chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
61342
|
+
throw refuse2("EVM_CHAIN_MISMATCH", `The built-in Hood RPC answered chain id ${chainId}, not ${HOOD_CHAIN_ID}.`, {
|
|
61343
|
+
suggestion: "Nothing was signed. Pass --rpc-url for another chain; the built-in endpoint is Hood's only."
|
|
61344
|
+
});
|
|
61345
|
+
}
|
|
61346
|
+
const hood = chainId === BigInt(HOOD_CHAIN_ID);
|
|
61347
|
+
const asset = await resolveEvmAsset(rpc2, input.asset, chainId);
|
|
61348
|
+
if ("usage" in asset)
|
|
61349
|
+
return asset;
|
|
61350
|
+
const max = input.amount.toLowerCase() === "max";
|
|
61351
|
+
if (asset.kind === "erc20" && asset.token !== undefined && sameEvmAddress(to, asset.token)) {
|
|
61352
|
+
throw refuse2("EVM_RECIPIENT_IS_TOKEN", `${to} is the ${asset.symbol} contract itself; sending it its own tokens is a loss.`);
|
|
61353
|
+
}
|
|
61354
|
+
let amountRaw = 0n;
|
|
61355
|
+
if (!max) {
|
|
61356
|
+
const parsed = parseUnits(input.amount, asset.decimals);
|
|
61357
|
+
if (!parsed.ok && parsed.reason === "precision") {
|
|
61358
|
+
throw refuse2("EVM_AMOUNT_PRECISION", `${input.amount} has more decimal places than ${asset.symbol}'s ${asset.decimals}.`);
|
|
61359
|
+
}
|
|
61360
|
+
if (!parsed.ok)
|
|
61361
|
+
return { usage: `--amount must be a positive decimal or max; ${input.amount} is neither.` };
|
|
61362
|
+
amountRaw = parsed.raw;
|
|
61363
|
+
}
|
|
61364
|
+
const nonce = await rpc2.getTransactionCount(input.from.address, "pending");
|
|
61365
|
+
const fees = await quoteFees(rpc2);
|
|
61366
|
+
const nativeBalance = await rpc2.getBalance(input.from.address);
|
|
61367
|
+
if (asset.kind === "erc20" && asset.token !== undefined) {
|
|
61368
|
+
if (max) {
|
|
61369
|
+
amountRaw = await rpc2.erc20BalanceOf(asset.token, input.from.address);
|
|
61370
|
+
if (amountRaw === 0n) {
|
|
61371
|
+
throw new VaultError("VAULT_INDEX_INVALID", `${input.from.label} holds no ${asset.symbol}; there is nothing to send.`);
|
|
61372
|
+
}
|
|
61373
|
+
}
|
|
61374
|
+
const draft2 = buildErc20Transfer({
|
|
61375
|
+
chainId,
|
|
61376
|
+
nonce,
|
|
61377
|
+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
|
|
61378
|
+
maxFeePerGas: fees.maxFeePerGas,
|
|
61379
|
+
gas: 0n,
|
|
61380
|
+
token: asset.token,
|
|
61381
|
+
recipient: to,
|
|
61382
|
+
amount: amountRaw
|
|
61383
|
+
});
|
|
61384
|
+
const gas2 = gasWithHeadroom(await estimate(rpc2, input.from.address, draft2));
|
|
61385
|
+
const tx2 = { ...draft2, gas: gas2 };
|
|
61386
|
+
const feeCap2 = gas2 * tx2.maxFeePerGas;
|
|
61387
|
+
if (nativeBalance < feeCap2) {
|
|
61388
|
+
throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the fee cap of ${formatUnits(feeCap2, NATIVE_DECIMALS)}.`);
|
|
61389
|
+
}
|
|
61390
|
+
const plan2 = {
|
|
61391
|
+
chainId,
|
|
61392
|
+
hood,
|
|
61393
|
+
from: input.from,
|
|
61394
|
+
asset,
|
|
61395
|
+
recipient: to,
|
|
61396
|
+
amountRaw,
|
|
61397
|
+
amount: formatUnits(amountRaw, asset.decimals),
|
|
61398
|
+
tx: tx2,
|
|
61399
|
+
feeCap: feeCap2,
|
|
61400
|
+
displayLines: []
|
|
61401
|
+
};
|
|
61402
|
+
plan2.displayLines = evmDisplayLines(plan2);
|
|
61403
|
+
return plan2;
|
|
61404
|
+
}
|
|
61405
|
+
const draft = buildNativeTransfer({
|
|
61406
|
+
chainId,
|
|
61407
|
+
nonce,
|
|
61408
|
+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
|
|
61409
|
+
maxFeePerGas: fees.maxFeePerGas,
|
|
61410
|
+
gas: 0n,
|
|
61411
|
+
to,
|
|
61412
|
+
value: max ? 0n : amountRaw
|
|
61413
|
+
});
|
|
61414
|
+
if (!max && nativeBalance < amountRaw) {
|
|
61415
|
+
throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the amount of ${formatUnits(amountRaw, NATIVE_DECIMALS)}.`);
|
|
61416
|
+
}
|
|
61417
|
+
const gas = gasWithHeadroom(await estimate(rpc2, input.from.address, draft));
|
|
61418
|
+
const feeCap = gas * draft.maxFeePerGas;
|
|
61419
|
+
let value = amountRaw;
|
|
61420
|
+
if (max) {
|
|
61421
|
+
value = nativeBalance - feeCap;
|
|
61422
|
+
if (value <= 0n) {
|
|
61423
|
+
throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, not above the fee cap of ${formatUnits(feeCap, NATIVE_DECIMALS)}; max leaves nothing to send.`);
|
|
61424
|
+
}
|
|
61425
|
+
} else if (nativeBalance < value + feeCap) {
|
|
61426
|
+
throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the amount plus the fee cap of ${formatUnits(value + feeCap, NATIVE_DECIMALS)}.`);
|
|
61427
|
+
}
|
|
61428
|
+
const tx = { ...draft, gas, value };
|
|
61429
|
+
const plan = {
|
|
61430
|
+
chainId,
|
|
61431
|
+
hood,
|
|
61432
|
+
from: input.from,
|
|
61433
|
+
asset,
|
|
61434
|
+
recipient: to,
|
|
61435
|
+
amountRaw: value,
|
|
61436
|
+
amount: formatUnits(value, NATIVE_DECIMALS),
|
|
61437
|
+
tx,
|
|
61438
|
+
feeCap,
|
|
61439
|
+
displayLines: []
|
|
61440
|
+
};
|
|
61441
|
+
plan.displayLines = evmDisplayLines(plan);
|
|
61442
|
+
return plan;
|
|
61443
|
+
}
|
|
61444
|
+
async function estimate(rpc2, from, tx) {
|
|
61445
|
+
try {
|
|
61446
|
+
return await rpc2.estimateGas({ from, to: tx.to, value: tx.value, data: tx.data });
|
|
61447
|
+
} catch (error) {
|
|
61448
|
+
throw new VaultError("VAULT_UNREADABLE", `Could not estimate gas: ${error instanceof Error ? error.message : String(error)}`, { suggestion: "Nothing was signed. A revert here usually means the balance does not cover the amount." });
|
|
61449
|
+
}
|
|
61450
|
+
}
|
|
61451
|
+
function displayEvmTransferPlan(ctx, plan) {
|
|
61452
|
+
ctx.deps.stdout.write(`Decoded EVM transfer (local signing only):
|
|
61453
|
+
`);
|
|
61454
|
+
for (const line of plan.displayLines)
|
|
61455
|
+
ctx.deps.stdout.write(` ${line}
|
|
61456
|
+
`);
|
|
61457
|
+
}
|
|
61458
|
+
function evmFactorPrompt(plan) {
|
|
61459
|
+
return `sign transfer of ${plan.amount} ${plan.asset.symbol} to ${plan.recipient}${plan.hood ? " on Hood" : ` on chain ${plan.chainId}`}`;
|
|
61460
|
+
}
|
|
61461
|
+
function judgeReceipt(receipt, head, depth, hash, chainId) {
|
|
61462
|
+
if (receipt.status === 0) {
|
|
61463
|
+
throw new VaultError("EVM_TRANSFER_REVERTED", `Transaction ${hash} reverted in block ${receipt.blockNumber}; the fee was spent.`, {
|
|
61464
|
+
suggestion: "Nothing else was signed. Read the transaction on an explorer before sending again.",
|
|
61465
|
+
details: { hash, chainId: chainId.toString(), blockNumber: receipt.blockNumber.toString(), status: "reverted" }
|
|
61466
|
+
});
|
|
61467
|
+
}
|
|
61468
|
+
const reached = head - receipt.blockNumber + 1n;
|
|
61469
|
+
if (reached < BigInt(depth))
|
|
61470
|
+
return;
|
|
61471
|
+
return {
|
|
61472
|
+
status: "confirmed",
|
|
61473
|
+
exit: 0,
|
|
61474
|
+
blockNumber: receipt.blockNumber,
|
|
61475
|
+
line: `Confirmed ${hash} in block ${receipt.blockNumber}, ${reached} block${reached === 1n ? "" : "s"} deep (depth ${depth}, not finality).`
|
|
61476
|
+
};
|
|
61477
|
+
}
|
|
61478
|
+
async function awaitReceipt(rpc2, ctx, hash, chainId) {
|
|
61479
|
+
const depth = requiredDepth(chainId);
|
|
61480
|
+
const deadline = ctx.deps.now() + EVM_RECEIPT_WAIT_MS;
|
|
61481
|
+
let seen;
|
|
61482
|
+
for (;; ) {
|
|
61483
|
+
try {
|
|
61484
|
+
const receipt = await rpc2.getTransactionReceipt(hash);
|
|
61485
|
+
if (receipt !== null) {
|
|
61486
|
+
seen = receipt;
|
|
61487
|
+
const outcome = judgeReceipt(receipt, await rpc2.blockNumber(), depth, hash, chainId);
|
|
61488
|
+
if (outcome !== undefined)
|
|
61489
|
+
return outcome;
|
|
61490
|
+
}
|
|
61491
|
+
} catch (error) {
|
|
61492
|
+
if (error instanceof VaultError)
|
|
61493
|
+
throw error;
|
|
61494
|
+
}
|
|
61495
|
+
if (ctx.deps.now() >= deadline)
|
|
61496
|
+
break;
|
|
61497
|
+
await ctx.deps.sleep(EVM_RECEIPT_POLL_MS);
|
|
61498
|
+
}
|
|
61499
|
+
if (seen !== undefined) {
|
|
61500
|
+
return {
|
|
61501
|
+
status: "uncertain",
|
|
61502
|
+
exit: 3,
|
|
61503
|
+
blockNumber: seen.blockNumber,
|
|
61504
|
+
line: `Submitted ${hash}: it is in block ${seen.blockNumber} but not yet ${depth} block${depth === 1 ? "" : "s"} deep after ${EVM_RECEIPT_WAIT_MS / 1000} s. Do not resend; check the hash on an explorer.`
|
|
61505
|
+
};
|
|
61506
|
+
}
|
|
61507
|
+
return {
|
|
61508
|
+
status: "uncertain",
|
|
61509
|
+
exit: 3,
|
|
61510
|
+
line: `Submitted ${hash}; no receipt after ${EVM_RECEIPT_WAIT_MS / 1000} s. It may still land: do not resend blindly; check the hash on an explorer first.`
|
|
61511
|
+
};
|
|
61512
|
+
}
|
|
61513
|
+
async function broadcast(rpc2, ctx, raw, hash, chainId) {
|
|
61514
|
+
try {
|
|
61515
|
+
await rpc2.sendRawTransaction(raw);
|
|
61516
|
+
} catch (error) {
|
|
61517
|
+
if (!(error instanceof EvmRpcError))
|
|
61518
|
+
throw error;
|
|
61519
|
+
const message = error.message.toLowerCase();
|
|
61520
|
+
if (error.kind === "transport") {
|
|
61521
|
+
return {
|
|
61522
|
+
status: "uncertain",
|
|
61523
|
+
exit: 3,
|
|
61524
|
+
line: `Submitted ${hash}, but the RPC did not answer eth_sendRawTransaction (${error.message}). It may still land: do not resend blindly; check the hash on an explorer first.`
|
|
61525
|
+
};
|
|
61526
|
+
}
|
|
61527
|
+
if (message.includes("already known")) {
|
|
61528
|
+
return {
|
|
61529
|
+
status: "uncertain",
|
|
61530
|
+
exit: 3,
|
|
61531
|
+
line: `Submitted ${hash}: the RPC already knows it, so it is in flight. Do not resend; check the hash on an explorer.`
|
|
61532
|
+
};
|
|
61533
|
+
}
|
|
61534
|
+
if (message.includes("nonce too low")) {
|
|
61535
|
+
let receipt = null;
|
|
61536
|
+
try {
|
|
61537
|
+
receipt = await rpc2.getTransactionReceipt(hash);
|
|
61538
|
+
} catch {
|
|
61539
|
+
receipt = null;
|
|
61540
|
+
}
|
|
61541
|
+
if (receipt !== null) {
|
|
61542
|
+
let head;
|
|
61543
|
+
try {
|
|
61544
|
+
head = await rpc2.blockNumber();
|
|
61545
|
+
} catch {
|
|
61546
|
+
return awaitReceipt(rpc2, ctx, hash, chainId);
|
|
61547
|
+
}
|
|
61548
|
+
const outcome = judgeReceipt(receipt, head, requiredDepth(chainId), hash, chainId);
|
|
61549
|
+
if (outcome !== undefined)
|
|
61550
|
+
return outcome;
|
|
61551
|
+
return awaitReceipt(rpc2, ctx, hash, chainId);
|
|
61552
|
+
}
|
|
61553
|
+
throw new VaultError("EVM_NONCE_STALE", `The RPC refused ${hash}: nonce too low, and it has no receipt for that hash. Another transaction used this nonce.`, {
|
|
61554
|
+
suggestion: "Nothing was resent. Run the transfer again; it reads the pending nonce afresh. The CLI never re-signs on its own.",
|
|
61555
|
+
details: { hash, chainId: chainId.toString() }
|
|
61556
|
+
});
|
|
61557
|
+
}
|
|
61558
|
+
return {
|
|
61559
|
+
status: "uncertain",
|
|
61560
|
+
exit: 3,
|
|
61561
|
+
line: `Submitted ${hash}, and the RPC answered: ${error.message}. The CLI cannot tell from that whether it is in flight: do not resend blindly; check the hash on an explorer first.`
|
|
61562
|
+
};
|
|
61563
|
+
}
|
|
61564
|
+
return awaitReceipt(rpc2, ctx, hash, chainId);
|
|
61565
|
+
}
|
|
61566
|
+
async function runEvmTransfer(input, rpc2) {
|
|
61567
|
+
const { ctx } = input;
|
|
61568
|
+
const { deps } = ctx;
|
|
61569
|
+
checkEvmDestination(input.to, input.from);
|
|
61570
|
+
deps.stderr.write(`Reading chain id, nonce, fees and balances for ${input.from.label} from ${rpcHostOf(input.rpcUrl)}${input.rpcUrl === DEFAULT_HOOD_RPC_URL ? " (the built-in Hood RPC)" : ""}.
|
|
61571
|
+
`);
|
|
61572
|
+
const planned = await planEvmTransfer(rpc2, input);
|
|
61573
|
+
if ("usage" in planned)
|
|
61574
|
+
return input.usage(planned.usage);
|
|
61575
|
+
const plan = planned;
|
|
61576
|
+
displayEvmTransferPlan(ctx, plan);
|
|
61577
|
+
await input.confirmLastSix(plan.recipient, plan.asset.kind === "erc20" ? "the token recipient" : "the destination");
|
|
61578
|
+
await input.confirmFactor(evmFactorPrompt(plan));
|
|
61579
|
+
const chainIdAgain = await rpc2.chainId();
|
|
61580
|
+
if (chainIdAgain !== plan.chainId) {
|
|
61581
|
+
throw refuse2("EVM_CHAIN_MISMATCH", `The RPC answered chain id ${chainIdAgain} after the factor, but ${plan.chainId} was displayed.`);
|
|
61582
|
+
}
|
|
61583
|
+
const nonceAgain = await rpc2.getTransactionCount(plan.from.address, "pending");
|
|
61584
|
+
if (nonceAgain !== plan.tx.nonce) {
|
|
61585
|
+
throw refuse2("EVM_NONCE_STALE", `The pending nonce is ${nonceAgain} after the factor, but ${plan.tx.nonce} was displayed; another transaction moved it.`, { suggestion: "Nothing was signed. Run the transfer again; it reads the pending nonce afresh." });
|
|
61586
|
+
}
|
|
61587
|
+
const secret = await input.decryptSecret();
|
|
61588
|
+
let signed;
|
|
61589
|
+
try {
|
|
61590
|
+
if (!sameEvmAddress(evmAddressFromSecret(secret), plan.from.address)) {
|
|
61591
|
+
throw new VaultError("VAULT_VERIFY_FAILED", "The decrypted key does not match the planned sender.");
|
|
61592
|
+
}
|
|
61593
|
+
signed = signTransaction(plan.tx, secret);
|
|
61594
|
+
} finally {
|
|
61595
|
+
wipe(secret);
|
|
61596
|
+
}
|
|
61597
|
+
const outcome = await broadcast(rpc2, ctx, signed.raw, signed.hash, plan.chainId);
|
|
61598
|
+
if (ctx.json) {
|
|
61599
|
+
input.writeJson({
|
|
61600
|
+
ok: outcome.status === "confirmed",
|
|
61601
|
+
chainId: Number(plan.chainId),
|
|
61602
|
+
hash: signed.hash,
|
|
61603
|
+
status: outcome.status,
|
|
61604
|
+
...outcome.blockNumber !== undefined ? { blockNumber: outcome.blockNumber.toString() } : {},
|
|
61605
|
+
depth: requiredDepth(plan.chainId),
|
|
61606
|
+
finalized: false,
|
|
61607
|
+
from: plan.from.address,
|
|
61608
|
+
to: plan.tx.to,
|
|
61609
|
+
recipient: plan.recipient,
|
|
61610
|
+
amount: plan.amount,
|
|
61611
|
+
asset: plan.asset.symbol,
|
|
61612
|
+
amountRaw: plan.amountRaw.toString(),
|
|
61613
|
+
...plan.asset.token !== undefined ? { token: plan.asset.token } : {},
|
|
61614
|
+
nonce: plan.tx.nonce.toString(),
|
|
61615
|
+
gas: plan.tx.gas.toString(),
|
|
61616
|
+
maxFeePerGas: plan.tx.maxFeePerGas.toString(),
|
|
61617
|
+
maxPriorityFeePerGas: plan.tx.maxPriorityFeePerGas.toString()
|
|
61618
|
+
});
|
|
61619
|
+
} else {
|
|
61620
|
+
deps.stdout.write(`${outcome.line}
|
|
61621
|
+
`);
|
|
61622
|
+
}
|
|
61623
|
+
return outcome.exit;
|
|
61624
|
+
}
|
|
61625
|
+
|
|
61626
|
+
// src/commands/vault-transfer.ts
|
|
59709
61627
|
init_promote_support();
|
|
59710
61628
|
init_store();
|
|
59711
61629
|
|
|
@@ -59788,20 +61706,29 @@ async function vaultTransfer(args, ctx) {
|
|
|
59788
61706
|
return usage(ctx, parsed.error);
|
|
59789
61707
|
const [to, extra] = parsed.positionals;
|
|
59790
61708
|
if (!to || extra !== undefined) {
|
|
59791
|
-
return usage(ctx, "Usage: candle vault transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>");
|
|
61709
|
+
return usage(ctx, "Usage: candle vault transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]");
|
|
59792
61710
|
}
|
|
59793
61711
|
const amount = parsed.values["--amount"];
|
|
59794
61712
|
const asset = parsed.values["--asset"];
|
|
59795
61713
|
const fromLabel = parsed.values["--from"];
|
|
59796
|
-
const
|
|
61714
|
+
const rpcUrlFlag = parsed.values["--rpc-url"];
|
|
59797
61715
|
if (!amount)
|
|
59798
61716
|
return usage(ctx, "--amount <n> is required.");
|
|
59799
61717
|
if (!asset)
|
|
59800
|
-
return usage(ctx, "--asset SOL|<mint> is required.");
|
|
61718
|
+
return usage(ctx, "--asset SOL|<mint>|ETH|USDG|<0x token> is required.");
|
|
59801
61719
|
if (!fromLabel)
|
|
59802
61720
|
return usage(ctx, "--from <label> is required.");
|
|
59803
|
-
if (
|
|
59804
|
-
|
|
61721
|
+
if (rpcUrlFlag !== undefined) {
|
|
61722
|
+
try {
|
|
61723
|
+
new URL(rpcUrlFlag);
|
|
61724
|
+
} catch {
|
|
61725
|
+
return usage(ctx, `--rpc-url is not a valid URL: ${rpcUrlFlag}`);
|
|
61726
|
+
}
|
|
61727
|
+
} else {
|
|
61728
|
+
const evmFromEnv = resolveEvmRpcUrl(undefined, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
61729
|
+
if ("error" in evmFromEnv)
|
|
61730
|
+
return usage(ctx, evmFromEnv.error);
|
|
61731
|
+
}
|
|
59805
61732
|
if (!refuseEnvPassphrase(ctx))
|
|
59806
61733
|
return 1;
|
|
59807
61734
|
if (!requireTty(ctx, "vault transfer"))
|
|
@@ -59822,6 +61749,34 @@ async function vaultTransfer(args, ctx) {
|
|
|
59822
61749
|
return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
|
|
59823
61750
|
}
|
|
59824
61751
|
assertTransferSigner(fromEntry);
|
|
61752
|
+
if (fromEntry.chain === "evm") {
|
|
61753
|
+
if (fromEntry.role !== "vault") {
|
|
61754
|
+
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${fromEntry.label || fromEntry.address} is an EVM ${fromEntry.role} entry; this release signs EVM transfers from vault keys only.`, { suggestion: "Nothing was signed." });
|
|
61755
|
+
}
|
|
61756
|
+
const evmRpc = resolveEvmRpcUrl(rpcUrlFlag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
|
|
61757
|
+
if ("error" in evmRpc)
|
|
61758
|
+
return usage(ctx, evmRpc.error);
|
|
61759
|
+
const evmRpcUrl = evmRpc.url;
|
|
61760
|
+
const secretRef2 = fromEntry;
|
|
61761
|
+
return runEvmTransfer({
|
|
61762
|
+
ctx,
|
|
61763
|
+
rpcUrl: evmRpcUrl,
|
|
61764
|
+
builtIn: evmRpc.builtIn,
|
|
61765
|
+
from: fromEntry,
|
|
61766
|
+
to,
|
|
61767
|
+
amount,
|
|
61768
|
+
asset,
|
|
61769
|
+
confirmLastSix: (address, what) => confirmLastSix(ctx, address, what),
|
|
61770
|
+
confirmFactor: (what) => opened.confirm(what),
|
|
61771
|
+
decryptSecret: () => decryptKey(vault, secretRef2.id),
|
|
61772
|
+
usage: (line) => usage(ctx, line),
|
|
61773
|
+
writeJson: (value) => writeJson(ctx.deps, value)
|
|
61774
|
+
}, createEvmRpc(evmRpcUrl, ctx.deps.fetch));
|
|
61775
|
+
}
|
|
61776
|
+
assertSolanaDestination(to, fromEntry);
|
|
61777
|
+
const rpcUrl2 = rpcUrlFlag;
|
|
61778
|
+
if (!rpcUrl2)
|
|
61779
|
+
return usage(ctx, "--rpc-url <url> is required for a transfer from a Solana key.");
|
|
59825
61780
|
const promoted = fromEntry.role === "tee-wallet";
|
|
59826
61781
|
if (promoted)
|
|
59827
61782
|
assertNoPendingSweep(fromEntry);
|
|
@@ -59889,7 +61844,7 @@ init_args();
|
|
|
59889
61844
|
init_release();
|
|
59890
61845
|
import { dirname as dirname10, join as join11 } from "node:path";
|
|
59891
61846
|
init_render();
|
|
59892
|
-
var
|
|
61847
|
+
var USAGE3 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
|
|
59893
61848
|
async function resolveIdentity(deps, bundlePath, flag) {
|
|
59894
61849
|
if (flag)
|
|
59895
61850
|
return { kind: "ok", uri: flag, provenance: "identity from --identity" };
|
|
@@ -59921,19 +61876,19 @@ async function verify(args, ctx) {
|
|
|
59921
61876
|
});
|
|
59922
61877
|
if ("error" in parsed) {
|
|
59923
61878
|
writeUsageFailure(deps, `${parsed.error}
|
|
59924
|
-
${
|
|
61879
|
+
${USAGE3}`, json);
|
|
59925
61880
|
return 2;
|
|
59926
61881
|
}
|
|
59927
61882
|
const file = parsed.positionals[0];
|
|
59928
61883
|
if (parsed.positionals.length !== 1 || file === undefined) {
|
|
59929
61884
|
writeUsageFailure(deps, `verify takes exactly one file.
|
|
59930
|
-
${
|
|
61885
|
+
${USAGE3}`, json);
|
|
59931
61886
|
return 2;
|
|
59932
61887
|
}
|
|
59933
61888
|
const bundlePath = parsed.values["--bundle"];
|
|
59934
61889
|
if (!bundlePath) {
|
|
59935
61890
|
writeUsageFailure(deps, `--bundle is required.
|
|
59936
|
-
${
|
|
61891
|
+
${USAGE3}`, json);
|
|
59937
61892
|
return 2;
|
|
59938
61893
|
}
|
|
59939
61894
|
const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
|
|
@@ -59947,7 +61902,7 @@ ${USAGE2}`, json);
|
|
|
59947
61902
|
}
|
|
59948
61903
|
if (resolved.kind === "absent") {
|
|
59949
61904
|
writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
|
|
59950
|
-
${
|
|
61905
|
+
${USAGE3}`, json);
|
|
59951
61906
|
return 2;
|
|
59952
61907
|
}
|
|
59953
61908
|
const identity = resolved.uri;
|
|
@@ -59988,6 +61943,177 @@ function messageOf2(error) {
|
|
|
59988
61943
|
return error instanceof Error ? error.message : String(error);
|
|
59989
61944
|
}
|
|
59990
61945
|
|
|
61946
|
+
// src/commands/wallets-trust.ts
|
|
61947
|
+
init_args();
|
|
61948
|
+
init_deps();
|
|
61949
|
+
init_profiles();
|
|
61950
|
+
init_render();
|
|
61951
|
+
init_promote_support();
|
|
61952
|
+
var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
|
|
61953
|
+
var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
|
|
61954
|
+
var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
|
|
61955
|
+
var DEVICE_TOKEN_REQUIRED2 = {
|
|
61956
|
+
code: "DEVICE_TOKEN_REQUIRED",
|
|
61957
|
+
message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
|
|
61958
|
+
suggestion: "Run: candle auth login"
|
|
61959
|
+
};
|
|
61960
|
+
function sessionLinkedLine(rows) {
|
|
61961
|
+
const n = rows.length;
|
|
61962
|
+
const it = n === 1 ? "it" : "them";
|
|
61963
|
+
const names = rows.map((row) => row.label ?? row.address).join(", ");
|
|
61964
|
+
return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
|
|
61965
|
+
}
|
|
61966
|
+
function nothingToChangeLine(shown, trusted) {
|
|
61967
|
+
const session = shown.sessionLinked ?? [];
|
|
61968
|
+
const unchanged = shown.unchanged;
|
|
61969
|
+
if (session.length > 0 && unchanged.length === 0) {
|
|
61970
|
+
const one = session.length === 1;
|
|
61971
|
+
return `Nothing to change: ${one ? "that wallet is" : `those ${session.length} wallets are`} always yours while linked. Revoke ${one ? "it" : "them"} to remove ${one ? "it" : "them"}.
|
|
61972
|
+
`;
|
|
61973
|
+
}
|
|
61974
|
+
const bits = [];
|
|
61975
|
+
if (session.length > 0)
|
|
61976
|
+
bits.push(sessionLinkedLine(session).replace(/\.$/, ""));
|
|
61977
|
+
if (unchanged.length > 0) {
|
|
61978
|
+
bits.push(unchanged.length === 1 ? `that wallet is already ${trusted ? "trusted" : "untrusted"}` : `all ${unchanged.length} wallets are already ${trusted ? "trusted" : "untrusted"}`);
|
|
61979
|
+
}
|
|
61980
|
+
if (bits.length === 0)
|
|
61981
|
+
return `Nothing to change.
|
|
61982
|
+
`;
|
|
61983
|
+
return `Nothing to change: ${bits.join("; ")}.
|
|
61984
|
+
`;
|
|
61985
|
+
}
|
|
61986
|
+
function writeTrustFailure(ctx, result) {
|
|
61987
|
+
const { deps, apiUrl, json } = ctx;
|
|
61988
|
+
let code = result.code;
|
|
61989
|
+
let message = result.message;
|
|
61990
|
+
let suggestion;
|
|
61991
|
+
if (result.status === 404) {
|
|
61992
|
+
code = "TRUST_UNSUPPORTED";
|
|
61993
|
+
message = "This Candle API does not support trusting wallets yet; nothing changed.";
|
|
61994
|
+
} else {
|
|
61995
|
+
const error = result.raw?.error;
|
|
61996
|
+
if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
|
|
61997
|
+
const ids = error.matches.map((m) => String(m.id)).join(", ");
|
|
61998
|
+
suggestion = `Name one of them by id or address: ${ids}`;
|
|
61999
|
+
} else if (error?.reason === "not_found") {
|
|
62000
|
+
suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
|
|
62001
|
+
}
|
|
62002
|
+
}
|
|
62003
|
+
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
|
|
62004
|
+
if (json) {
|
|
62005
|
+
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
62006
|
+
`);
|
|
62007
|
+
} else {
|
|
62008
|
+
deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
62009
|
+
`);
|
|
62010
|
+
}
|
|
62011
|
+
return 1;
|
|
62012
|
+
}
|
|
62013
|
+
function trustTable(rows) {
|
|
62014
|
+
return renderTable(["line", "label", "wallet", "address"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address]));
|
|
62015
|
+
}
|
|
62016
|
+
async function setTrust(args, ctx, trusted) {
|
|
62017
|
+
const { deps, apiUrl, json } = ctx;
|
|
62018
|
+
const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
|
|
62019
|
+
const command = trusted ? "wallets trust" : "wallets untrust";
|
|
62020
|
+
const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
|
|
62021
|
+
if ("error" in parsed) {
|
|
62022
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
62023
|
+
return 2;
|
|
62024
|
+
}
|
|
62025
|
+
if (parsed.positionals.length === 0) {
|
|
62026
|
+
writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
|
|
62027
|
+
return 2;
|
|
62028
|
+
}
|
|
62029
|
+
const skipConfirm = !trusted && parsed.booleans.has("--yes");
|
|
62030
|
+
await printIdentity(ctx);
|
|
62031
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
62032
|
+
if (!deviceToken) {
|
|
62033
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
|
|
62034
|
+
return 1;
|
|
62035
|
+
}
|
|
62036
|
+
if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
|
|
62037
|
+
writeLocalFailure(deps, {
|
|
62038
|
+
code: "TRUST_REQUIRES_TTY",
|
|
62039
|
+
message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
|
|
62040
|
+
suggestion: trusted ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to untrust without the prompt."
|
|
62041
|
+
}, json);
|
|
62042
|
+
return 1;
|
|
62043
|
+
}
|
|
62044
|
+
const call = (body) => apiRequest(TRUST_PATH, {
|
|
62045
|
+
method: "POST",
|
|
62046
|
+
body,
|
|
62047
|
+
auth: "device",
|
|
62048
|
+
credentials: { deviceToken },
|
|
62049
|
+
apiUrl,
|
|
62050
|
+
fetch: deps.fetch,
|
|
62051
|
+
env: deps.env
|
|
62052
|
+
});
|
|
62053
|
+
const preview = await call({ dryRun: true, wallets: parsed.positionals, trusted });
|
|
62054
|
+
if (!preview.ok)
|
|
62055
|
+
return writeTrustFailure(ctx, preview);
|
|
62056
|
+
const shown = preview.body;
|
|
62057
|
+
const changing = shown.changed;
|
|
62058
|
+
if (changing.length === 0) {
|
|
62059
|
+
if (json) {
|
|
62060
|
+
deps.stdout.write(`${JSON.stringify({ ...shown, command })}
|
|
62061
|
+
`);
|
|
62062
|
+
} else {
|
|
62063
|
+
deps.stdout.write(nothingToChangeLine(shown, trusted));
|
|
62064
|
+
}
|
|
62065
|
+
return 0;
|
|
62066
|
+
}
|
|
62067
|
+
const n = changing.length;
|
|
62068
|
+
const screen = [
|
|
62069
|
+
trustTable(changing),
|
|
62070
|
+
...shown.unchanged.length > 0 ? [
|
|
62071
|
+
`${shown.unchanged.length} already ${trusted ? "trusted" : "untrusted"}: ${shown.unchanged.map((row) => row.label ?? row.address).join(", ")}`
|
|
62072
|
+
] : [],
|
|
62073
|
+
...(shown.sessionLinked?.length ?? 0) > 0 ? [sessionLinkedLine(shown.sessionLinked ?? [])] : [],
|
|
62074
|
+
"",
|
|
62075
|
+
trusted ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be trusted: your agents can move funds into ${n === 1 ? "it" : "them"} with no cap, like a wallet you linked while signed in.` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be trusted: moving funds into ${n === 1 ? "it" : "them"} will need the withdrawal allowlist again.`,
|
|
62076
|
+
""
|
|
62077
|
+
];
|
|
62078
|
+
deps.stderr.write(`${screen.join(`
|
|
62079
|
+
`)}
|
|
62080
|
+
`);
|
|
62081
|
+
if (!skipConfirm) {
|
|
62082
|
+
const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${trusted ? "trust" : "untrust"} ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
|
|
62083
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
62084
|
+
writeLocalFailure(deps, {
|
|
62085
|
+
code: "TRUST_NOT_ACKNOWLEDGED",
|
|
62086
|
+
message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
|
|
62087
|
+
suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
|
|
62088
|
+
}, json);
|
|
62089
|
+
return 1;
|
|
62090
|
+
}
|
|
62091
|
+
}
|
|
62092
|
+
const committed = await call({ walletIds: changing.map((row) => row.id), trusted });
|
|
62093
|
+
if (!committed.ok)
|
|
62094
|
+
return writeTrustFailure(ctx, committed);
|
|
62095
|
+
const result = committed.body;
|
|
62096
|
+
if (json) {
|
|
62097
|
+
deps.stdout.write(`${JSON.stringify({ ...result, command })}
|
|
62098
|
+
`);
|
|
62099
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
62100
|
+
}
|
|
62101
|
+
const done = result.changed.length;
|
|
62102
|
+
deps.stdout.write(`${trusted ? "Trusted" : "Untrusted"} ${done} wallet${done === 1 ? "" : "s"}.
|
|
62103
|
+
`);
|
|
62104
|
+
for (const skip of result.skipped) {
|
|
62105
|
+
deps.stderr.write(`Skipped ${skip.walletId}: ${skip.reason === "revoked" ? "revoked" : "not on this account"}.
|
|
62106
|
+
`);
|
|
62107
|
+
}
|
|
62108
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
62109
|
+
}
|
|
62110
|
+
function walletsTrust(args, ctx) {
|
|
62111
|
+
return setTrust(args, ctx, true);
|
|
62112
|
+
}
|
|
62113
|
+
function walletsUntrust(args, ctx) {
|
|
62114
|
+
return setTrust(args, ctx, false);
|
|
62115
|
+
}
|
|
62116
|
+
|
|
59991
62117
|
// src/config.ts
|
|
59992
62118
|
import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
59993
62119
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -60303,6 +62429,7 @@ function extractGlobalFlags(argv) {
|
|
|
60303
62429
|
}
|
|
60304
62430
|
var COMMANDS = {
|
|
60305
62431
|
swap: { bare: swap, subcommands: { status: swapStatus } },
|
|
62432
|
+
transfer: { bare: transfer },
|
|
60306
62433
|
launch: { bare: launch },
|
|
60307
62434
|
pnl: { bare: pnl },
|
|
60308
62435
|
portfolio: { bare: portfolio },
|
|
@@ -60310,7 +62437,7 @@ var COMMANDS = {
|
|
|
60310
62437
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
60311
62438
|
keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
|
|
60312
62439
|
wallets: {
|
|
60313
|
-
subcommands: { import: walletsImport, revoke: walletsRevoke },
|
|
62440
|
+
subcommands: { import: walletsImport, revoke: walletsRevoke, trust: walletsTrust, untrust: walletsUntrust },
|
|
60314
62441
|
bare: wallets
|
|
60315
62442
|
},
|
|
60316
62443
|
vault: {
|