@candledottv/cli 0.11.6 → 0.11.8
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 +15 -3
- package/dist/index.js +3156 -256
- package/package.json +1 -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,35 @@ 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(", ");
|
|
361
|
+
}
|
|
362
|
+
function terminalText(value) {
|
|
363
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
334
364
|
}
|
|
335
365
|
function renderTable(headers, rows) {
|
|
336
366
|
const widths = headers.map((header, col) => Math.max(header.length, ...rows.map((row) => (row[col] ?? "").length)));
|
|
@@ -433,7 +463,7 @@ function portalDeviceUrl(apiUrl, portalOrigin) {
|
|
|
433
463
|
return `${apiUrl}/agents`;
|
|
434
464
|
}
|
|
435
465
|
}
|
|
436
|
-
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";
|
|
437
467
|
var init_render = __esm(() => {
|
|
438
468
|
init_agent_key_access();
|
|
439
469
|
ALL_AGENT_SCOPES = [
|
|
@@ -442,9 +472,10 @@ var init_render = __esm(() => {
|
|
|
442
472
|
"account:read",
|
|
443
473
|
"activity:write",
|
|
444
474
|
"swap:write",
|
|
445
|
-
"transfer:write"
|
|
475
|
+
"transfer:write",
|
|
476
|
+
"transfer:bound"
|
|
446
477
|
];
|
|
447
|
-
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");
|
|
448
479
|
});
|
|
449
480
|
|
|
450
481
|
// src/profiles.ts
|
|
@@ -2161,6 +2192,9 @@ function libraryInstallInstruction(platform) {
|
|
|
2161
2192
|
}
|
|
2162
2193
|
|
|
2163
2194
|
// src/fido2-helper/protocol.ts
|
|
2195
|
+
function excludeCapacity(device) {
|
|
2196
|
+
return device.maxCredentialCountInList ?? 1;
|
|
2197
|
+
}
|
|
2164
2198
|
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;
|
|
2165
2199
|
var init_protocol = () => {};
|
|
2166
2200
|
|
|
@@ -3140,7 +3174,9 @@ var init_errors = __esm(() => {
|
|
|
3140
3174
|
"WALLET_LIMIT_REACHED",
|
|
3141
3175
|
"TIER_REQUIRED",
|
|
3142
3176
|
"LINKED_WALLET_ROOM_UNREADABLE",
|
|
3143
|
-
"PROMOTE_ACCOUNT_UNRESOLVED"
|
|
3177
|
+
"PROMOTE_ACCOUNT_UNRESOLVED",
|
|
3178
|
+
"TRANSFER_SWEEP_PENDING",
|
|
3179
|
+
"VAULT_KEY_ALREADY_ENROLLED"
|
|
3144
3180
|
];
|
|
3145
3181
|
VaultError = class VaultError extends Error {
|
|
3146
3182
|
code;
|
|
@@ -5953,6 +5989,9 @@ function translateHelperFailure(code, message, platform) {
|
|
|
5953
5989
|
LIBRARY_MISSING: "VAULT_HELPER_MISSING"
|
|
5954
5990
|
};
|
|
5955
5991
|
const suggestion = "Nothing was derived and no other factor was tried.";
|
|
5992
|
+
if (code === "CREDENTIAL_EXCLUDED") {
|
|
5993
|
+
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 });
|
|
5994
|
+
}
|
|
5956
5995
|
if (code === "DEVICE_NOT_READABLE") {
|
|
5957
5996
|
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." });
|
|
5958
5997
|
}
|
|
@@ -6013,6 +6052,51 @@ async function callHelper(deps, helperPath, request) {
|
|
|
6013
6052
|
throw translateHelperFailure(String(response.code), String(response.message), deps.platform);
|
|
6014
6053
|
return response;
|
|
6015
6054
|
}
|
|
6055
|
+
function alreadyEnrolled(device, envelopeId) {
|
|
6056
|
+
const product = device.product || "This security key";
|
|
6057
|
+
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.`, {
|
|
6058
|
+
suggestion: ALREADY_ENROLLED_SUGGESTION,
|
|
6059
|
+
...envelopeId !== undefined ? { details: { envelopeId } } : {}
|
|
6060
|
+
});
|
|
6061
|
+
}
|
|
6062
|
+
function helperPredatesExcludeList(helperPath, leftover) {
|
|
6063
|
+
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.`, {
|
|
6064
|
+
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."
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
function assertHelperExcludeList(info, helperPath, excludeCount) {
|
|
6068
|
+
if (excludeCount === 0)
|
|
6069
|
+
return;
|
|
6070
|
+
const features = Array.isArray(info.features) ? info.features : [];
|
|
6071
|
+
if (!features.includes(EXCLUDE_LIST_FEATURE))
|
|
6072
|
+
throw helperPredatesExcludeList(helperPath, false);
|
|
6073
|
+
}
|
|
6074
|
+
function assertExcludeListFits(device, excludeCount) {
|
|
6075
|
+
const capacity = excludeCapacity(device);
|
|
6076
|
+
if (excludeCount <= capacity)
|
|
6077
|
+
return;
|
|
6078
|
+
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." });
|
|
6079
|
+
}
|
|
6080
|
+
function assertDeviceServesFactor(device) {
|
|
6081
|
+
if (!device.extensions.includes("hmac-secret")) {
|
|
6082
|
+
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." });
|
|
6083
|
+
}
|
|
6084
|
+
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6085
|
+
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." });
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
async function listSecurityKeys(deps, opts) {
|
|
6089
|
+
const location = await locateFido2Helper(deps);
|
|
6090
|
+
if (location.state === "absent")
|
|
6091
|
+
throw helperMissing(location);
|
|
6092
|
+
const info = await callHelper(deps, location.path, {
|
|
6093
|
+
op: "info",
|
|
6094
|
+
vaultId: opts.vaultId,
|
|
6095
|
+
envelopeId: opts.envelopeId,
|
|
6096
|
+
digest: base64.encode(operationDigest({ vaultId: opts.vaultId, envelopeId: opts.envelopeId, op: "info", nonce: b64u(randomBytes2(16)) }))
|
|
6097
|
+
});
|
|
6098
|
+
return { helperPath: location.path, info };
|
|
6099
|
+
}
|
|
6016
6100
|
function describeDeviceForList(device) {
|
|
6017
6101
|
const product = device.product || "security key";
|
|
6018
6102
|
return `--device ${device.deviceId} ${product}${device.manufacturer ? ` (${device.manufacturer})` : ""}${device.readable ? "" : " [not readable by this user]"}`;
|
|
@@ -6095,17 +6179,12 @@ async function openSecurityKeySession(deps, opts) {
|
|
|
6095
6179
|
deps.stderr.write(`Using the attached security key: ${device.product || "security key"} (--device ${device.deviceId})
|
|
6096
6180
|
`);
|
|
6097
6181
|
}
|
|
6098
|
-
if (opts.requireFeatures)
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
}
|
|
6102
|
-
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6103
|
-
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." });
|
|
6104
|
-
}
|
|
6105
|
-
}
|
|
6182
|
+
if (opts.requireFeatures)
|
|
6183
|
+
assertDeviceServesFactor(device);
|
|
6184
|
+
opts.beforePin?.(device, info);
|
|
6106
6185
|
const session = { helperPath: location.path, device, snapshotId: info.snapshotId };
|
|
6107
6186
|
if (device.options.clientPin === true) {
|
|
6108
|
-
const typed = await deps.promptSecret(`PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6187
|
+
const typed = await deps.promptSecret(opts.pinPrompt?.(device) ?? `PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6109
6188
|
if (typed === "") {
|
|
6110
6189
|
throw new VaultError("VAULT_PIN_REQUIRED", "This security key needs its PIN and none was typed; nothing was sent to it.");
|
|
6111
6190
|
}
|
|
@@ -6159,7 +6238,7 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6159
6238
|
if (opts.deviceFlag === undefined && allReadable && !uncertain)
|
|
6160
6239
|
state.set(envelopeId, "not attached");
|
|
6161
6240
|
}
|
|
6162
|
-
return { state, holder };
|
|
6241
|
+
return { state, holder, devices, credentials: credentialOf };
|
|
6163
6242
|
} catch {
|
|
6164
6243
|
return;
|
|
6165
6244
|
}
|
|
@@ -6167,20 +6246,37 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6167
6246
|
async function registerCredential(deps, session, opts) {
|
|
6168
6247
|
deps.stderr.write(`Touch ${session.device.product || "the security key"} to register the vault's credential on it.
|
|
6169
6248
|
`);
|
|
6170
|
-
const digest = operationDigest({
|
|
6171
|
-
const response = await callHelper(deps, session.helperPath, {
|
|
6172
|
-
op: "register",
|
|
6249
|
+
const digest = operationDigest({
|
|
6173
6250
|
vaultId: opts.vaultId,
|
|
6174
6251
|
envelopeId: opts.envelopeId,
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
expectSnapshot: session.snapshotId,
|
|
6178
|
-
rpId: RP_ID,
|
|
6179
|
-
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6180
|
-
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6181
|
-
clientDataHash: base64.encode(digest),
|
|
6182
|
-
...session.pin !== undefined ? { pin: session.pin } : {}
|
|
6252
|
+
op: "register",
|
|
6253
|
+
nonce: b64u(randomBytes2(16))
|
|
6183
6254
|
});
|
|
6255
|
+
const exclude = opts.excludeCredentialIds ?? [];
|
|
6256
|
+
let response;
|
|
6257
|
+
try {
|
|
6258
|
+
response = await callHelper(deps, session.helperPath, {
|
|
6259
|
+
op: "register",
|
|
6260
|
+
vaultId: opts.vaultId,
|
|
6261
|
+
envelopeId: opts.envelopeId,
|
|
6262
|
+
digest: base64.encode(digest),
|
|
6263
|
+
deviceId: session.device.deviceId,
|
|
6264
|
+
expectSnapshot: session.snapshotId,
|
|
6265
|
+
rpId: RP_ID,
|
|
6266
|
+
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6267
|
+
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6268
|
+
clientDataHash: base64.encode(digest),
|
|
6269
|
+
...session.pin !== undefined ? { pin: session.pin } : {},
|
|
6270
|
+
...exclude.length > 0 ? { excludeCredentialIds: exclude } : {}
|
|
6271
|
+
});
|
|
6272
|
+
} catch (error) {
|
|
6273
|
+
if (error instanceof VaultError && error.code === "VAULT_KEY_ALREADY_ENROLLED")
|
|
6274
|
+
throw alreadyEnrolled(session.device);
|
|
6275
|
+
throw error;
|
|
6276
|
+
}
|
|
6277
|
+
if (exclude.length > 0 && response.excluded !== exclude.length) {
|
|
6278
|
+
throw helperPredatesExcludeList(session.helperPath, true);
|
|
6279
|
+
}
|
|
6184
6280
|
const authData = base64.decode(response.authData);
|
|
6185
6281
|
assertAuthenticatorData(authData, RP_ID, "registration");
|
|
6186
6282
|
return {
|
|
@@ -6220,7 +6316,7 @@ async function assertPrf(deps, session, envelope, vaultId, purpose) {
|
|
|
6220
6316
|
}
|
|
6221
6317
|
return prfOutput;
|
|
6222
6318
|
}
|
|
6223
|
-
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.";
|
|
6319
|
+
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";
|
|
6224
6320
|
var init_fido2 = __esm(() => {
|
|
6225
6321
|
init_sha256();
|
|
6226
6322
|
init_esm();
|
|
@@ -8344,6 +8440,23 @@ function retryAfterMs(header, now) {
|
|
|
8344
8440
|
return;
|
|
8345
8441
|
return Math.max(0, at - now);
|
|
8346
8442
|
}
|
|
8443
|
+
function readProgramAccounts(method, entries, sliced) {
|
|
8444
|
+
const pubkeys = entries.map((entry) => {
|
|
8445
|
+
const pubkey = entry?.pubkey;
|
|
8446
|
+
if (typeof pubkey !== "string")
|
|
8447
|
+
throw new SolanaRpcError(`RPC ${method} answered an account without a pubkey`);
|
|
8448
|
+
return pubkey;
|
|
8449
|
+
});
|
|
8450
|
+
if (!sliced)
|
|
8451
|
+
return pubkeys;
|
|
8452
|
+
return entries.map((entry, i) => {
|
|
8453
|
+
const data = entry.account?.data;
|
|
8454
|
+
const encoded = Array.isArray(data) && data[1] === "base64" ? data[0] : undefined;
|
|
8455
|
+
if (typeof encoded !== "string")
|
|
8456
|
+
throw new SolanaRpcError(`RPC ${method} answered an account without base64 data`);
|
|
8457
|
+
return { pubkey: pubkeys[i], data: new Uint8Array(Buffer.from(encoded, "base64")) };
|
|
8458
|
+
});
|
|
8459
|
+
}
|
|
8347
8460
|
function rawAccountView(value, address) {
|
|
8348
8461
|
if (!value)
|
|
8349
8462
|
return null;
|
|
@@ -8488,28 +8601,31 @@ function createSolanaRpc(url, fetchFn) {
|
|
|
8488
8601
|
const r = await call("getSignaturesForAddress", [address, { limit: 1 }]);
|
|
8489
8602
|
return Array.isArray(r) && r.length > 0;
|
|
8490
8603
|
},
|
|
8491
|
-
async isBlockhashValid(blockhash) {
|
|
8492
|
-
const r = await call("isBlockhashValid", [blockhash, { commitment
|
|
8604
|
+
async isBlockhashValid(blockhash, commitment = "finalized") {
|
|
8605
|
+
const r = await call("isBlockhashValid", [blockhash, { commitment }]);
|
|
8493
8606
|
if (typeof r?.value !== "boolean")
|
|
8494
8607
|
throw new Error("isBlockhashValid answered with a non-boolean value");
|
|
8495
8608
|
return r.value;
|
|
8496
8609
|
},
|
|
8497
|
-
async
|
|
8498
|
-
const r = await call("getProgramAccounts", [
|
|
8610
|
+
getProgramAccounts: async (programId, filters, opts) => {
|
|
8611
|
+
const r = await call("getProgramAccounts", [
|
|
8612
|
+
programId,
|
|
8613
|
+
{
|
|
8614
|
+
encoding: "base64",
|
|
8615
|
+
commitment: "finalized",
|
|
8616
|
+
dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
|
|
8617
|
+
filters
|
|
8618
|
+
}
|
|
8619
|
+
], opts?.signal);
|
|
8499
8620
|
if (!Array.isArray(r))
|
|
8500
8621
|
throw new SolanaRpcError("RPC getProgramAccounts answered without an account list");
|
|
8501
|
-
return
|
|
8502
|
-
if (typeof entry?.pubkey !== "string") {
|
|
8503
|
-
throw new SolanaRpcError("RPC getProgramAccounts answered an account without a pubkey");
|
|
8504
|
-
}
|
|
8505
|
-
return entry.pubkey;
|
|
8506
|
-
});
|
|
8622
|
+
return readProgramAccounts("getProgramAccounts", r, opts?.dataSlice !== undefined);
|
|
8507
8623
|
},
|
|
8508
|
-
async
|
|
8624
|
+
getProgramAccountsV2: async (programId, filters, opts) => {
|
|
8509
8625
|
const config = {
|
|
8510
8626
|
encoding: "base64",
|
|
8511
8627
|
commitment: "finalized",
|
|
8512
|
-
dataSlice: { offset: 0, length: 0 },
|
|
8628
|
+
dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
|
|
8513
8629
|
filters,
|
|
8514
8630
|
limit: opts?.limit ?? PROGRAM_ACCOUNTS_V2_LIMIT
|
|
8515
8631
|
};
|
|
@@ -8520,19 +8636,17 @@ function createSolanaRpc(url, fetchFn) {
|
|
|
8520
8636
|
throw new SolanaRpcError("RPC getProgramAccountsV2 answered without an account list");
|
|
8521
8637
|
}
|
|
8522
8638
|
const page = r;
|
|
8523
|
-
const
|
|
8524
|
-
|
|
8525
|
-
throw new SolanaRpcError("RPC getProgramAccountsV2 answered an account without a pubkey");
|
|
8526
|
-
}
|
|
8527
|
-
return entry.pubkey;
|
|
8528
|
-
});
|
|
8639
|
+
const sliced = opts?.dataSlice !== undefined;
|
|
8640
|
+
const accounts = readProgramAccounts("getProgramAccountsV2", page.accounts, sliced);
|
|
8529
8641
|
const cursor = page.paginationKey;
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8642
|
+
let paginationKey = null;
|
|
8643
|
+
if (cursor !== null && cursor !== undefined) {
|
|
8644
|
+
if (typeof cursor !== "string" || cursor.length === 0) {
|
|
8645
|
+
throw new SolanaRpcError("RPC getProgramAccountsV2 answered a paginationKey that is not a string");
|
|
8646
|
+
}
|
|
8647
|
+
paginationKey = cursor;
|
|
8534
8648
|
}
|
|
8535
|
-
return {
|
|
8649
|
+
return sliced ? { accounts, paginationKey } : { pubkeys: accounts, paginationKey };
|
|
8536
8650
|
}
|
|
8537
8651
|
};
|
|
8538
8652
|
}
|
|
@@ -8647,7 +8761,7 @@ async function keysList(args, ctx) {
|
|
|
8647
8761
|
async function keysCreate(args, ctx) {
|
|
8648
8762
|
const { deps, apiUrl, json } = ctx;
|
|
8649
8763
|
const parsed = parseArgs(args, {
|
|
8650
|
-
valueFlags: ["--scopes", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8764
|
+
valueFlags: ["--scopes", "--access", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8651
8765
|
});
|
|
8652
8766
|
if ("error" in parsed) {
|
|
8653
8767
|
writeUsageFailure(deps, parsed.error, json);
|
|
@@ -8657,7 +8771,20 @@ async function keysCreate(args, ctx) {
|
|
|
8657
8771
|
writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
|
|
8658
8772
|
return 2;
|
|
8659
8773
|
}
|
|
8660
|
-
|
|
8774
|
+
if (parsed.values["--access"] !== undefined && parsed.values["--scopes"] !== undefined) {
|
|
8775
|
+
writeUsageFailure(deps, "--access and --scopes are mutually exclusive; pass one of them.", json);
|
|
8776
|
+
return 2;
|
|
8777
|
+
}
|
|
8778
|
+
let accessScopes;
|
|
8779
|
+
if (parsed.values["--access"] !== undefined) {
|
|
8780
|
+
const preset = ACCESS_LEVELS[parsed.values["--access"]];
|
|
8781
|
+
if (preset === undefined) {
|
|
8782
|
+
writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
|
|
8783
|
+
return 2;
|
|
8784
|
+
}
|
|
8785
|
+
accessScopes = scopesForPreset(preset);
|
|
8786
|
+
}
|
|
8787
|
+
const requestedScopes = accessScopes ?? (parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined);
|
|
8661
8788
|
const environment = parsed.values["--environment"];
|
|
8662
8789
|
const label = parsed.values["--label"]?.trim();
|
|
8663
8790
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > 64)) {
|
|
@@ -8753,7 +8880,7 @@ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError
|
|
|
8753
8880
|
`);
|
|
8754
8881
|
}
|
|
8755
8882
|
if (!requestedScopes) {
|
|
8756
|
-
deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8883
|
+
deps.stdout.write(`No --access or --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8757
8884
|
`);
|
|
8758
8885
|
}
|
|
8759
8886
|
if (storeError === undefined) {
|
|
@@ -8820,7 +8947,7 @@ async function keysRevoke(args, ctx) {
|
|
|
8820
8947
|
}
|
|
8821
8948
|
return 0;
|
|
8822
8949
|
}
|
|
8823
|
-
var KEYS_PATH = "/api/v1/agent/keys", NO_DEVICE_TOKEN;
|
|
8950
|
+
var KEYS_PATH = "/api/v1/agent/keys", ACCESS_LEVELS, NO_DEVICE_TOKEN;
|
|
8824
8951
|
var init_keys = __esm(() => {
|
|
8825
8952
|
init_agent_key_access();
|
|
8826
8953
|
init_args();
|
|
@@ -8828,6 +8955,11 @@ var init_keys = __esm(() => {
|
|
|
8828
8955
|
init_profiles();
|
|
8829
8956
|
init_render();
|
|
8830
8957
|
init_secret_store();
|
|
8958
|
+
ACCESS_LEVELS = {
|
|
8959
|
+
read: "read",
|
|
8960
|
+
"read-write": "readwrite",
|
|
8961
|
+
"read-write-transfer": "readwritetransfer"
|
|
8962
|
+
};
|
|
8831
8963
|
NO_DEVICE_TOKEN = {
|
|
8832
8964
|
code: "NO_DEVICE_TOKEN",
|
|
8833
8965
|
message: "No device token available.",
|
|
@@ -8837,13 +8969,13 @@ var init_keys = __esm(() => {
|
|
|
8837
8969
|
|
|
8838
8970
|
// src/vault/signer-roles.ts
|
|
8839
8971
|
function tokenMintFilters(key) {
|
|
8840
|
-
return [{ dataSize: 82 }, memcmp(
|
|
8972
|
+
return [{ dataSize: 82 }, memcmp(4, key)];
|
|
8841
8973
|
}
|
|
8842
8974
|
function tokenFreezeFilters(key) {
|
|
8843
8975
|
return [{ dataSize: 82 }, memcmp(46, concat2(COPTION_SOME_U32, key))];
|
|
8844
8976
|
}
|
|
8845
8977
|
function token2022MintFilters(key) {
|
|
8846
|
-
const authority = memcmp(
|
|
8978
|
+
const authority = memcmp(4, key);
|
|
8847
8979
|
return [
|
|
8848
8980
|
[{ dataSize: 82 }, authority],
|
|
8849
8981
|
[memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
|
|
@@ -8856,6 +8988,9 @@ function token2022FreezeFilters(key) {
|
|
|
8856
8988
|
[memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
|
|
8857
8989
|
];
|
|
8858
8990
|
}
|
|
8991
|
+
function keepSetAuthority(hits) {
|
|
8992
|
+
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);
|
|
8993
|
+
}
|
|
8859
8994
|
function programUpgradeFilters(key) {
|
|
8860
8995
|
return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat2(COPTION_SOME_U8, key))];
|
|
8861
8996
|
}
|
|
@@ -8950,19 +9085,31 @@ async function readSignerRoles(rpc, addresses, opts) {
|
|
|
8950
9085
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(Object.assign(new Error("timed out"), { name: "TimeoutError" })), timeoutMs) : undefined;
|
|
8951
9086
|
try {
|
|
8952
9087
|
requests += 1;
|
|
9088
|
+
const tagSlice = task.group.tagSlice;
|
|
8953
9089
|
if (task.via === "v2") {
|
|
8954
9090
|
if (rpc.getProgramAccountsV2 === undefined) {
|
|
8955
9091
|
throw new SolanaRpcError("RPC getProgramAccountsV2 failed: -32601 Method not found", {
|
|
8956
9092
|
rpcCode: RPC_METHOD_NOT_FOUND
|
|
8957
9093
|
});
|
|
8958
9094
|
}
|
|
8959
|
-
|
|
9095
|
+
const pageOpts = {
|
|
8960
9096
|
signal: controller.signal,
|
|
8961
9097
|
...task.paginationKey !== undefined ? { paginationKey: task.paginationKey } : {}
|
|
8962
|
-
}
|
|
9098
|
+
};
|
|
9099
|
+
if (tagSlice === undefined)
|
|
9100
|
+
return await rpc.getProgramAccountsV2(task.group.programId, filters, pageOpts);
|
|
9101
|
+
const page = await rpc.getProgramAccountsV2(task.group.programId, filters, { ...pageOpts, dataSlice: tagSlice });
|
|
9102
|
+
return { pubkeys: keepSetAuthority(page.accounts), paginationKey: page.paginationKey };
|
|
8963
9103
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
9104
|
+
if (tagSlice === undefined) {
|
|
9105
|
+
const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
|
|
9106
|
+
return { pubkeys, paginationKey: null };
|
|
9107
|
+
}
|
|
9108
|
+
const hits = await rpc.getProgramAccounts(task.group.programId, filters, {
|
|
9109
|
+
signal: controller.signal,
|
|
9110
|
+
dataSlice: tagSlice
|
|
9111
|
+
});
|
|
9112
|
+
return { pubkeys: keepSetAuthority(hits), paginationKey: null };
|
|
8966
9113
|
} finally {
|
|
8967
9114
|
if (timer !== undefined)
|
|
8968
9115
|
clearTimeout(timer);
|
|
@@ -9205,7 +9352,7 @@ var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", S
|
|
|
9205
9352
|
return out;
|
|
9206
9353
|
}, COPTION_SOME_U32, COPTION_SOME_U8, LOADER_PROGRAM_DATA_TAG, LOADER_PROGRAM_TAG, TOKEN_2022_MINT_ACCOUNT_TYPE, memcmp = (offset, bytes) => ({
|
|
9207
9354
|
memcmp: { offset, bytes: b642(bytes), encoding: "base64" }
|
|
9208
|
-
}), ROLE_GROUPS, PROGRAM_LABELS, NOT_CHECKED_LINE = "Not checked: multisig membership, Token-2022 extension authorities, metadata update authority.", thousands = (n) => n.toLocaleString("en-US");
|
|
9355
|
+
}), MINT_AUTHORITY_TAG_SLICE, ROLE_GROUPS, PROGRAM_LABELS, NOT_CHECKED_LINE = "Not checked: multisig membership, Token-2022 extension authorities, metadata update authority.", thousands = (n) => n.toLocaleString("en-US");
|
|
9209
9356
|
var init_signer_roles = __esm(() => {
|
|
9210
9357
|
init_esm();
|
|
9211
9358
|
init_solana_lite();
|
|
@@ -9229,6 +9376,7 @@ var init_signer_roles = __esm(() => {
|
|
|
9229
9376
|
LOADER_PROGRAM_DATA_TAG = new Uint8Array([3, 0, 0, 0]);
|
|
9230
9377
|
LOADER_PROGRAM_TAG = new Uint8Array([2, 0, 0, 0]);
|
|
9231
9378
|
TOKEN_2022_MINT_ACCOUNT_TYPE = new Uint8Array([1]);
|
|
9379
|
+
MINT_AUTHORITY_TAG_SLICE = { offset: 0, length: 4 };
|
|
9232
9380
|
ROLE_GROUPS = [
|
|
9233
9381
|
{
|
|
9234
9382
|
id: "token-mint",
|
|
@@ -9236,7 +9384,8 @@ var init_signer_roles = __esm(() => {
|
|
|
9236
9384
|
program: "token",
|
|
9237
9385
|
programId: TOKEN_PROGRAM_ID,
|
|
9238
9386
|
label: "token mint",
|
|
9239
|
-
shapes: (key) => [tokenMintFilters(key)]
|
|
9387
|
+
shapes: (key) => [tokenMintFilters(key)],
|
|
9388
|
+
tagSlice: MINT_AUTHORITY_TAG_SLICE
|
|
9240
9389
|
},
|
|
9241
9390
|
{
|
|
9242
9391
|
id: "token-freeze",
|
|
@@ -9252,7 +9401,8 @@ var init_signer_roles = __esm(() => {
|
|
|
9252
9401
|
program: "token-2022",
|
|
9253
9402
|
programId: TOKEN_2022_PROGRAM_ID,
|
|
9254
9403
|
label: "Token-2022 mint",
|
|
9255
|
-
shapes: token2022MintFilters
|
|
9404
|
+
shapes: token2022MintFilters,
|
|
9405
|
+
tagSlice: MINT_AUTHORITY_TAG_SLICE
|
|
9256
9406
|
},
|
|
9257
9407
|
{
|
|
9258
9408
|
id: "token2022-freeze",
|
|
@@ -9298,6 +9448,7 @@ var init_signer_roles = __esm(() => {
|
|
|
9298
9448
|
// src/vault/promote-support.ts
|
|
9299
9449
|
var exports_promote_support = {};
|
|
9300
9450
|
__export(exports_promote_support, {
|
|
9451
|
+
withToKey: () => withToKey,
|
|
9301
9452
|
subjectPhrase: () => subjectPhrase,
|
|
9302
9453
|
shortAddress: () => shortAddress,
|
|
9303
9454
|
runRoleCheck: () => runRoleCheck,
|
|
@@ -9307,6 +9458,7 @@ __export(exports_promote_support, {
|
|
|
9307
9458
|
promoteSentence: () => promoteSentence,
|
|
9308
9459
|
printPromoteSentence: () => printPromoteSentence,
|
|
9309
9460
|
findVaultRoleEntry: () => findVaultRoleEntry,
|
|
9461
|
+
findTransferSource: () => findTransferSource,
|
|
9310
9462
|
findEntryByLabelOrAddress: () => findEntryByLabelOrAddress,
|
|
9311
9463
|
controlledByJson: () => controlledByJson,
|
|
9312
9464
|
confirmPrompt: () => confirmPrompt,
|
|
@@ -9352,6 +9504,10 @@ function findVaultRoleEntry(index, labelOrAddress) {
|
|
|
9352
9504
|
return byLabel;
|
|
9353
9505
|
return index.entries.find((entry) => entry.address === labelOrAddress);
|
|
9354
9506
|
}
|
|
9507
|
+
function findTransferSource(index, labelOrAddress) {
|
|
9508
|
+
const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
|
|
9509
|
+
return byLabel("vault") ?? byLabel("tee-wallet") ?? index.entries.find((entry) => entry.address === labelOrAddress);
|
|
9510
|
+
}
|
|
9355
9511
|
function findEntryByLabelOrAddress(index, labelOrAddress) {
|
|
9356
9512
|
const byLabel = index.entries.find((entry) => entry.label !== undefined && entry.label === labelOrAddress);
|
|
9357
9513
|
if (byLabel !== undefined)
|
|
@@ -9472,6 +9628,9 @@ async function confirmPromotion(ctx, n) {
|
|
|
9472
9628
|
function shortAddress(address) {
|
|
9473
9629
|
return `${address.slice(0, 6)}…${address.slice(-4)}`;
|
|
9474
9630
|
}
|
|
9631
|
+
function withToKey(controlledBy, toKey) {
|
|
9632
|
+
return { ...controlledBy, toKey };
|
|
9633
|
+
}
|
|
9475
9634
|
function accountUnresolved(reason) {
|
|
9476
9635
|
return new VaultError("PROMOTE_ACCOUNT_UNRESOLVED", `Could not confirm which Candle account this API key acts for (${reason}); nothing was written.`, {
|
|
9477
9636
|
suggestion: "Check the key with: candle doctor. Promotion registers the keys to that account, so it does not proceed on a cached value."
|
|
@@ -9545,23 +9704,51 @@ function renderControlledBy(controlledBy, n) {
|
|
|
9545
9704
|
const label = cleaned.length > 0 ? `(${cleaned}) ` : "";
|
|
9546
9705
|
const environment = controlledBy.environment ?? "not a Candle host";
|
|
9547
9706
|
const from = controlledBy.apiUrlFrom !== undefined ? `, from ${controlledBy.apiUrlFrom}` : "";
|
|
9707
|
+
const { toKey } = controlledBy;
|
|
9708
|
+
if (toKey === undefined) {
|
|
9709
|
+
return [
|
|
9710
|
+
`${subject} will be controlled by:`,
|
|
9711
|
+
` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
|
|
9712
|
+
` API key ${controlledBy.keyPrefix}… ${label}${source}`,
|
|
9713
|
+
` API ${controlledBy.apiUrl} (${environment}${from})`
|
|
9714
|
+
].join(`
|
|
9715
|
+
`);
|
|
9716
|
+
}
|
|
9717
|
+
const toLabel = toKey.label !== null && labelCell(toKey.label).length > 0 ? `(${labelCell(toKey.label)}) ` : "";
|
|
9548
9718
|
return [
|
|
9549
9719
|
`${subject} will be controlled by:`,
|
|
9550
9720
|
` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
|
|
9551
|
-
` API key ${
|
|
9552
|
-
`
|
|
9721
|
+
` API key ${toKey.keyPrefix}… ${toLabel}--to-key, bound by a rebind after the import`,
|
|
9722
|
+
` imported under ${controlledBy.keyPrefix}… ${label}${source}`,
|
|
9723
|
+
` API ${controlledBy.apiUrl} (${environment}${from})`,
|
|
9724
|
+
...toKey.warnings
|
|
9553
9725
|
].join(`
|
|
9554
9726
|
`);
|
|
9555
9727
|
}
|
|
9556
9728
|
function controlledByJson(controlledBy) {
|
|
9729
|
+
const importedUnder = {
|
|
9730
|
+
keyPrefix: controlledBy.keyPrefix,
|
|
9731
|
+
keyLabel: controlledBy.keyLabel,
|
|
9732
|
+
keySource: controlledBy.keySource
|
|
9733
|
+
};
|
|
9734
|
+
if (controlledBy.toKey === undefined) {
|
|
9735
|
+
return {
|
|
9736
|
+
account: controlledBy.account,
|
|
9737
|
+
username: controlledBy.username,
|
|
9738
|
+
...importedUnder,
|
|
9739
|
+
apiUrl: controlledBy.apiUrl,
|
|
9740
|
+
environment: controlledBy.environment
|
|
9741
|
+
};
|
|
9742
|
+
}
|
|
9557
9743
|
return {
|
|
9558
9744
|
account: controlledBy.account,
|
|
9559
9745
|
username: controlledBy.username,
|
|
9560
|
-
keyPrefix: controlledBy.keyPrefix,
|
|
9561
|
-
keyLabel: controlledBy.
|
|
9562
|
-
keySource:
|
|
9746
|
+
keyPrefix: controlledBy.toKey.keyPrefix,
|
|
9747
|
+
keyLabel: controlledBy.toKey.label,
|
|
9748
|
+
keySource: "--to-key",
|
|
9563
9749
|
apiUrl: controlledBy.apiUrl,
|
|
9564
|
-
environment: controlledBy.environment
|
|
9750
|
+
environment: controlledBy.environment,
|
|
9751
|
+
importedUnder
|
|
9565
9752
|
};
|
|
9566
9753
|
}
|
|
9567
9754
|
async function runRoleCheck(ctx, rpc, addresses) {
|
|
@@ -13625,16 +13812,18 @@ async function request(ctx, key, path, body) {
|
|
|
13625
13812
|
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
13626
13813
|
return result.body;
|
|
13627
13814
|
}
|
|
13628
|
-
async function
|
|
13815
|
+
async function listTradingWallets(ctx, key, scope) {
|
|
13629
13816
|
let cursor;
|
|
13630
13817
|
const rows = [];
|
|
13631
13818
|
const cursors = new Set;
|
|
13632
13819
|
let appId = "";
|
|
13820
|
+
let scopes = [];
|
|
13633
13821
|
for (;; ) {
|
|
13634
13822
|
const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
|
|
13635
|
-
if (!response.scopes?.includes(scope))
|
|
13823
|
+
if (scope !== undefined && !response.scopes?.includes(scope))
|
|
13636
13824
|
throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
|
|
13637
13825
|
appId = response.privyAppId ?? "";
|
|
13826
|
+
scopes = response.scopes;
|
|
13638
13827
|
if (!Array.isArray(response.page))
|
|
13639
13828
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
13640
13829
|
rows.push(...response.page);
|
|
@@ -13645,7 +13834,7 @@ async function teeWallets(ctx, key, scope) {
|
|
|
13645
13834
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
|
|
13646
13835
|
cursors.add(cursor);
|
|
13647
13836
|
}
|
|
13648
|
-
return { rows, appId };
|
|
13837
|
+
return { rows, appId, scopes };
|
|
13649
13838
|
}
|
|
13650
13839
|
function matchesName(row, name) {
|
|
13651
13840
|
return row.id === name || row.address === name || row.label === name;
|
|
@@ -13675,32 +13864,31 @@ async function completeTradingWallet(ctx, row, appId, scope) {
|
|
|
13675
13864
|
};
|
|
13676
13865
|
}
|
|
13677
13866
|
async function tradingWallet(ctx, key, name, scope) {
|
|
13678
|
-
const { rows, appId } = await
|
|
13867
|
+
const { rows, appId } = await listTradingWallets(ctx, key, scope);
|
|
13679
13868
|
const matches = rows.filter((row) => matchesName(row, name));
|
|
13680
13869
|
if (matches.length !== 1) {
|
|
13681
13870
|
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.` : rows.length === 0 ? "This key has no TEE wallets bound to it. Enrol one, or select the profile whose key holds it." : `No TEE wallet on this key is called "${name}". Bound to this key: ${teeWalletList(rows)}.`);
|
|
13682
13871
|
}
|
|
13683
13872
|
return await completeTradingWallet(ctx, matches[0], appId, scope);
|
|
13684
13873
|
}
|
|
13685
|
-
async function tradingPayer(ctx, key, name) {
|
|
13686
|
-
const
|
|
13687
|
-
const { rows, appId } = await teeWallets(ctx, key, scope);
|
|
13874
|
+
async function tradingPayer(ctx, key, name, scope = "swap:write") {
|
|
13875
|
+
const { rows, appId, scopes } = await listTradingWallets(ctx, key, scope);
|
|
13688
13876
|
const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
|
|
13689
|
-
const asEmbedded = () => ({ kind: "embedded", address: embedded });
|
|
13877
|
+
const asEmbedded = () => ({ kind: "embedded", address: embedded, scopes });
|
|
13690
13878
|
const options = [
|
|
13691
13879
|
...rows.map((row) => `TEE ${describeWallet(row)}`),
|
|
13692
13880
|
...embedded ? [`embedded (${embedded})`] : []
|
|
13693
13881
|
].join("; ");
|
|
13694
13882
|
if (name === undefined) {
|
|
13695
13883
|
if (rows.length === 1 && !embedded)
|
|
13696
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
|
|
13884
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope), scopes };
|
|
13697
13885
|
if (rows.length === 0 && embedded)
|
|
13698
13886
|
return asEmbedded();
|
|
13699
13887
|
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}.`);
|
|
13700
13888
|
}
|
|
13701
13889
|
const matches = rows.filter((row) => matchesName(row, name));
|
|
13702
13890
|
if (matches.length === 1)
|
|
13703
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
|
|
13891
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope), scopes };
|
|
13704
13892
|
if (matches.length === 0 && embedded === name)
|
|
13705
13893
|
return asEmbedded();
|
|
13706
13894
|
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}.`);
|
|
@@ -13808,7 +13996,7 @@ Minimum received: ${safeText(quote.minimumReceived)}
|
|
|
13808
13996
|
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary trade prompt.");
|
|
13809
13997
|
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
13810
13998
|
}
|
|
13811
|
-
async function
|
|
13999
|
+
async function saveOperationSignature(ctx, key, id, kind, transaction) {
|
|
13812
14000
|
const bytes = Buffer.from(transaction, "base64");
|
|
13813
14001
|
let offset = 0;
|
|
13814
14002
|
let count = 0;
|
|
@@ -13825,7 +14013,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
|
|
|
13825
14013
|
const signature = base58.encode(bytes.subarray(offset, offset + 64));
|
|
13826
14014
|
const path = operationPath(ctx, key, id);
|
|
13827
14015
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
13828
|
-
await writeFile4(temporary, JSON.stringify({ id, kind
|
|
14016
|
+
await writeFile4(temporary, JSON.stringify({ id, kind, signature }), { mode: 384 });
|
|
13829
14017
|
const file = await open3(temporary, "r");
|
|
13830
14018
|
try {
|
|
13831
14019
|
await file.sync();
|
|
@@ -13835,7 +14023,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
|
|
|
13835
14023
|
await rename3(temporary, path);
|
|
13836
14024
|
return signature;
|
|
13837
14025
|
}
|
|
13838
|
-
var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, BASES;
|
|
14026
|
+
var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, lpAmountSchema, lpBuildSchema, lpPositionsSchema, lpPoolsSchema, BASES;
|
|
13839
14027
|
var init_trading = __esm(() => {
|
|
13840
14028
|
init_esm();
|
|
13841
14029
|
init_zod();
|
|
@@ -13897,6 +14085,51 @@ var init_trading = __esm(() => {
|
|
|
13897
14085
|
wallets: exports_external.object({ solana: exports_external.object({ address: exports_external.string() }).passthrough().nullable().optional() }).passthrough().optional()
|
|
13898
14086
|
}).passthrough();
|
|
13899
14087
|
operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
|
|
14088
|
+
lpAmountSchema = exports_external.object({ mint: exports_external.string(), raw: exports_external.string().regex(/^\d+$/), decimals: exports_external.number().int() });
|
|
14089
|
+
lpBuildSchema = exports_external.object({
|
|
14090
|
+
build: exports_external.object({
|
|
14091
|
+
action: exports_external.enum(["add", "remove", "claim"]),
|
|
14092
|
+
pool: exports_external.string(),
|
|
14093
|
+
position: exports_external.string(),
|
|
14094
|
+
transaction: exports_external.string().min(1),
|
|
14095
|
+
walletAddress: exports_external.string(),
|
|
14096
|
+
signature: exports_external.string().optional()
|
|
14097
|
+
}).passthrough(),
|
|
14098
|
+
preview: exports_external.object({
|
|
14099
|
+
amounts: exports_external.array(lpAmountSchema).default([]),
|
|
14100
|
+
tokenRisks: risksSchema.default([]),
|
|
14101
|
+
warnings: exports_external.array(exports_external.string()).default([]),
|
|
14102
|
+
candleFeeBps: exports_external.number().optional()
|
|
14103
|
+
}).passthrough().nullable(),
|
|
14104
|
+
replay: exports_external.boolean().optional()
|
|
14105
|
+
});
|
|
14106
|
+
lpPositionsSchema = exports_external.object({
|
|
14107
|
+
positions: exports_external.array(exports_external.object({
|
|
14108
|
+
position: exports_external.string(),
|
|
14109
|
+
pool: exports_external.string(),
|
|
14110
|
+
tokens: exports_external.array(exports_external.object({
|
|
14111
|
+
mint: exports_external.string(),
|
|
14112
|
+
decimals: exports_external.number().int(),
|
|
14113
|
+
amountRaw: exports_external.string(),
|
|
14114
|
+
unclaimedFeesRaw: exports_external.string(),
|
|
14115
|
+
valueUsd: exports_external.number().nullable().optional()
|
|
14116
|
+
}).passthrough()),
|
|
14117
|
+
poolShare: exports_external.number().optional(),
|
|
14118
|
+
valueUsd: exports_external.number().nullable().optional()
|
|
14119
|
+
}).passthrough())
|
|
14120
|
+
});
|
|
14121
|
+
lpPoolsSchema = exports_external.object({
|
|
14122
|
+
page: exports_external.number().int(),
|
|
14123
|
+
pages: exports_external.number().int(),
|
|
14124
|
+
pools: exports_external.array(exports_external.object({
|
|
14125
|
+
pool: exports_external.string(),
|
|
14126
|
+
tokens: exports_external.array(exports_external.string()),
|
|
14127
|
+
liquidityUsd: exports_external.number().nullable().optional(),
|
|
14128
|
+
baseFeePercent: exports_external.number().nullable().optional(),
|
|
14129
|
+
volume24hUsd: exports_external.number().nullable().optional(),
|
|
14130
|
+
estimatedAprPercent: exports_external.number().nullable().optional()
|
|
14131
|
+
}).passthrough())
|
|
14132
|
+
});
|
|
13900
14133
|
BASES = {
|
|
13901
14134
|
SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
|
|
13902
14135
|
USDC: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
|
|
@@ -22057,6 +22290,8 @@ __export(exports_vault_support, {
|
|
|
22057
22290
|
menuSafeText: () => menuSafeText,
|
|
22058
22291
|
findExternalEntry: () => findExternalEntry,
|
|
22059
22292
|
factorMenu: () => factorMenu,
|
|
22293
|
+
factorAddPassphraseOnlyReason: () => factorAddPassphraseOnlyReason,
|
|
22294
|
+
factorAddAmong: () => factorAddAmong,
|
|
22060
22295
|
describeRole: () => describeRole,
|
|
22061
22296
|
describeEntry: () => describeEntry,
|
|
22062
22297
|
derivationNotice: () => derivationNotice,
|
|
@@ -22155,6 +22390,20 @@ async function requireVaultRaw(ctx, resolved) {
|
|
|
22155
22390
|
throw missingVault(ctx, resolved);
|
|
22156
22391
|
return raw;
|
|
22157
22392
|
}
|
|
22393
|
+
function factorAddAmong(envelopes) {
|
|
22394
|
+
return {
|
|
22395
|
+
envelopeIds: envelopes.filter((envelope) => isPassphraseEnvelope(envelope) || isCtap2Envelope(envelope)).map((envelope) => envelope.id),
|
|
22396
|
+
because: factorAddPassphraseOnlyReason,
|
|
22397
|
+
excludedBecause: "adding a factor opens the vault with the passphrase or a security key."
|
|
22398
|
+
};
|
|
22399
|
+
}
|
|
22400
|
+
function factorAddPassphraseOnlyReason(unusable) {
|
|
22401
|
+
if (unusable.length === 0) {
|
|
22402
|
+
return "adding a factor opens the vault with the passphrase or a security key, and this vault has no security key.";
|
|
22403
|
+
}
|
|
22404
|
+
const keys = unusable.map((entry) => `${entry.word} ${entry.id} cannot be used on this machine: ${entry.availability}`);
|
|
22405
|
+
return `adding a factor opens the vault with the passphrase or a security key, and ${keys.join("; ")}.`;
|
|
22406
|
+
}
|
|
22158
22407
|
function derivationNotice(line, purpose) {
|
|
22159
22408
|
if (purpose === undefined)
|
|
22160
22409
|
return line;
|
|
@@ -22181,6 +22430,9 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22181
22430
|
const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
|
|
22182
22431
|
const notice = noticeFor(opts.purpose);
|
|
22183
22432
|
if (choice.kind === "passphrase") {
|
|
22433
|
+
if (opts.onFactorChosen) {
|
|
22434
|
+
await opts.onFactorChosen(choice.envelopeId !== undefined ? { kind: "passphrase", envelopeId: choice.envelopeId } : { kind: "passphrase" });
|
|
22435
|
+
}
|
|
22184
22436
|
if (choice.onlyBecause !== undefined)
|
|
22185
22437
|
deps.stderr.write(`${PASSPHRASE_ONLY_PREFIX}${choice.onlyBecause}
|
|
22186
22438
|
`);
|
|
@@ -22202,6 +22454,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22202
22454
|
}
|
|
22203
22455
|
if (choice.kind === "touch-id") {
|
|
22204
22456
|
const envelope2 = choice.envelope;
|
|
22457
|
+
if (opts.onFactorChosen)
|
|
22458
|
+
await opts.onFactorChosen({ kind: "touch-id", envelope: envelope2 });
|
|
22205
22459
|
const session2 = await openEnclaveSession(deps, envelope2.helper);
|
|
22206
22460
|
const open5 = async (p, r, reason2) => {
|
|
22207
22461
|
const current = parseVaultFile(r);
|
|
@@ -22230,6 +22484,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22230
22484
|
}
|
|
22231
22485
|
if (choice.kind === "passkey") {
|
|
22232
22486
|
const envelope2 = choice.envelope;
|
|
22487
|
+
if (opts.onFactorChosen)
|
|
22488
|
+
await opts.onFactorChosen({ kind: "passkey", envelope: envelope2 });
|
|
22233
22489
|
const session2 = await openPasskeySession(deps, envelope2.helper);
|
|
22234
22490
|
const open5 = async (p, r, purpose) => {
|
|
22235
22491
|
const current = parseVaultFile(r);
|
|
@@ -22257,11 +22513,14 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22257
22513
|
};
|
|
22258
22514
|
}
|
|
22259
22515
|
const envelope = choice.envelope;
|
|
22516
|
+
const keyDevice = opts.onFactorChosen ? await opts.onFactorChosen({ kind: "security-key", envelope }) : undefined;
|
|
22260
22517
|
const session = await openSecurityKeySession(deps, {
|
|
22261
22518
|
vaultId: file.vaultId,
|
|
22262
22519
|
envelopeId: envelope.id,
|
|
22263
|
-
|
|
22264
|
-
|
|
22520
|
+
...keyDevice !== undefined ? { preferDevice: keyDevice.preferDevice } : {
|
|
22521
|
+
deviceFlag: ctx.vaultDevice,
|
|
22522
|
+
...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {}
|
|
22523
|
+
},
|
|
22265
22524
|
requireFeatures: false
|
|
22266
22525
|
});
|
|
22267
22526
|
const open4 = async (p, r) => {
|
|
@@ -22355,7 +22614,7 @@ async function chooseFactor(ctx, vaultId, allEnvelopes, facts, requested, opts =
|
|
|
22355
22614
|
}
|
|
22356
22615
|
if (passphrases.length === 0 && drivable.length === 1)
|
|
22357
22616
|
return choiceFor(drivable[0]);
|
|
22358
|
-
const attached = ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22617
|
+
const attached = "attached" in opts ? opts.attached : ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22359
22618
|
const notUsableHere = envelopes.filter((envelope) => !isPassphraseEnvelope(envelope) && !drivable.includes(envelope));
|
|
22360
22619
|
const menu = factorMenu({
|
|
22361
22620
|
keys: drivableKeys,
|
|
@@ -37604,7 +37863,7 @@ var init_update_notice = __esm(() => {
|
|
|
37604
37863
|
});
|
|
37605
37864
|
|
|
37606
37865
|
// ../mcp/src/orchestrate.ts
|
|
37607
|
-
import { randomUUID as
|
|
37866
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
37608
37867
|
function requireApiKey(cfg) {
|
|
37609
37868
|
if (!cfg.apiKey) {
|
|
37610
37869
|
throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
|
|
@@ -37716,7 +37975,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
|
|
|
37716
37975
|
}
|
|
37717
37976
|
async function executeTrade(args, cfg, doFetch) {
|
|
37718
37977
|
const apiKey = requireApiKey(cfg);
|
|
37719
|
-
const clientTradeId = args.clientTradeId ??
|
|
37978
|
+
const clientTradeId = args.clientTradeId ?? randomUUID4();
|
|
37720
37979
|
if (args.amount !== undefined && args.percent !== undefined) {
|
|
37721
37980
|
return errText("pass exactly one of amount or percent, not both", { clientTradeId });
|
|
37722
37981
|
}
|
|
@@ -37854,7 +38113,7 @@ async function executeTrade(args, cfg, doFetch) {
|
|
|
37854
38113
|
}
|
|
37855
38114
|
async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
37856
38115
|
const apiKey = requireApiKey(cfg);
|
|
37857
|
-
const clientLaunchId = args.clientLaunchId ??
|
|
38116
|
+
const clientLaunchId = args.clientLaunchId ?? randomUUID4();
|
|
37858
38117
|
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...launchFields } = args;
|
|
37859
38118
|
let buyAmount;
|
|
37860
38119
|
if (devBuy !== undefined) {
|
|
@@ -38263,8 +38522,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
38263
38522
|
inputSchema: setProfileWalletsShape
|
|
38264
38523
|
}, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
|
|
38265
38524
|
register("candle_get_profile_pnl", {
|
|
38266
|
-
title: "Read an agent profile's
|
|
38267
|
-
description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their
|
|
38525
|
+
title: "Read an agent profile's P&L",
|
|
38526
|
+
description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits, withdrawals and " + "transfers are excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before " + "quoting the number; they mean the total is partial. Reads only.",
|
|
38268
38527
|
inputSchema: profilePnlShape
|
|
38269
38528
|
}, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
|
|
38270
38529
|
register("candle_get_profile_trades", {
|
|
@@ -38593,7 +38852,7 @@ init_render();
|
|
|
38593
38852
|
init_secret_store();
|
|
38594
38853
|
|
|
38595
38854
|
// src/version.ts
|
|
38596
|
-
var CLI_VERSION = "0.11.
|
|
38855
|
+
var CLI_VERSION = "0.11.8";
|
|
38597
38856
|
|
|
38598
38857
|
// src/commands/auth.ts
|
|
38599
38858
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -38615,6 +38874,10 @@ async function authLogin(args, ctx) {
|
|
|
38615
38874
|
return 2;
|
|
38616
38875
|
}
|
|
38617
38876
|
const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
|
|
38877
|
+
if (scopes?.includes("transfer:bound")) {
|
|
38878
|
+
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);
|
|
38879
|
+
return 2;
|
|
38880
|
+
}
|
|
38618
38881
|
const label = parsed.values["--label"]?.trim();
|
|
38619
38882
|
const noBrowser = parsed.booleans.has("--no-browser");
|
|
38620
38883
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > MAX_CLIENT_NAME_LENGTH)) {
|
|
@@ -39092,6 +39355,71 @@ var HELP = {
|
|
|
39092
39355
|
],
|
|
39093
39356
|
env: ENV_API
|
|
39094
39357
|
},
|
|
39358
|
+
lp: {
|
|
39359
|
+
group: "Trade",
|
|
39360
|
+
summary: "Meteora DAMM v2 liquidity from a TEE wallet: pools, add, positions, remove, claim",
|
|
39361
|
+
description: "Provide liquidity on any Meteora DAMM v2 pool from a TEE wallet, through its bound key (scope lp:write, opt-in). Every write is quoted, shown with the pool's Token-2022 warnings, confirmed, signed by Candle's relay and broadcast over your RPC; no Candle fee. A sweep closes open positions itself (tee sweep).",
|
|
39362
|
+
usage: ["candle lp <subcommand> [flags]"],
|
|
39363
|
+
rows: [
|
|
39364
|
+
{
|
|
39365
|
+
invocation: "pools <token> [--page <n>]",
|
|
39366
|
+
description: "Pools listing a token: liquidity, fee, 24h volume, estimated yield"
|
|
39367
|
+
},
|
|
39368
|
+
{
|
|
39369
|
+
invocation: "add <pool> --amount <n> <token> --wallet <tee> [--position <nft>]",
|
|
39370
|
+
description: "Open a position, or add to one; the other side follows the pool's ratio"
|
|
39371
|
+
},
|
|
39372
|
+
{ invocation: "positions [--wallet <tee>]", description: "Every position across your TEE wallets" },
|
|
39373
|
+
{
|
|
39374
|
+
invocation: "remove <position> --percent <n> [--wallet <tee>]",
|
|
39375
|
+
description: "Withdraw; at 100% also claims fees and closes the position"
|
|
39376
|
+
},
|
|
39377
|
+
{ invocation: "claim <position> [--wallet <tee>]", description: "Claim fees and any rewards" }
|
|
39378
|
+
],
|
|
39379
|
+
flags: [
|
|
39380
|
+
{ invocation: "--wallet <tee>", description: "The TEE wallet by id, address or unique label" },
|
|
39381
|
+
{ invocation: "--rpc-url <url>", description: "Your Solana RPC, for mint reads and the broadcast" },
|
|
39382
|
+
{
|
|
39383
|
+
invocation: "--client-trade-id <id>",
|
|
39384
|
+
description: "Idempotency: the same id never deposits or withdraws twice"
|
|
39385
|
+
},
|
|
39386
|
+
{ invocation: "--slippage-bps <n>", description: "add and remove: the tolerance, 0 to 1000 (default 100)" },
|
|
39387
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt (an ordinary prompt only)" }
|
|
39388
|
+
],
|
|
39389
|
+
examples: [
|
|
39390
|
+
"candle lp pools So11111111111111111111111111111111111111112",
|
|
39391
|
+
"candle lp add <pool> --amount 0.5 SOL --wallet AgentOne --rpc-url https://<rpc>",
|
|
39392
|
+
"candle lp positions",
|
|
39393
|
+
"candle lp remove <position> --percent 100 --wallet AgentOne --rpc-url https://<rpc>"
|
|
39394
|
+
],
|
|
39395
|
+
env: ENV_API
|
|
39396
|
+
},
|
|
39397
|
+
transfer: {
|
|
39398
|
+
group: "Trade",
|
|
39399
|
+
summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
|
|
39400
|
+
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.",
|
|
39401
|
+
usage: [
|
|
39402
|
+
"candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
|
|
39403
|
+
],
|
|
39404
|
+
rows: [],
|
|
39405
|
+
flags: [
|
|
39406
|
+
{
|
|
39407
|
+
invocation: "--to <address|wallet name|vault>",
|
|
39408
|
+
description: "Where the funds go: vault is the wallet's own pin"
|
|
39409
|
+
},
|
|
39410
|
+
{ invocation: "--asset <SOL|USDC|CNDL>", description: "A base asset (required for a linked-wallet destination)" },
|
|
39411
|
+
{ invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
|
|
39412
|
+
{ invocation: "--amount <decimal|max>", description: "How much, or max for the whole spendable balance" },
|
|
39413
|
+
{ invocation: "--wallet <tee>", description: "The TEE wallet the funds leave; optional with one payer" },
|
|
39414
|
+
{ invocation: "--rpc-url <url>", description: "Your own Solana RPC, to read a --mint's decimals" },
|
|
39415
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt (the destination is still printed)" }
|
|
39416
|
+
],
|
|
39417
|
+
examples: [
|
|
39418
|
+
"candle transfer --to vault --asset SOL --amount max --wallet AgentOne",
|
|
39419
|
+
"candle transfer --to treasury --asset USDC --amount 250 --wallet AgentOne --yes --json"
|
|
39420
|
+
],
|
|
39421
|
+
env: ENV_API
|
|
39422
|
+
},
|
|
39095
39423
|
launch: {
|
|
39096
39424
|
group: "Trade",
|
|
39097
39425
|
summary: "Create a Solana token (the first buy is a separate swap)",
|
|
@@ -39107,6 +39435,31 @@ var HELP = {
|
|
|
39107
39435
|
examples: ["candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne"],
|
|
39108
39436
|
env: ENV_API
|
|
39109
39437
|
},
|
|
39438
|
+
portfolio: {
|
|
39439
|
+
group: "Account",
|
|
39440
|
+
summary: "Every wallet's holdings, prices and value in one table (vault read over your own RPC)",
|
|
39441
|
+
description: "Vault, TEE and embedded wallets, each token with amount, price and value, then a total. TEE and embedded balances come from Candle; vault and external wallets are read over your own RPC, and Candle is sent only the mints they hold, for prices. Unpriced tokens are shown as unpriced and left out of the total. Where Candle serves LP, each TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial.",
|
|
39442
|
+
usage: ["candle portfolio [--rpc-url <url>] [--json]"],
|
|
39443
|
+
rows: [],
|
|
39444
|
+
flags: [
|
|
39445
|
+
{
|
|
39446
|
+
invocation: "--rpc-url <url>",
|
|
39447
|
+
description: "Your own Solana RPC, for the vault. Without it (or CANDLE_SOLANA_RPC_URL) the vault is not read"
|
|
39448
|
+
},
|
|
39449
|
+
KEYSTORE_FLAG
|
|
39450
|
+
],
|
|
39451
|
+
examples: ["candle portfolio", "candle portfolio --rpc-url https://your-rpc.example --json"],
|
|
39452
|
+
env: [...ENV_API, ...ENV_LOCAL_SIGNING]
|
|
39453
|
+
},
|
|
39454
|
+
pnl: {
|
|
39455
|
+
group: "Account",
|
|
39456
|
+
summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
|
|
39457
|
+
description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the web P&L chart shows. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its own P&L. Unpriced positions are shown as unpriced and never valued at zero. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis), and vs holding (what the deposited tokens would be worth held); the total then covers tokens and LP.",
|
|
39458
|
+
usage: ["candle pnl [--profile <name>] [--json]"],
|
|
39459
|
+
rows: [],
|
|
39460
|
+
examples: ["candle pnl", "candle pnl --profile scalper --json"],
|
|
39461
|
+
env: ENV_API
|
|
39462
|
+
},
|
|
39110
39463
|
keys: {
|
|
39111
39464
|
group: "Account",
|
|
39112
39465
|
summary: "API keys, and the wallets each key may use",
|
|
@@ -39115,11 +39468,11 @@ var HELP = {
|
|
|
39115
39468
|
rows: [
|
|
39116
39469
|
{
|
|
39117
39470
|
invocation: "list [--scopes]",
|
|
39118
|
-
description: "List API keys: name and Read or Read:Write access; --scopes adds the raw scopes"
|
|
39471
|
+
description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
|
|
39119
39472
|
},
|
|
39120
39473
|
{
|
|
39121
|
-
invocation: "create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
|
|
39122
|
-
description: "Create an API key"
|
|
39474
|
+
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]]",
|
|
39475
|
+
description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs)"
|
|
39123
39476
|
},
|
|
39124
39477
|
{ invocation: "revoke <prefix>", description: "Revoke an API key" },
|
|
39125
39478
|
{ invocation: "wallets <prefix>", description: "Wallets an agent profile can use" },
|
|
@@ -39131,6 +39484,7 @@ var HELP = {
|
|
|
39131
39484
|
],
|
|
39132
39485
|
examples: [
|
|
39133
39486
|
"candle keys list",
|
|
39487
|
+
"candle keys create --access read-write-transfer --label rebalancer",
|
|
39134
39488
|
"candle keys create --scopes trade:write --label agent-one",
|
|
39135
39489
|
"candle keys wallets ck_live_ab12",
|
|
39136
39490
|
"candle keys revoke ck_live_ab12"
|
|
@@ -39141,19 +39495,28 @@ var HELP = {
|
|
|
39141
39495
|
group: "Account",
|
|
39142
39496
|
display: "wallet",
|
|
39143
39497
|
summary: "Launch and linked wallets; import or revoke one (wallets is an alias)",
|
|
39144
|
-
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.",
|
|
39498
|
+
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.",
|
|
39145
39499
|
usage: ["candle wallet [flags]", "candle wallet <subcommand> [flags]"],
|
|
39146
39500
|
rows: [
|
|
39147
39501
|
{
|
|
39148
39502
|
invocation: "import --chain <solana|evm> [options]",
|
|
39149
39503
|
description: "Import a wallet you own (key via --key-file or hidden prompt)"
|
|
39150
39504
|
},
|
|
39151
|
-
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" }
|
|
39505
|
+
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
|
|
39506
|
+
{
|
|
39507
|
+
invocation: "trust <label|address|id|prefix*>...",
|
|
39508
|
+
description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
|
|
39509
|
+
},
|
|
39510
|
+
{
|
|
39511
|
+
invocation: "untrust <label|address|id|prefix*>... [--yes]",
|
|
39512
|
+
description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
|
|
39513
|
+
}
|
|
39152
39514
|
],
|
|
39153
39515
|
examples: [
|
|
39154
39516
|
"candle wallet",
|
|
39155
39517
|
"candle wallet import --chain solana --key-file ./signer.json",
|
|
39156
|
-
"candle wallet revoke wal_123"
|
|
39518
|
+
"candle wallet revoke wal_123",
|
|
39519
|
+
"candle wallet trust 'tr-*' 'dest-*'"
|
|
39157
39520
|
],
|
|
39158
39521
|
env: ENV_API
|
|
39159
39522
|
},
|
|
@@ -39228,14 +39591,14 @@ var HELP = {
|
|
|
39228
39591
|
},
|
|
39229
39592
|
{
|
|
39230
39593
|
invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
|
|
39231
|
-
description: "Sign a vault
|
|
39594
|
+
description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
|
|
39232
39595
|
},
|
|
39233
39596
|
{
|
|
39234
|
-
invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>]",
|
|
39597
|
+
invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
|
|
39235
39598
|
description: "Fresh TEE key, or promote one vault key in place. Reads, over your RPC, whether each key is a token mint, freeze, program upgrade or stake authority (9 requests per key; public endpoints refuse the token scans). Multisig membership is not checked."
|
|
39236
39599
|
},
|
|
39237
39600
|
{
|
|
39238
|
-
invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings]",
|
|
39601
|
+
invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--to-key <prefix|label>] [--token-holdings]",
|
|
39239
39602
|
description: "Promote many vault keys in place: one unlock, one reviewed acknowledgement. Reads, over your RPC, whether each key is a token mint, freeze, program upgrade or stake authority (9 requests per key; public endpoints refuse the token scans). Multisig membership is not checked."
|
|
39240
39603
|
},
|
|
39241
39604
|
{
|
|
@@ -39257,7 +39620,10 @@ var HELP = {
|
|
|
39257
39620
|
invocation: "--factor <id|kind>",
|
|
39258
39621
|
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"
|
|
39259
39622
|
},
|
|
39260
|
-
{
|
|
39623
|
+
{
|
|
39624
|
+
invocation: "--device <id>",
|
|
39625
|
+
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)"
|
|
39626
|
+
},
|
|
39261
39627
|
{
|
|
39262
39628
|
invocation: "--balances",
|
|
39263
39629
|
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"
|
|
@@ -39269,6 +39635,10 @@ var HELP = {
|
|
|
39269
39635
|
{
|
|
39270
39636
|
invocation: "--pairs-from <file>",
|
|
39271
39637
|
description: "promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
|
|
39638
|
+
},
|
|
39639
|
+
{
|
|
39640
|
+
invocation: "--to-key <prefix|label>",
|
|
39641
|
+
description: "promote, promote-batch: bind the promoted wallets to this key instead of the calling one. The import runs under the calling key as before, then the wallets are moved to the named key by a rebind (device token; the target's secret never touches this machine). The target is checked before the unlock; a wallet whose rebind fails is named as still on the calling key, with the tee rebind command that finishes."
|
|
39272
39642
|
}
|
|
39273
39643
|
],
|
|
39274
39644
|
examples: [
|
|
@@ -39278,6 +39648,7 @@ var HELP = {
|
|
|
39278
39648
|
"candle vault rename key-7 treasury-cold",
|
|
39279
39649
|
"candle vault new-key --chain solana --labels-from ./replacement-names.txt",
|
|
39280
39650
|
"candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc>",
|
|
39651
|
+
"candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc> --to-key tr-01",
|
|
39281
39652
|
"candle vault enroll security-key --label yubikey-a",
|
|
39282
39653
|
"candle vault backup --to /Volumes/BACKUP/vault.enc",
|
|
39283
39654
|
"candle vault backup --to icloud",
|
|
@@ -39310,7 +39681,7 @@ var HELP = {
|
|
|
39310
39681
|
},
|
|
39311
39682
|
{
|
|
39312
39683
|
invocation: "sweep <address> --rpc-url <url> [--emergency]",
|
|
39313
|
-
description: "Sign locally and move everything to the pinned vault"
|
|
39684
|
+
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)"
|
|
39314
39685
|
},
|
|
39315
39686
|
{
|
|
39316
39687
|
invocation: "rebind <wallet...> --to-key <prefix|label>",
|
|
@@ -43820,7 +44191,7 @@ function validClientId(id) {
|
|
|
43820
44191
|
}
|
|
43821
44192
|
async function lookupOperation(ctx, key, id, kind) {
|
|
43822
44193
|
const local = await savedOperation(ctx, key, id);
|
|
43823
|
-
const kinds = kind ? [kind] : local ? [local.kind] : ["trade", "swap", "launch"];
|
|
44194
|
+
const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
|
|
43824
44195
|
const found = [];
|
|
43825
44196
|
for (const candidate of kinds) {
|
|
43826
44197
|
try {
|
|
@@ -44095,7 +44466,7 @@ async function launch(args, ctx) {
|
|
|
44095
44466
|
if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
|
|
44096
44467
|
throw new TradingError("QUOTE_EXPIRED", "The launch build expired before signing.");
|
|
44097
44468
|
const signed = await relaySign(ctx, key, wallet, built.transaction);
|
|
44098
|
-
const signature = await
|
|
44469
|
+
const signature = await saveOperationSignature(ctx, key, id, "launch", signed);
|
|
44099
44470
|
const broadcastSignature = await createSolanaRpc(url, ctx.deps.fetch).sendTransaction(signed);
|
|
44100
44471
|
if (broadcastSignature !== signature)
|
|
44101
44472
|
throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
|
|
@@ -44108,6 +44479,368 @@ async function launch(args, ctx) {
|
|
|
44108
44479
|
}
|
|
44109
44480
|
}
|
|
44110
44481
|
|
|
44482
|
+
// src/commands/lp.ts
|
|
44483
|
+
init_args();
|
|
44484
|
+
init_render();
|
|
44485
|
+
init_solana_lite();
|
|
44486
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
44487
|
+
init_trading();
|
|
44488
|
+
var LP_SCOPE = "lp:write";
|
|
44489
|
+
var CONFIRM_POLL_MS2 = 2000;
|
|
44490
|
+
var CONFIRM_MAX_POLLS2 = 45;
|
|
44491
|
+
async function lpRequest(ctx, key, path, body) {
|
|
44492
|
+
const result = await apiRequest(path, {
|
|
44493
|
+
apiUrl: ctx.apiUrl,
|
|
44494
|
+
credentials: { apiKey: key },
|
|
44495
|
+
auth: "key",
|
|
44496
|
+
method: body ? "POST" : "GET",
|
|
44497
|
+
body,
|
|
44498
|
+
fetch: ctx.deps.fetch,
|
|
44499
|
+
env: ctx.deps.env
|
|
44500
|
+
});
|
|
44501
|
+
if (!result.ok) {
|
|
44502
|
+
if (result.status === 404 && !result.code)
|
|
44503
|
+
throw new TradingError("LP_NOT_ENABLED", "This Candle deployment does not serve LP routes (LP_ENABLED is off there, or the API predates them).");
|
|
44504
|
+
throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
|
|
44505
|
+
}
|
|
44506
|
+
if (!result.body || typeof result.body !== "object")
|
|
44507
|
+
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
44508
|
+
return result.body;
|
|
44509
|
+
}
|
|
44510
|
+
function solanaAddress(value, what) {
|
|
44511
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
|
|
44512
|
+
throw new TradingError("INVALID_ADDRESS", `${what} must be a Solana address (base58).`);
|
|
44513
|
+
return value;
|
|
44514
|
+
}
|
|
44515
|
+
function poolToken(value) {
|
|
44516
|
+
const base = baseAsset(value);
|
|
44517
|
+
if (base)
|
|
44518
|
+
return BASES[base]?.mint;
|
|
44519
|
+
return solanaAddress(value, "The token");
|
|
44520
|
+
}
|
|
44521
|
+
function matchesWallet(row, name) {
|
|
44522
|
+
return row.id === name || row.address === name || row.label === name;
|
|
44523
|
+
}
|
|
44524
|
+
function usage2(ctx, line) {
|
|
44525
|
+
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
44526
|
+
return 2;
|
|
44527
|
+
}
|
|
44528
|
+
function amountLine(amount) {
|
|
44529
|
+
const base = Object.entries(BASES).find(([, asset]) => asset.mint === amount.mint)?.[0];
|
|
44530
|
+
return `${decimalAmount(amount.raw, amount.decimals)} ${base ?? amount.mint}`;
|
|
44531
|
+
}
|
|
44532
|
+
async function lpPools(args, ctx) {
|
|
44533
|
+
const parsed = parseArgs(args, { valueFlags: ["--page"] });
|
|
44534
|
+
const page = Number(parsed && !("error" in parsed) ? parsed.values["--page"] ?? "1" : "1");
|
|
44535
|
+
if ("error" in parsed)
|
|
44536
|
+
return usage2(ctx, parsed.error);
|
|
44537
|
+
if (parsed.positionals.length !== 1 || !Number.isSafeInteger(page) || page < 1)
|
|
44538
|
+
return usage2(ctx, "Usage: candle lp pools <token> [--page <n>]");
|
|
44539
|
+
try {
|
|
44540
|
+
const token = poolToken(parsed.positionals[0]);
|
|
44541
|
+
const key = await tradingKey(ctx);
|
|
44542
|
+
const pools = lpPoolsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/pools/${encodeURIComponent(token)}?page=${page}`));
|
|
44543
|
+
if (ctx.json)
|
|
44544
|
+
return printTradingResult(ctx, { success: true, token, ...pools });
|
|
44545
|
+
const out = ctx.deps.stdout;
|
|
44546
|
+
if (pools.pools.length === 0) {
|
|
44547
|
+
out.write(`No DAMM v2 pool lists ${token} (page ${pools.page} of ${pools.pages}).
|
|
44548
|
+
`);
|
|
44549
|
+
return 0;
|
|
44550
|
+
}
|
|
44551
|
+
const usd = (value) => value === null || value === undefined ? "unavailable" : `$${value.toFixed(2)}`;
|
|
44552
|
+
const pct = (value) => value === null || value === undefined ? "unavailable" : `${value.toFixed(2)}%`;
|
|
44553
|
+
out.write(`DAMM v2 pools for ${token} (page ${pools.page} of ${pools.pages}):
|
|
44554
|
+
`);
|
|
44555
|
+
for (const pool of pools.pools) {
|
|
44556
|
+
out.write(` ${safeText(pool.pool)}
|
|
44557
|
+
pair ${pool.tokens.map(safeText).join(" / ")}
|
|
44558
|
+
liquidity ${usd(pool.liquidityUsd)}, fee ${pct(pool.baseFeePercent)}, 24h volume ${usd(pool.volume24hUsd)}, est. yield ${pct(pool.estimatedAprPercent)} APR (indexed, not a quote)
|
|
44559
|
+
`);
|
|
44560
|
+
}
|
|
44561
|
+
return 0;
|
|
44562
|
+
} catch (error) {
|
|
44563
|
+
return tradingFailure(ctx, error);
|
|
44564
|
+
}
|
|
44565
|
+
}
|
|
44566
|
+
async function lpPositions(args, ctx) {
|
|
44567
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
|
|
44568
|
+
if ("error" in parsed)
|
|
44569
|
+
return usage2(ctx, parsed.error);
|
|
44570
|
+
if (parsed.positionals.length !== 0)
|
|
44571
|
+
return usage2(ctx, "Usage: candle lp positions [--wallet <tee>]");
|
|
44572
|
+
try {
|
|
44573
|
+
const key = await tradingKey(ctx);
|
|
44574
|
+
const { rows } = await listTradingWallets(ctx, key);
|
|
44575
|
+
const name = parsed.values["--wallet"];
|
|
44576
|
+
const wallets = rows.filter((row) => row.chain === "solana" && (name === undefined || matchesWallet(row, name)));
|
|
44577
|
+
if (name !== undefined && wallets.length === 0)
|
|
44578
|
+
throw new TradingError("TEE_WALLET_REQUIRED", `No TEE wallet on this key is called "${name}".`);
|
|
44579
|
+
const report = [];
|
|
44580
|
+
for (const row of wallets) {
|
|
44581
|
+
const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
|
|
44582
|
+
report.push({
|
|
44583
|
+
id: row.id,
|
|
44584
|
+
address: row.address,
|
|
44585
|
+
...row.label ? { label: row.label } : {},
|
|
44586
|
+
positions: positions.positions
|
|
44587
|
+
});
|
|
44588
|
+
}
|
|
44589
|
+
if (ctx.json)
|
|
44590
|
+
return printTradingResult(ctx, { success: true, wallets: report });
|
|
44591
|
+
const out = ctx.deps.stdout;
|
|
44592
|
+
const total = report.reduce((n, wallet) => n + wallet.positions.length, 0);
|
|
44593
|
+
if (total === 0) {
|
|
44594
|
+
out.write(`No DAMM v2 positions across ${report.length} TEE wallet(s).
|
|
44595
|
+
`);
|
|
44596
|
+
return 0;
|
|
44597
|
+
}
|
|
44598
|
+
for (const wallet of report) {
|
|
44599
|
+
if (wallet.positions.length === 0)
|
|
44600
|
+
continue;
|
|
44601
|
+
out.write(`${wallet.label ? `${safeText(wallet.label)} ` : ""}(${wallet.id}, ${safeText(wallet.address)})
|
|
44602
|
+
`);
|
|
44603
|
+
for (const position of wallet.positions) {
|
|
44604
|
+
out.write(` position ${safeText(position.position)} in pool ${safeText(position.pool)}
|
|
44605
|
+
`);
|
|
44606
|
+
for (const token of position.tokens) {
|
|
44607
|
+
out.write(` ${amountLine({ mint: token.mint, raw: token.amountRaw, decimals: token.decimals })}, unclaimed fees ${decimalAmount(token.unclaimedFeesRaw, token.decimals)}${token.valueUsd === null || token.valueUsd === undefined ? "" : ` ($${token.valueUsd.toFixed(2)})`}
|
|
44608
|
+
`);
|
|
44609
|
+
}
|
|
44610
|
+
out.write(` value ${position.valueUsd === null || position.valueUsd === undefined ? "unpriced" : `$${position.valueUsd.toFixed(2)}`}${position.poolShare === undefined ? "" : `, ${(position.poolShare * 100).toFixed(4)}% of the pool`}
|
|
44611
|
+
`);
|
|
44612
|
+
}
|
|
44613
|
+
}
|
|
44614
|
+
return 0;
|
|
44615
|
+
} catch (error) {
|
|
44616
|
+
return tradingFailure(ctx, error);
|
|
44617
|
+
}
|
|
44618
|
+
}
|
|
44619
|
+
async function walletHolding(ctx, key, position, name) {
|
|
44620
|
+
if (name !== undefined)
|
|
44621
|
+
return tradingWallet(ctx, key, name, LP_SCOPE);
|
|
44622
|
+
const { rows, appId } = await listTradingWallets(ctx, key, LP_SCOPE);
|
|
44623
|
+
for (const row of rows) {
|
|
44624
|
+
if (row.chain !== "solana")
|
|
44625
|
+
continue;
|
|
44626
|
+
const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
|
|
44627
|
+
if (positions.positions.some((held) => held.position === position))
|
|
44628
|
+
return completeTradingWallet(ctx, row, appId, LP_SCOPE);
|
|
44629
|
+
}
|
|
44630
|
+
throw new TradingError("LP_POSITION_NOT_FOUND", `No TEE wallet bound to this key holds position ${position}. See candle lp positions, or name the wallet with --wallet.`);
|
|
44631
|
+
}
|
|
44632
|
+
async function waitConfirmed(ctx, rpc2, signature, id) {
|
|
44633
|
+
for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
|
|
44634
|
+
const observed = classifyStatus(await rpc2.getSignatureStatus(signature));
|
|
44635
|
+
if (observed.kind === "finalized")
|
|
44636
|
+
return;
|
|
44637
|
+
if (observed.kind === "failed")
|
|
44638
|
+
throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}.`);
|
|
44639
|
+
if (observed.kind === "nonfinal") {
|
|
44640
|
+
if (observed.err !== null && observed.err !== undefined)
|
|
44641
|
+
throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed: ${JSON.stringify(observed.err)}.`);
|
|
44642
|
+
if (observed.confirmationStatus === "confirmed")
|
|
44643
|
+
return;
|
|
44644
|
+
}
|
|
44645
|
+
await ctx.deps.sleep(CONFIRM_POLL_MS2);
|
|
44646
|
+
}
|
|
44647
|
+
throw new TradingError("LP_CONFIRM_PENDING", `Transaction ${signature} was sent but not confirmed within ${CONFIRM_MAX_POLLS2 * CONFIRM_POLL_MS2 / 1000}s. Re-run the same command with --client-trade-id ${id}: it confirms the saved signature and sends nothing new.`);
|
|
44648
|
+
}
|
|
44649
|
+
async function confirmPreview(ctx, plan, built) {
|
|
44650
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
44651
|
+
output.write(`${safeText(plan.intent)}
|
|
44652
|
+
Payer: ${safeText(plan.wallet.address)}
|
|
44653
|
+
`);
|
|
44654
|
+
output.write(`Pool: ${safeText(built.build.pool)}
|
|
44655
|
+
Position: ${safeText(built.build.position)}
|
|
44656
|
+
`);
|
|
44657
|
+
const preview = built.preview;
|
|
44658
|
+
const amounts = preview?.amounts ?? [];
|
|
44659
|
+
if (amounts.length > 0) {
|
|
44660
|
+
const verb = plan.action === "add" ? "Deposit (maximum, at the pool's ratio)" : plan.action === "remove" ? "Withdraw (minimum)" : "Claim";
|
|
44661
|
+
output.write(`${verb}: ${amounts.map(amountLine).join(" + ")}
|
|
44662
|
+
`);
|
|
44663
|
+
}
|
|
44664
|
+
output.write(`Candle LP fee: ${safeText(preview?.candleFeeBps ?? 0)} bps
|
|
44665
|
+
`);
|
|
44666
|
+
for (const warning of preview?.warnings ?? [])
|
|
44667
|
+
output.write(`Warning: ${safeText(warning)}
|
|
44668
|
+
`);
|
|
44669
|
+
for (const risk of preview?.tokenRisks ?? [])
|
|
44670
|
+
output.write(`Warning (${safeText(risk.mint)}): ${safeText(risk.message)}
|
|
44671
|
+
`);
|
|
44672
|
+
if (built.replay)
|
|
44673
|
+
output.write(`This client id was already built; the same artifact is shown again.
|
|
44674
|
+
`);
|
|
44675
|
+
if (plan.yes)
|
|
44676
|
+
return true;
|
|
44677
|
+
if (!ctx.deps.isTTY.stdin)
|
|
44678
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary LP prompt.");
|
|
44679
|
+
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
44680
|
+
}
|
|
44681
|
+
async function runLpOperation(ctx, plan) {
|
|
44682
|
+
const { id, key, wallet, action } = plan;
|
|
44683
|
+
const confirm = async (signature2, extra) => {
|
|
44684
|
+
const result = await lpRequest(ctx, key, "/api/v1/agent/lp/confirm", { clientTradeId: id, signature: signature2 });
|
|
44685
|
+
return printTradingResult(ctx, {
|
|
44686
|
+
...result,
|
|
44687
|
+
clientTradeId: id,
|
|
44688
|
+
kind: "lp",
|
|
44689
|
+
action,
|
|
44690
|
+
signature: signature2,
|
|
44691
|
+
wallet: safeText(wallet.address),
|
|
44692
|
+
...extra
|
|
44693
|
+
});
|
|
44694
|
+
};
|
|
44695
|
+
const local = await savedOperation(ctx, key, id);
|
|
44696
|
+
if (local?.signature)
|
|
44697
|
+
return confirm(local.signature, { resumed: true });
|
|
44698
|
+
if (!local && !await claimOperation(ctx, key, id, "lp"))
|
|
44699
|
+
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
44700
|
+
ctx.deps.stderr.write(`Operation: ${id}
|
|
44701
|
+
`);
|
|
44702
|
+
const built = lpBuildSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/${action}/build`, {
|
|
44703
|
+
linkedWalletId: wallet.id,
|
|
44704
|
+
clientTradeId: id,
|
|
44705
|
+
...plan.body
|
|
44706
|
+
}));
|
|
44707
|
+
if (built.build.walletAddress !== wallet.address || built.build.action !== action)
|
|
44708
|
+
throw new TradingError("INVALID_RESPONSE", "The LP build does not name the requested wallet and action; nothing was signed.");
|
|
44709
|
+
if (built.build.signature)
|
|
44710
|
+
return confirm(built.build.signature, { resumed: true });
|
|
44711
|
+
const preview = {
|
|
44712
|
+
pool: built.build.pool,
|
|
44713
|
+
position: built.build.position,
|
|
44714
|
+
amounts: built.preview?.amounts ?? [],
|
|
44715
|
+
warnings: built.preview?.warnings ?? [],
|
|
44716
|
+
tokenRisks: built.preview?.tokenRisks ?? [],
|
|
44717
|
+
candleFeeBps: built.preview?.candleFeeBps ?? 0
|
|
44718
|
+
};
|
|
44719
|
+
if (!await confirmPreview(ctx, plan, built))
|
|
44720
|
+
return printTradingResult(ctx, {
|
|
44721
|
+
success: true,
|
|
44722
|
+
status: "cancelled",
|
|
44723
|
+
clientTradeId: id,
|
|
44724
|
+
kind: "lp",
|
|
44725
|
+
action,
|
|
44726
|
+
preview
|
|
44727
|
+
});
|
|
44728
|
+
const signed = await relaySign(ctx, key, wallet, built.build.transaction);
|
|
44729
|
+
const signature = await saveOperationSignature(ctx, key, id, "lp", signed);
|
|
44730
|
+
const rpc2 = createSolanaRpc(plan.url, ctx.deps.fetch);
|
|
44731
|
+
const echoed = await rpc2.sendTransaction(signed);
|
|
44732
|
+
if (echoed !== signature)
|
|
44733
|
+
throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
|
|
44734
|
+
ctx.deps.stderr.write(`LP ${action} signature: ${signature}
|
|
44735
|
+
`);
|
|
44736
|
+
await waitConfirmed(ctx, rpc2, signature, id);
|
|
44737
|
+
return confirm(signature, { preview });
|
|
44738
|
+
}
|
|
44739
|
+
function slippageOf(flag) {
|
|
44740
|
+
const slippage = Number(flag ?? "100");
|
|
44741
|
+
if (!Number.isInteger(slippage) || slippage < 0 || slippage > 1000)
|
|
44742
|
+
throw new TradingError("INVALID_AMOUNT", "--slippage-bps must be an integer from 0 to 1000.");
|
|
44743
|
+
return slippage;
|
|
44744
|
+
}
|
|
44745
|
+
async function lpAdd(args, ctx) {
|
|
44746
|
+
const parsed = parseArgs(args, {
|
|
44747
|
+
valueFlags: ["--amount", "--wallet", "--position", "--client-trade-id", "--slippage-bps", "--rpc-url"],
|
|
44748
|
+
booleanFlags: ["--yes"]
|
|
44749
|
+
});
|
|
44750
|
+
if ("error" in parsed)
|
|
44751
|
+
return usage2(ctx, parsed.error);
|
|
44752
|
+
const flags = parsed.values;
|
|
44753
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44754
|
+
if (parsed.positionals.length !== 2 || !flags["--amount"] || !flags["--wallet"] || !validClientId(id))
|
|
44755
|
+
return usage2(ctx, "Usage: candle lp add <pool> --amount <decimal> <token> --wallet <tee> [--position <nft-mint>] [--slippage-bps 100] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
|
|
44756
|
+
try {
|
|
44757
|
+
const pool = solanaAddress(parsed.positionals[0], "The pool");
|
|
44758
|
+
const token = poolToken(parsed.positionals[1]);
|
|
44759
|
+
const position = flags["--position"] ? solanaAddress(flags["--position"], "--position") : undefined;
|
|
44760
|
+
rawAmount(flags["--amount"], 18);
|
|
44761
|
+
const slippageBps = slippageOf(flags["--slippage-bps"]);
|
|
44762
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44763
|
+
const key = await tradingKey(ctx);
|
|
44764
|
+
const wallet = await tradingWallet(ctx, key, flags["--wallet"], LP_SCOPE);
|
|
44765
|
+
const decimals = await decimalsFor(ctx, baseAsset(token) ?? token, flags["--rpc-url"]);
|
|
44766
|
+
const amountRaw = rawAmount(flags["--amount"], decimals);
|
|
44767
|
+
return await runLpOperation(ctx, {
|
|
44768
|
+
action: "add",
|
|
44769
|
+
id,
|
|
44770
|
+
key,
|
|
44771
|
+
wallet,
|
|
44772
|
+
url,
|
|
44773
|
+
yes: parsed.booleans.has("--yes"),
|
|
44774
|
+
intent: `Add ${decimalAmount(amountRaw, decimals)} ${baseAsset(token) ?? token} of liquidity to DAMM v2 pool ${pool}${position ? ` (position ${position})` : " (new position)"}`,
|
|
44775
|
+
body: { pool, token, amountRaw, slippageBps, ...position ? { position } : {} }
|
|
44776
|
+
});
|
|
44777
|
+
} catch (error) {
|
|
44778
|
+
return tradingFailure(ctx, error, id);
|
|
44779
|
+
}
|
|
44780
|
+
}
|
|
44781
|
+
async function lpRemove(args, ctx) {
|
|
44782
|
+
const parsed = parseArgs(args, {
|
|
44783
|
+
valueFlags: ["--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
|
|
44784
|
+
booleanFlags: ["--yes"]
|
|
44785
|
+
});
|
|
44786
|
+
if ("error" in parsed)
|
|
44787
|
+
return usage2(ctx, parsed.error);
|
|
44788
|
+
const flags = parsed.values;
|
|
44789
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44790
|
+
const percent = Number(flags["--percent"]);
|
|
44791
|
+
if (parsed.positionals.length !== 1 || !flags["--percent"] || !/^\d+(\.\d{1,2})?$/.test(flags["--percent"]) || !(percent > 0 && percent <= 100) || !validClientId(id))
|
|
44792
|
+
return usage2(ctx, "Usage: candle lp remove <position> --percent <0.01-100> [--wallet <tee>] [--slippage-bps 100] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
|
|
44793
|
+
try {
|
|
44794
|
+
const position = solanaAddress(parsed.positionals[0], "The position");
|
|
44795
|
+
const slippageBps = slippageOf(flags["--slippage-bps"]);
|
|
44796
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44797
|
+
const key = await tradingKey(ctx);
|
|
44798
|
+
const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
|
|
44799
|
+
return await runLpOperation(ctx, {
|
|
44800
|
+
action: "remove",
|
|
44801
|
+
id,
|
|
44802
|
+
key,
|
|
44803
|
+
wallet,
|
|
44804
|
+
url,
|
|
44805
|
+
yes: parsed.booleans.has("--yes"),
|
|
44806
|
+
intent: percent === 100 ? `Remove all liquidity from position ${position}, claim its fees and close it (rent returns to the wallet)` : `Remove ${percent}% of the liquidity in position ${position}`,
|
|
44807
|
+
body: { position, percent, slippageBps }
|
|
44808
|
+
});
|
|
44809
|
+
} catch (error) {
|
|
44810
|
+
return tradingFailure(ctx, error, id);
|
|
44811
|
+
}
|
|
44812
|
+
}
|
|
44813
|
+
async function lpClaim(args, ctx) {
|
|
44814
|
+
const parsed = parseArgs(args, {
|
|
44815
|
+
valueFlags: ["--wallet", "--client-trade-id", "--rpc-url"],
|
|
44816
|
+
booleanFlags: ["--yes"]
|
|
44817
|
+
});
|
|
44818
|
+
if ("error" in parsed)
|
|
44819
|
+
return usage2(ctx, parsed.error);
|
|
44820
|
+
const flags = parsed.values;
|
|
44821
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44822
|
+
if (parsed.positionals.length !== 1 || !validClientId(id))
|
|
44823
|
+
return usage2(ctx, "Usage: candle lp claim <position> [--wallet <tee>] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
|
|
44824
|
+
try {
|
|
44825
|
+
const position = solanaAddress(parsed.positionals[0], "The position");
|
|
44826
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44827
|
+
const key = await tradingKey(ctx);
|
|
44828
|
+
const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
|
|
44829
|
+
return await runLpOperation(ctx, {
|
|
44830
|
+
action: "claim",
|
|
44831
|
+
id,
|
|
44832
|
+
key,
|
|
44833
|
+
wallet,
|
|
44834
|
+
url,
|
|
44835
|
+
yes: parsed.booleans.has("--yes"),
|
|
44836
|
+
intent: `Claim the fees and any rewards of position ${position}`,
|
|
44837
|
+
body: { position }
|
|
44838
|
+
});
|
|
44839
|
+
} catch (error) {
|
|
44840
|
+
return tradingFailure(ctx, error, id);
|
|
44841
|
+
}
|
|
44842
|
+
}
|
|
44843
|
+
|
|
44111
44844
|
// src/commands/mcp.ts
|
|
44112
44845
|
init_args();
|
|
44113
44846
|
init_deps();
|
|
@@ -44434,20 +45167,20 @@ async function writeSecretNames(deps, profile, names) {
|
|
|
44434
45167
|
else
|
|
44435
45168
|
await deps.updateProfile(profile, { secretNames: sorted });
|
|
44436
45169
|
}
|
|
44437
|
-
function
|
|
45170
|
+
function usage3(ctx, line) {
|
|
44438
45171
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
44439
45172
|
return 2;
|
|
44440
45173
|
}
|
|
44441
45174
|
async function secretsSet(args, ctx) {
|
|
44442
45175
|
const parsed = parseArgs(args, {});
|
|
44443
45176
|
if ("error" in parsed)
|
|
44444
|
-
return
|
|
45177
|
+
return usage3(ctx, parsed.error);
|
|
44445
45178
|
const [raw, extra] = parsed.positionals;
|
|
44446
45179
|
if (!raw || extra !== undefined)
|
|
44447
|
-
return
|
|
45180
|
+
return usage3(ctx, "Usage: candle secrets set <name>");
|
|
44448
45181
|
const name = canonicalSecretName(raw);
|
|
44449
45182
|
if (name === undefined) {
|
|
44450
|
-
return
|
|
45183
|
+
return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
44451
45184
|
}
|
|
44452
45185
|
if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
|
|
44453
45186
|
writeLocalFailure(ctx.deps, {
|
|
@@ -44459,7 +45192,7 @@ async function secretsSet(args, ctx) {
|
|
|
44459
45192
|
}
|
|
44460
45193
|
const value = await ctx.deps.promptSecret(`Value for ${name} (input hidden): `);
|
|
44461
45194
|
if (value.length === 0)
|
|
44462
|
-
return
|
|
45195
|
+
return usage3(ctx, "An empty value was typed; nothing was stored.");
|
|
44463
45196
|
try {
|
|
44464
45197
|
await ctx.deps.secretsStore.set(secretRef(ctx.profile, name), value);
|
|
44465
45198
|
} catch (error) {
|
|
@@ -44479,9 +45212,9 @@ async function secretsSet(args, ctx) {
|
|
|
44479
45212
|
async function secretsList(args, ctx) {
|
|
44480
45213
|
const parsed = parseArgs(args, {});
|
|
44481
45214
|
if ("error" in parsed)
|
|
44482
|
-
return
|
|
45215
|
+
return usage3(ctx, parsed.error);
|
|
44483
45216
|
if (parsed.positionals.length > 0)
|
|
44484
|
-
return
|
|
45217
|
+
return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
44485
45218
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
44486
45219
|
if (ctx.json) {
|
|
44487
45220
|
ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
|
|
@@ -44500,13 +45233,13 @@ async function secretsList(args, ctx) {
|
|
|
44500
45233
|
async function secretsRemove(args, ctx) {
|
|
44501
45234
|
const parsed = parseArgs(args, {});
|
|
44502
45235
|
if ("error" in parsed)
|
|
44503
|
-
return
|
|
45236
|
+
return usage3(ctx, parsed.error);
|
|
44504
45237
|
const [raw, extra] = parsed.positionals;
|
|
44505
45238
|
if (!raw || extra !== undefined)
|
|
44506
|
-
return
|
|
45239
|
+
return usage3(ctx, "Usage: candle secrets remove <name>");
|
|
44507
45240
|
const name = canonicalSecretName(raw);
|
|
44508
45241
|
if (name === undefined) {
|
|
44509
|
-
return
|
|
45242
|
+
return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
44510
45243
|
}
|
|
44511
45244
|
await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name));
|
|
44512
45245
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
@@ -44618,6 +45351,645 @@ async function runPlugin(name, rawArgs, ctx) {
|
|
|
44618
45351
|
return ctx.deps.runPlugin(path, split2.passthrough, env);
|
|
44619
45352
|
}
|
|
44620
45353
|
|
|
45354
|
+
// src/commands/pnl.ts
|
|
45355
|
+
init_args();
|
|
45356
|
+
init_deps();
|
|
45357
|
+
init_profiles();
|
|
45358
|
+
init_render();
|
|
45359
|
+
|
|
45360
|
+
// src/usd.ts
|
|
45361
|
+
function formatUsd(value) {
|
|
45362
|
+
if (!Number.isFinite(value))
|
|
45363
|
+
return "?";
|
|
45364
|
+
const sign2 = value < 0 ? "-" : "";
|
|
45365
|
+
const abs = Math.abs(value);
|
|
45366
|
+
if (abs > 0 && abs < 0.005)
|
|
45367
|
+
return `${sign2}<$0.01`;
|
|
45368
|
+
return `${sign2}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
45369
|
+
}
|
|
45370
|
+
function formatPrice(value) {
|
|
45371
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
45372
|
+
return "?";
|
|
45373
|
+
if (value >= 1)
|
|
45374
|
+
return formatUsd(value);
|
|
45375
|
+
return `$${Number(value.toPrecision(4)).toLocaleString("en-US", { maximumFractionDigits: 12 })}`;
|
|
45376
|
+
}
|
|
45377
|
+
function formatAmount(raw, decimals) {
|
|
45378
|
+
const digits = BigInt(raw).toString().padStart(decimals + 1, "0");
|
|
45379
|
+
if (decimals === 0)
|
|
45380
|
+
return digits;
|
|
45381
|
+
const whole = digits.slice(0, -decimals);
|
|
45382
|
+
const fraction = digits.slice(-decimals).replace(/0+$/, "");
|
|
45383
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
45384
|
+
}
|
|
45385
|
+
function formatQuantity(value) {
|
|
45386
|
+
if (!Number.isFinite(value))
|
|
45387
|
+
return "?";
|
|
45388
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
45389
|
+
}
|
|
45390
|
+
function shortAddress2(address) {
|
|
45391
|
+
return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address;
|
|
45392
|
+
}
|
|
45393
|
+
|
|
45394
|
+
// src/commands/pnl.ts
|
|
45395
|
+
var NO_API_KEY2 = {
|
|
45396
|
+
code: "NO_API_KEY",
|
|
45397
|
+
message: "No API key for this profile.",
|
|
45398
|
+
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
45399
|
+
};
|
|
45400
|
+
async function pnl(args, ctx) {
|
|
45401
|
+
const { deps, apiUrl, json } = ctx;
|
|
45402
|
+
const parsed = parseArgs(args, {});
|
|
45403
|
+
if ("error" in parsed) {
|
|
45404
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
45405
|
+
return 2;
|
|
45406
|
+
}
|
|
45407
|
+
if (parsed.positionals.length > 0) {
|
|
45408
|
+
writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}. Usage: candle pnl [--profile <name>]`, json);
|
|
45409
|
+
return 2;
|
|
45410
|
+
}
|
|
45411
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
45412
|
+
if (!apiKey) {
|
|
45413
|
+
writeLocalFailure(deps, NO_API_KEY2, json);
|
|
45414
|
+
return 1;
|
|
45415
|
+
}
|
|
45416
|
+
const perProfile = ctx.profileFlag !== undefined;
|
|
45417
|
+
let keyPrefix;
|
|
45418
|
+
if (perProfile) {
|
|
45419
|
+
keyPrefix = apiKeyPrefix(apiKey);
|
|
45420
|
+
if (keyPrefix === undefined) {
|
|
45421
|
+
writeLocalFailure(deps, {
|
|
45422
|
+
code: "BAD_REQUEST",
|
|
45423
|
+
message: `The API key for profile ${ctx.profileFlag} is not a Candle agent key, so it names no profile to read.`,
|
|
45424
|
+
suggestion: "Run `candle auth login --profile <name>` to store a key for that profile."
|
|
45425
|
+
}, json);
|
|
45426
|
+
return 1;
|
|
45427
|
+
}
|
|
45428
|
+
}
|
|
45429
|
+
await printIdentity(ctx);
|
|
45430
|
+
const path = perProfile ? `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl` : "/api/v1/agent/books";
|
|
45431
|
+
const result = await apiRequest(path, {
|
|
45432
|
+
auth: "key",
|
|
45433
|
+
credentials: { apiKey },
|
|
45434
|
+
apiUrl,
|
|
45435
|
+
fetch: deps.fetch,
|
|
45436
|
+
env: deps.env
|
|
45437
|
+
});
|
|
45438
|
+
if (!result.ok) {
|
|
45439
|
+
if (!perProfile && result.code === "SCOPE_MISSING") {
|
|
45440
|
+
writeLocalFailure(deps, {
|
|
45441
|
+
code: "SCOPE_MISSING",
|
|
45442
|
+
message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
|
|
45443
|
+
suggestion: "Log in with a Read or Read:Write key, or read this key's own P&L: candle pnl --profile <name>"
|
|
45444
|
+
}, json);
|
|
45445
|
+
return 1;
|
|
45446
|
+
}
|
|
45447
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
45448
|
+
return 1;
|
|
45449
|
+
}
|
|
45450
|
+
const body = result.body;
|
|
45451
|
+
if (json) {
|
|
45452
|
+
const { success: _success, ...rest } = body;
|
|
45453
|
+
deps.stdout.write(`${JSON.stringify({ ok: true, scope: perProfile ? "profile" : "account", ...rest })}
|
|
45454
|
+
`);
|
|
45455
|
+
return 0;
|
|
45456
|
+
}
|
|
45457
|
+
if (perProfile) {
|
|
45458
|
+
const { pnl: p, lp } = body;
|
|
45459
|
+
deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
|
|
45460
|
+
|
|
45461
|
+
`);
|
|
45462
|
+
writeSummary(ctx, {
|
|
45463
|
+
realizedNetUsd: p.realizedNetUsd,
|
|
45464
|
+
realizedGrossUsd: p.realizedGrossUsd,
|
|
45465
|
+
feesUsd: p.feesUsd,
|
|
45466
|
+
unrealizedUsd: p.unrealizedUsd,
|
|
45467
|
+
unmarked: p.unmarkedPositions,
|
|
45468
|
+
unvalued: p.unvalued,
|
|
45469
|
+
counted: p.counted,
|
|
45470
|
+
positions: p.openPositions.length,
|
|
45471
|
+
truncated: p.truncated,
|
|
45472
|
+
lookback: p.lookback,
|
|
45473
|
+
lookbackUnit: "trades",
|
|
45474
|
+
oldestMarkAt: p.oldestMarkAt,
|
|
45475
|
+
lp
|
|
45476
|
+
});
|
|
45477
|
+
writePositions(ctx, p.openPositions, false);
|
|
45478
|
+
writeLpPositions(ctx, lp, false);
|
|
45479
|
+
return 0;
|
|
45480
|
+
}
|
|
45481
|
+
const books = body;
|
|
45482
|
+
deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
|
|
45483
|
+
|
|
45484
|
+
`);
|
|
45485
|
+
writeSummary(ctx, {
|
|
45486
|
+
...books.all,
|
|
45487
|
+
positions: books.positions.length,
|
|
45488
|
+
truncated: books.truncated,
|
|
45489
|
+
lookback: books.lookback,
|
|
45490
|
+
lookbackUnit: "ledger rows",
|
|
45491
|
+
oldestMarkAt: books.oldestMarkAt,
|
|
45492
|
+
lp: books.lp
|
|
45493
|
+
});
|
|
45494
|
+
writePositions(ctx, books.positions, true);
|
|
45495
|
+
writeLpPositions(ctx, books.lp, true);
|
|
45496
|
+
return 0;
|
|
45497
|
+
}
|
|
45498
|
+
function writeSummary(ctx, s) {
|
|
45499
|
+
const marked = s.positions - s.unmarked;
|
|
45500
|
+
const lines = [
|
|
45501
|
+
[
|
|
45502
|
+
"Realized net",
|
|
45503
|
+
formatUsd(s.realizedNetUsd),
|
|
45504
|
+
`gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
|
|
45505
|
+
],
|
|
45506
|
+
[
|
|
45507
|
+
"Unrealized",
|
|
45508
|
+
formatUsd(s.unrealizedUsd),
|
|
45509
|
+
`${marked} of ${s.positions} open ${s.positions === 1 ? "position" : "positions"} marked${s.unmarked > 0 ? `; ${s.unmarked} unpriced, not counted` : ""}`
|
|
45510
|
+
]
|
|
45511
|
+
];
|
|
45512
|
+
const lp = s.lp;
|
|
45513
|
+
if (lp?.read) {
|
|
45514
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
45515
|
+
lines.push([
|
|
45516
|
+
"LP realized",
|
|
45517
|
+
formatUsd(lp.realizedUsd),
|
|
45518
|
+
`withdrawn ${formatUsd(lp.withdrawnUsd)} against ${formatUsd(lp.realizedBasisUsd)} of cost basis, plus ${formatUsd(lp.claimedFeesUsd)} claimed fees`
|
|
45519
|
+
], [
|
|
45520
|
+
"LP unrealized",
|
|
45521
|
+
formatUsd(lp.unrealizedUsd),
|
|
45522
|
+
`${lp.valued} of ${plural(lp.openPositions, "open LP position", "open LP positions")} valued${lp.unpriced > 0 ? `; ${lp.unpriced} unpriced, not counted` : ""}${lp.unreadable > 0 ? `; ${lp.unreadable} not read, not counted` : ""}${lp.closedOutsideLedger > 0 ? `; ${lp.closedOutsideLedger} closed outside the ledger, not counted` : ""}`
|
|
45523
|
+
], [
|
|
45524
|
+
"LP vs holding",
|
|
45525
|
+
lp.vsHoldingPositions > 0 ? formatUsd(lp.vsHoldingUsd) : "-",
|
|
45526
|
+
lp.vsHoldingPositions > 0 ? `${lp.vsHoldingPositions} of ${lp.valued} valued, against holding the deposited tokens (now ${formatUsd(lp.holdValueUsd)})` : "no valued LP position with every deposit priced"
|
|
45527
|
+
], [
|
|
45528
|
+
"Total",
|
|
45529
|
+
formatUsd(s.realizedNetUsd + s.unrealizedUsd + lp.realizedUsd + lp.unrealizedUsd),
|
|
45530
|
+
"realized net plus unrealized, tokens and LP"
|
|
45531
|
+
]);
|
|
45532
|
+
} else if (lp) {
|
|
45533
|
+
lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized, tokens only"]);
|
|
45534
|
+
} else {
|
|
45535
|
+
lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
|
|
45536
|
+
}
|
|
45537
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
45538
|
+
const valueWidth = Math.max(...lines.map(([, value]) => value.length));
|
|
45539
|
+
for (const [label, value, note] of lines) {
|
|
45540
|
+
ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
|
|
45541
|
+
`);
|
|
45542
|
+
}
|
|
45543
|
+
if (s.oldestMarkAt !== undefined) {
|
|
45544
|
+
ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
|
|
45545
|
+
`);
|
|
45546
|
+
}
|
|
45547
|
+
if (s.unvalued > 0 || (s.unresolved ?? 0) > 0) {
|
|
45548
|
+
ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
|
|
45549
|
+
`);
|
|
45550
|
+
}
|
|
45551
|
+
if (s.truncated) {
|
|
45552
|
+
ctx.deps.stdout.write(`History is truncated: this covers the most recent ${s.lookback} ${s.lookbackUnit}, not the account's lifetime.
|
|
45553
|
+
`);
|
|
45554
|
+
}
|
|
45555
|
+
if (lp?.read) {
|
|
45556
|
+
if (lp.unvalued > 0) {
|
|
45557
|
+
ctx.deps.stdout.write(`${lp.unvalued} LP ledger ${lp.unvalued === 1 ? "leg" : "legs"} could not be valued and ${lp.unvalued === 1 ? "is" : "are"} not in these figures.
|
|
45558
|
+
`);
|
|
45559
|
+
}
|
|
45560
|
+
if (lp.closedOutsideLedger > 0) {
|
|
45561
|
+
ctx.deps.stdout.write(`${lp.closedOutsideLedger} LP ${lp.closedOutsideLedger === 1 ? "position was" : "positions were"} closed outside the ledger (a sweep close writes no confirmation), so ${lp.closedOutsideLedger === 1 ? "its" : "their"} result is unknown and not in these figures.
|
|
45562
|
+
`);
|
|
45563
|
+
}
|
|
45564
|
+
if (lp.truncated) {
|
|
45565
|
+
ctx.deps.stdout.write(`LP history is truncated: this covers the most recent ${lp.lookback} LP operations, not the account's lifetime.
|
|
45566
|
+
`);
|
|
45567
|
+
}
|
|
45568
|
+
}
|
|
45569
|
+
}
|
|
45570
|
+
function writeLpPositions(ctx, lp, withBook) {
|
|
45571
|
+
if (!lp?.read || lp.positions.length === 0)
|
|
45572
|
+
return;
|
|
45573
|
+
const headers = ["POSITION", "POOL", "WALLET", "VALUE", "COST BASIS", "UNREALIZED", "VS HOLDING"];
|
|
45574
|
+
if (withBook)
|
|
45575
|
+
headers.push("BOOK");
|
|
45576
|
+
const value = (p) => p.status === "valued" && p.valueUsd !== undefined ? formatUsd(p.valueUsd) : p.status === "unpriced" ? "unpriced" : p.status === "unreadable" ? "not read" : "closed outside the ledger";
|
|
45577
|
+
const rows = lp.positions.map((p) => {
|
|
45578
|
+
const row = [
|
|
45579
|
+
shortAddress2(p.position),
|
|
45580
|
+
shortAddress2(p.pool),
|
|
45581
|
+
shortAddress2(p.wallet),
|
|
45582
|
+
value(p),
|
|
45583
|
+
formatUsd(p.costBasisUsd),
|
|
45584
|
+
p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-",
|
|
45585
|
+
p.vsHoldingUsd !== undefined ? formatUsd(p.vsHoldingUsd) : "-"
|
|
45586
|
+
];
|
|
45587
|
+
if (withBook)
|
|
45588
|
+
row.push(p.book ?? "-");
|
|
45589
|
+
return row;
|
|
45590
|
+
});
|
|
45591
|
+
ctx.deps.stdout.write(`
|
|
45592
|
+
Open LP positions
|
|
45593
|
+
${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
45594
|
+
`);
|
|
45595
|
+
}
|
|
45596
|
+
function writePositions(ctx, positions, withBook) {
|
|
45597
|
+
if (positions.length === 0) {
|
|
45598
|
+
ctx.deps.stdout.write(`
|
|
45599
|
+
No open positions.
|
|
45600
|
+
`);
|
|
45601
|
+
return;
|
|
45602
|
+
}
|
|
45603
|
+
const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
|
|
45604
|
+
if (withBook)
|
|
45605
|
+
headers.push("BOOK");
|
|
45606
|
+
const rows = positions.map((p) => {
|
|
45607
|
+
const row = [
|
|
45608
|
+
p.symbol?.trim() || shortAddress2(p.mint),
|
|
45609
|
+
formatQuantity(p.quantity),
|
|
45610
|
+
formatPrice(p.avgEntryUsd),
|
|
45611
|
+
p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
|
|
45612
|
+
p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-"
|
|
45613
|
+
];
|
|
45614
|
+
if (withBook)
|
|
45615
|
+
row.push(p.book ?? "-");
|
|
45616
|
+
return row;
|
|
45617
|
+
});
|
|
45618
|
+
ctx.deps.stdout.write(`
|
|
45619
|
+
Open positions
|
|
45620
|
+
${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
45621
|
+
`);
|
|
45622
|
+
}
|
|
45623
|
+
|
|
45624
|
+
// src/commands/portfolio.ts
|
|
45625
|
+
init_args();
|
|
45626
|
+
init_deps();
|
|
45627
|
+
init_profiles();
|
|
45628
|
+
init_render();
|
|
45629
|
+
init_solana_lite();
|
|
45630
|
+
init_store();
|
|
45631
|
+
init_vault_support();
|
|
45632
|
+
var SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
45633
|
+
var KNOWN_SYMBOLS = {
|
|
45634
|
+
[SOL_MINT]: "SOL",
|
|
45635
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
|
|
45636
|
+
"9dXSV8VWuYvGfTzqvkBeoFwH9ihVTybDuWo5VaJPCNDL": "CNDL"
|
|
45637
|
+
};
|
|
45638
|
+
var CHUNK = 100;
|
|
45639
|
+
var TOKEN_READS_IN_FLIGHT = 8;
|
|
45640
|
+
var NO_API_KEY3 = {
|
|
45641
|
+
code: "NO_API_KEY",
|
|
45642
|
+
message: "No API key for this profile.",
|
|
45643
|
+
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
45644
|
+
};
|
|
45645
|
+
async function portfolio(args, ctx) {
|
|
45646
|
+
const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
|
|
45647
|
+
if ("error" in parsed)
|
|
45648
|
+
return usage(ctx, parsed.error);
|
|
45649
|
+
if (parsed.positionals.length > 0)
|
|
45650
|
+
return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
45651
|
+
const { deps } = ctx;
|
|
45652
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
45653
|
+
if (!apiKey) {
|
|
45654
|
+
writeLocalFailure(deps, NO_API_KEY3, ctx.json);
|
|
45655
|
+
return 1;
|
|
45656
|
+
}
|
|
45657
|
+
const rpcGiven = parsed.values["--rpc-url"] !== undefined || Boolean(deps.env[RPC_URL_ENV]?.trim());
|
|
45658
|
+
let rpcUrl2;
|
|
45659
|
+
if (rpcGiven) {
|
|
45660
|
+
const resolved = rpcUrlFrom(ctx, parsed);
|
|
45661
|
+
if (typeof resolved !== "string")
|
|
45662
|
+
return usage(ctx, resolved.error);
|
|
45663
|
+
rpcUrl2 = resolved;
|
|
45664
|
+
}
|
|
45665
|
+
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
45666
|
+
if ("error" in resolvedVault)
|
|
45667
|
+
return usage(ctx, resolvedVault.error);
|
|
45668
|
+
return runVaultCommand(ctx, async ({ hold }) => {
|
|
45669
|
+
let vaultEntries;
|
|
45670
|
+
let vaultReason;
|
|
45671
|
+
const raw = rpcUrl2 === undefined ? null : await readVaultRaw(resolvedVault.path);
|
|
45672
|
+
if (rpcUrl2 === undefined) {
|
|
45673
|
+
vaultReason = `not read: vault balances are read only over your own RPC; pass --rpc-url or set ${RPC_URL_ENV}`;
|
|
45674
|
+
} else if (raw === null) {
|
|
45675
|
+
vaultReason = `no vault at ${resolvedVault.path}`;
|
|
45676
|
+
} else {
|
|
45677
|
+
if (!refuseEnvPassphrase(ctx))
|
|
45678
|
+
return 1;
|
|
45679
|
+
if (!requirePromptStreams(ctx, "portfolio"))
|
|
45680
|
+
return 1;
|
|
45681
|
+
const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
|
|
45682
|
+
vaultEntries = vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role }));
|
|
45683
|
+
}
|
|
45684
|
+
await printIdentity(ctx);
|
|
45685
|
+
const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
|
|
45686
|
+
if (vaultEntries && vaultEntries.length > 0 && rpcHost !== undefined) {
|
|
45687
|
+
const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
|
|
45688
|
+
deps.stderr.write(`Reading ${vaultEntries.length} vault ${vaultEntries.length === 1 ? "address" : "addresses"} from ${rpcHost} in ${requests} requests. That endpoint sees them together; Candle sees none of them.
|
|
45689
|
+
`);
|
|
45690
|
+
}
|
|
45691
|
+
const [candle, vaultRead] = await Promise.all([
|
|
45692
|
+
apiRequest("/api/v1/agent/portfolio", {
|
|
45693
|
+
auth: "key",
|
|
45694
|
+
credentials: { apiKey },
|
|
45695
|
+
apiUrl: ctx.apiUrl,
|
|
45696
|
+
fetch: deps.fetch,
|
|
45697
|
+
env: deps.env
|
|
45698
|
+
}),
|
|
45699
|
+
vaultEntries && rpcUrl2 ? readOwnRpc(vaultEntries.map((entry) => entry.address), rpcUrl2, deps.fetch) : Promise.resolve(undefined)
|
|
45700
|
+
]);
|
|
45701
|
+
if (!candle.ok) {
|
|
45702
|
+
writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
|
|
45703
|
+
return 1;
|
|
45704
|
+
}
|
|
45705
|
+
const fromCandle = candle.body;
|
|
45706
|
+
const prices = { ...fromCandle.prices ?? {} };
|
|
45707
|
+
let priceFailure;
|
|
45708
|
+
if (vaultRead) {
|
|
45709
|
+
const wanted = new Set;
|
|
45710
|
+
for (const wallet2 of vaultRead.byAddress.values()) {
|
|
45711
|
+
if (wallet2.lamports !== null)
|
|
45712
|
+
wanted.add(SOL_MINT);
|
|
45713
|
+
for (const token of wallet2.tokens ?? [])
|
|
45714
|
+
wanted.add(token.mint);
|
|
45715
|
+
}
|
|
45716
|
+
const missing = [...wanted].filter((mint) => prices[mint] === undefined);
|
|
45717
|
+
for (let at = 0;at < missing.length; at += CHUNK) {
|
|
45718
|
+
const priced = await apiRequest("/api/v1/agent/prices", {
|
|
45719
|
+
method: "POST",
|
|
45720
|
+
body: { mints: missing.slice(at, at + CHUNK) },
|
|
45721
|
+
auth: "key",
|
|
45722
|
+
credentials: { apiKey },
|
|
45723
|
+
apiUrl: ctx.apiUrl,
|
|
45724
|
+
fetch: deps.fetch,
|
|
45725
|
+
env: deps.env
|
|
45726
|
+
});
|
|
45727
|
+
if (priced.ok)
|
|
45728
|
+
Object.assign(prices, priced.body.prices ?? {});
|
|
45729
|
+
else
|
|
45730
|
+
priceFailure ??= priced.message;
|
|
45731
|
+
}
|
|
45732
|
+
}
|
|
45733
|
+
if (priceFailure !== undefined) {
|
|
45734
|
+
deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
|
|
45735
|
+
`);
|
|
45736
|
+
}
|
|
45737
|
+
const lpByWallet = new Map;
|
|
45738
|
+
const lpOf = (address) => {
|
|
45739
|
+
const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
|
|
45740
|
+
lpByWallet.set(address, entry);
|
|
45741
|
+
return entry;
|
|
45742
|
+
};
|
|
45743
|
+
for (const position of fromCandle.lp?.positions ?? [])
|
|
45744
|
+
lpOf(position.wallet).positions.push(lpRow(position, prices));
|
|
45745
|
+
for (const unread of fromCandle.lp?.unreadable ?? [])
|
|
45746
|
+
lpOf(unread.wallet).unread.push(unread.position);
|
|
45747
|
+
const wallet = (address, read, extra) => {
|
|
45748
|
+
const holdings = read === undefined ? null : valueHoldings(read, prices);
|
|
45749
|
+
const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
|
|
45750
|
+
const lp2 = fromCandle.lp ? lpOf(address) : undefined;
|
|
45751
|
+
return {
|
|
45752
|
+
address,
|
|
45753
|
+
...extra,
|
|
45754
|
+
holdings,
|
|
45755
|
+
...unread.length > 0 ? { unread } : {},
|
|
45756
|
+
...lp2 ? { lpPositions: lp2.positions } : {},
|
|
45757
|
+
...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
|
|
45758
|
+
valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
45759
|
+
unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
45760
|
+
};
|
|
45761
|
+
};
|
|
45762
|
+
const group = (name, wallets, read, reason) => ({
|
|
45763
|
+
group: name,
|
|
45764
|
+
read,
|
|
45765
|
+
...reason !== undefined ? { reason } : {},
|
|
45766
|
+
wallets,
|
|
45767
|
+
valueUsd: wallets.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
45768
|
+
unpriced: wallets.reduce((sum, w) => sum + w.unpriced, 0)
|
|
45769
|
+
});
|
|
45770
|
+
const orNull = (read) => read.lamports === null && read.tokens === null ? undefined : read;
|
|
45771
|
+
const groups = [
|
|
45772
|
+
group("vault", (vaultEntries ?? []).map((entry) => {
|
|
45773
|
+
const read = vaultRead?.byAddress.get(entry.address);
|
|
45774
|
+
return wallet(entry.address, read ? orNull(read) : undefined, { label: entry.label, role: entry.role });
|
|
45775
|
+
}), vaultEntries !== undefined, vaultReason),
|
|
45776
|
+
group("tee", (fromCandle.tee ?? []).map((row) => wallet(row.address, orNull(row), {
|
|
45777
|
+
id: row.id,
|
|
45778
|
+
...row.label ? { label: row.label } : {},
|
|
45779
|
+
active: row.active
|
|
45780
|
+
})), true),
|
|
45781
|
+
group("embedded", (fromCandle.embedded ?? []).map((row) => wallet(row.address, orNull(row), {})), true)
|
|
45782
|
+
];
|
|
45783
|
+
const unavailable = [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []];
|
|
45784
|
+
const lpUnread = (fromCandle.lp?.unreadable ?? []).length;
|
|
45785
|
+
const complete = fromCandle.complete !== false && unavailable.length === 0 && lpUnread === 0;
|
|
45786
|
+
const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
|
|
45787
|
+
const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
|
|
45788
|
+
const lp = fromCandle.lp ? {
|
|
45789
|
+
positions: fromCandle.lp.positions.length,
|
|
45790
|
+
unpriced: fromCandle.lp.positions.filter((p) => p.valueUsd === null).length,
|
|
45791
|
+
unreadable: lpUnread,
|
|
45792
|
+
valueUsd: fromCandle.lp.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
|
|
45793
|
+
} : undefined;
|
|
45794
|
+
if (ctx.json) {
|
|
45795
|
+
writeJson(deps, {
|
|
45796
|
+
ok: true,
|
|
45797
|
+
totalUsd,
|
|
45798
|
+
unpriced,
|
|
45799
|
+
complete,
|
|
45800
|
+
unavailable,
|
|
45801
|
+
...rpcHost !== undefined ? { rpcHost } : {},
|
|
45802
|
+
...lp ? { lp } : {},
|
|
45803
|
+
groups
|
|
45804
|
+
});
|
|
45805
|
+
return complete ? 0 : 3;
|
|
45806
|
+
}
|
|
45807
|
+
writeTable(ctx, groups, { totalUsd, unpriced, unavailable: unavailable.length, lp });
|
|
45808
|
+
return complete ? 0 : 3;
|
|
45809
|
+
});
|
|
45810
|
+
}
|
|
45811
|
+
async function readOwnRpc(addresses, rpcUrl2, fetchFn) {
|
|
45812
|
+
const rpc2 = createSolanaRpc(rpcUrl2, fetchFn);
|
|
45813
|
+
const unique = [...new Set(addresses)];
|
|
45814
|
+
const lamports = new Map;
|
|
45815
|
+
for (let at = 0;at < unique.length; at += CHUNK) {
|
|
45816
|
+
const chunk = unique.slice(at, at + CHUNK);
|
|
45817
|
+
try {
|
|
45818
|
+
const accounts = await rpc2.getMultipleAccounts(chunk);
|
|
45819
|
+
for (const [i, address] of chunk.entries())
|
|
45820
|
+
lamports.set(address, (accounts[i]?.lamports ?? 0n).toString());
|
|
45821
|
+
} catch {
|
|
45822
|
+
for (const address of chunk)
|
|
45823
|
+
lamports.set(address, null);
|
|
45824
|
+
}
|
|
45825
|
+
}
|
|
45826
|
+
const tokens = new Map;
|
|
45827
|
+
const failed = new Set;
|
|
45828
|
+
const reads = unique.flatMap((owner) => [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) => ({ owner, programId })));
|
|
45829
|
+
let next = 0;
|
|
45830
|
+
await Promise.all(Array.from({ length: Math.min(TOKEN_READS_IN_FLIGHT, reads.length) }, async () => {
|
|
45831
|
+
while (next < reads.length) {
|
|
45832
|
+
const { owner, programId } = reads[next++];
|
|
45833
|
+
try {
|
|
45834
|
+
const accounts = await rpc2.getTokenAccountsByOwner(owner, programId);
|
|
45835
|
+
const held = tokens.get(owner) ?? new Map;
|
|
45836
|
+
tokens.set(owner, held);
|
|
45837
|
+
const program = programId === TOKEN_PROGRAM_ID ? "token" : "token-2022";
|
|
45838
|
+
for (const account of accounts) {
|
|
45839
|
+
if (!/^\d+$/.test(account.amountRaw) || BigInt(account.amountRaw) === 0n)
|
|
45840
|
+
continue;
|
|
45841
|
+
const key = `${program}:${account.mint}`;
|
|
45842
|
+
const prior = held.get(key);
|
|
45843
|
+
held.set(key, {
|
|
45844
|
+
mint: account.mint,
|
|
45845
|
+
amountRaw: (BigInt(prior?.amountRaw ?? "0") + BigInt(account.amountRaw)).toString(),
|
|
45846
|
+
decimals: account.decimals,
|
|
45847
|
+
program
|
|
45848
|
+
});
|
|
45849
|
+
}
|
|
45850
|
+
} catch {
|
|
45851
|
+
failed.add(owner);
|
|
45852
|
+
}
|
|
45853
|
+
}
|
|
45854
|
+
}));
|
|
45855
|
+
const byAddress = new Map;
|
|
45856
|
+
const unavailable = [];
|
|
45857
|
+
for (const address of unique) {
|
|
45858
|
+
const sol = lamports.get(address) ?? null;
|
|
45859
|
+
const held = failed.has(address) ? null : [...tokens.get(address)?.values() ?? []];
|
|
45860
|
+
byAddress.set(address, { lamports: sol, tokens: held });
|
|
45861
|
+
if (sol === null || held === null)
|
|
45862
|
+
unavailable.push(address);
|
|
45863
|
+
}
|
|
45864
|
+
return { byAddress, unavailable };
|
|
45865
|
+
}
|
|
45866
|
+
function valueHoldings(read, prices) {
|
|
45867
|
+
const raw = [];
|
|
45868
|
+
if (read.lamports !== null && BigInt(read.lamports) > 0n) {
|
|
45869
|
+
raw.push({ mint: SOL_MINT, amountRaw: read.lamports, decimals: 9, program: "native" });
|
|
45870
|
+
}
|
|
45871
|
+
for (const token of read.tokens ?? [])
|
|
45872
|
+
raw.push(token);
|
|
45873
|
+
return raw.map((h) => {
|
|
45874
|
+
const price = prices[h.mint];
|
|
45875
|
+
const priceUsd = price?.priceUsd ?? null;
|
|
45876
|
+
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
45877
|
+
return {
|
|
45878
|
+
...h,
|
|
45879
|
+
symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
|
|
45880
|
+
amount,
|
|
45881
|
+
priceUsd,
|
|
45882
|
+
valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
|
|
45883
|
+
priceSource: price?.source ?? null
|
|
45884
|
+
};
|
|
45885
|
+
});
|
|
45886
|
+
}
|
|
45887
|
+
function lpRow(position, prices) {
|
|
45888
|
+
return {
|
|
45889
|
+
position: position.position,
|
|
45890
|
+
pool: position.pool,
|
|
45891
|
+
tokens: position.tokens.map((token) => ({
|
|
45892
|
+
...token,
|
|
45893
|
+
symbol: KNOWN_SYMBOLS[token.mint] ?? token.symbol ?? prices[token.mint]?.symbol ?? null,
|
|
45894
|
+
amount: formatAmount(token.amountRaw, token.decimals),
|
|
45895
|
+
unclaimedFees: formatAmount(token.unclaimedFeesRaw, token.decimals)
|
|
45896
|
+
})),
|
|
45897
|
+
...position.poolShare !== undefined ? { poolShare: position.poolShare } : {},
|
|
45898
|
+
valueUsd: position.valueUsd,
|
|
45899
|
+
unpriced: position.unpriced
|
|
45900
|
+
};
|
|
45901
|
+
}
|
|
45902
|
+
function writeTable(ctx, groups, totals) {
|
|
45903
|
+
const { deps } = ctx;
|
|
45904
|
+
const rows = [];
|
|
45905
|
+
const lpRows = [];
|
|
45906
|
+
const notes = [];
|
|
45907
|
+
for (const g of groups) {
|
|
45908
|
+
const shown = g.wallets.filter((w) => w.holdings === null || w.holdings.length > 0 || (w.unread?.length ?? 0) > 0 || (w.lpPositions?.length ?? 0) > 0 || (w.lpUnread?.length ?? 0) > 0).sort((a, b) => b.valueUsd - a.valueUsd);
|
|
45909
|
+
let lpCount = 0;
|
|
45910
|
+
let lpUnread = 0;
|
|
45911
|
+
for (const w of shown) {
|
|
45912
|
+
const name = `${w.label ? `${w.label} ` : ""}(${shortAddress2(w.address)})${w.role === "external" ? " external" : ""}`;
|
|
45913
|
+
const side = (t, amount) => `${amount} ${t.symbol ?? shortAddress2(t.mint)}`;
|
|
45914
|
+
for (const p of [...w.lpPositions ?? []].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1))) {
|
|
45915
|
+
lpCount += 1;
|
|
45916
|
+
lpRows.push([
|
|
45917
|
+
g.group,
|
|
45918
|
+
name,
|
|
45919
|
+
shortAddress2(p.position),
|
|
45920
|
+
shortAddress2(p.pool),
|
|
45921
|
+
p.tokens.map((t) => side(t, t.amount)).join(" + "),
|
|
45922
|
+
p.tokens.map((t) => side(t, t.unclaimedFees)).join(" + "),
|
|
45923
|
+
p.valueUsd === null ? "unpriced" : formatUsd(p.valueUsd)
|
|
45924
|
+
]);
|
|
45925
|
+
}
|
|
45926
|
+
for (const position of w.lpUnread ?? []) {
|
|
45927
|
+
lpUnread += 1;
|
|
45928
|
+
lpRows.push([g.group, name, shortAddress2(position), "-", "not read", "-", "-"]);
|
|
45929
|
+
}
|
|
45930
|
+
if (w.holdings === null) {
|
|
45931
|
+
rows.push([g.group, name, "-", "not read", "-", "-"]);
|
|
45932
|
+
continue;
|
|
45933
|
+
}
|
|
45934
|
+
const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
|
|
45935
|
+
for (const h of sorted) {
|
|
45936
|
+
rows.push([
|
|
45937
|
+
g.group,
|
|
45938
|
+
name,
|
|
45939
|
+
h.symbol ?? shortAddress2(h.mint),
|
|
45940
|
+
h.amount,
|
|
45941
|
+
h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
|
|
45942
|
+
h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
|
|
45943
|
+
]);
|
|
45944
|
+
}
|
|
45945
|
+
for (const part of w.unread ?? [])
|
|
45946
|
+
rows.push([g.group, name, part === "sol" ? "SOL" : "tokens", "not read", "-", "-"]);
|
|
45947
|
+
}
|
|
45948
|
+
const empty = g.wallets.length - shown.length;
|
|
45949
|
+
const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
|
|
45950
|
+
notes.push([
|
|
45951
|
+
g.group,
|
|
45952
|
+
g.read ? formatUsd(g.valueUsd) : "-",
|
|
45953
|
+
g.read ? `${count}${empty > 0 ? `, ${empty} empty not shown` : ""}${lpCount > 0 ? `, ${lpCount} LP ${lpCount === 1 ? "position" : "positions"}` : ""}${lpUnread > 0 ? `, ${lpUnread} LP not read` : ""}${g.unpriced > 0 ? `, ${g.unpriced} unpriced` : ""}` : g.reason ?? "not read"
|
|
45954
|
+
]);
|
|
45955
|
+
}
|
|
45956
|
+
if (rows.length > 0)
|
|
45957
|
+
deps.stdout.write(`
|
|
45958
|
+
${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
45959
|
+
`);
|
|
45960
|
+
else
|
|
45961
|
+
deps.stdout.write(`
|
|
45962
|
+
Nothing held in any wallet read.
|
|
45963
|
+
`);
|
|
45964
|
+
if (lpRows.length > 0)
|
|
45965
|
+
deps.stdout.write(`
|
|
45966
|
+
LP positions
|
|
45967
|
+
${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
|
|
45968
|
+
`);
|
|
45969
|
+
notes.push([
|
|
45970
|
+
"total",
|
|
45971
|
+
formatUsd(totals.totalUsd),
|
|
45972
|
+
totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : "every holding priced"
|
|
45973
|
+
]);
|
|
45974
|
+
const width = Math.max(...notes.map(([label]) => label.length));
|
|
45975
|
+
const valueWidth = Math.max(...notes.map(([, value]) => value.length));
|
|
45976
|
+
deps.stdout.write(`
|
|
45977
|
+
`);
|
|
45978
|
+
for (const [label, value, note] of notes) {
|
|
45979
|
+
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
|
|
45980
|
+
`);
|
|
45981
|
+
}
|
|
45982
|
+
if (totals.unavailable > 0) {
|
|
45983
|
+
deps.stdout.write(`${totals.unavailable} ${totals.unavailable === 1 ? "wallet" : "wallets"} could not be read in full. What was not read is marked "not read" and is not in the total.
|
|
45984
|
+
`);
|
|
45985
|
+
}
|
|
45986
|
+
if ((totals.lp?.unreadable ?? 0) > 0) {
|
|
45987
|
+
const n = totals.lp?.unreadable ?? 0;
|
|
45988
|
+
deps.stdout.write(`${n} LP ${n === 1 ? "position" : "positions"} could not be read (the pool did not answer). ${n === 1 ? "It is" : "They are"} marked "not read" and not in the total.
|
|
45989
|
+
`);
|
|
45990
|
+
}
|
|
45991
|
+
}
|
|
45992
|
+
|
|
44621
45993
|
// src/commands/profile.ts
|
|
44622
45994
|
init_args();
|
|
44623
45995
|
init_profiles();
|
|
@@ -45353,7 +46725,7 @@ async function readMints(rpc2, mints) {
|
|
|
45353
46725
|
}
|
|
45354
46726
|
return out;
|
|
45355
46727
|
}
|
|
45356
|
-
function
|
|
46728
|
+
function formatAmount2(raw, decimals) {
|
|
45357
46729
|
if (decimals === undefined)
|
|
45358
46730
|
return `${raw} raw`;
|
|
45359
46731
|
const negative = raw < 0n;
|
|
@@ -45363,7 +46735,7 @@ function formatAmount(raw, decimals) {
|
|
|
45363
46735
|
return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
|
|
45364
46736
|
}
|
|
45365
46737
|
function formatSol(lamports) {
|
|
45366
|
-
return `${
|
|
46738
|
+
return `${formatAmount2(lamports, 9)} SOL`;
|
|
45367
46739
|
}
|
|
45368
46740
|
function displayLines(input) {
|
|
45369
46741
|
const { tx, compiled, signers, simulation, mints } = input;
|
|
@@ -45386,7 +46758,7 @@ function displayLines(input) {
|
|
|
45386
46758
|
}
|
|
45387
46759
|
for (const token of deltas.tokens.filter((delta) => delta.owner === entry.address)) {
|
|
45388
46760
|
const info = mints.get(token.mint);
|
|
45389
|
-
lines.push(` ${token.mint}: ${
|
|
46761
|
+
lines.push(` ${token.mint}: ${formatAmount2(token.before, info?.decimals)} -> ${formatAmount2(token.after, info?.decimals)} (${token.after >= token.before ? "+" : ""}${formatAmount2(token.after - token.before, info?.decimals)}${info?.decimals === undefined ? "" : `, ${info.decimals} dp`}) in ${token.account}`);
|
|
45390
46762
|
}
|
|
45391
46763
|
}
|
|
45392
46764
|
const others = [];
|
|
@@ -45401,7 +46773,7 @@ function displayLines(input) {
|
|
|
45401
46773
|
if (signerAddresses.has(token.owner) || token.after <= token.before)
|
|
45402
46774
|
continue;
|
|
45403
46775
|
const info = mints.get(token.mint);
|
|
45404
|
-
others.push(`${token.owner} receives +${
|
|
46776
|
+
others.push(`${token.owner} receives +${formatAmount2(token.after - token.before, info?.decimals)} of ${token.mint} (account ${token.account})`);
|
|
45405
46777
|
}
|
|
45406
46778
|
if (others.length === 0)
|
|
45407
46779
|
lines.push("others no other account gains a balance in the simulation");
|
|
@@ -45689,6 +47061,285 @@ async function signMessage2(args, ctx) {
|
|
|
45689
47061
|
init_esm();
|
|
45690
47062
|
init_args();
|
|
45691
47063
|
init_deps();
|
|
47064
|
+
|
|
47065
|
+
// src/lp-close.ts
|
|
47066
|
+
init_solana_lite();
|
|
47067
|
+
var DAMM_V2_PROGRAM_ID = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
|
|
47068
|
+
var CLOSE_PROGRAM_ALLOWLIST = new Set([
|
|
47069
|
+
DAMM_V2_PROGRAM_ID,
|
|
47070
|
+
TOKEN_PROGRAM_ID,
|
|
47071
|
+
TOKEN_2022_PROGRAM_ID,
|
|
47072
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
47073
|
+
COMPUTE_BUDGET_PROGRAM_ID,
|
|
47074
|
+
SYSTEM_PROGRAM_ID
|
|
47075
|
+
]);
|
|
47076
|
+
var DAMM_POSITION_CLOSE_UNAVAILABLE = "DAMM_POSITION_CLOSE_UNAVAILABLE";
|
|
47077
|
+
var DAMM_CLOSE_TRANSACTION_REFUSED = "DAMM_CLOSE_TRANSACTION_REFUSED";
|
|
47078
|
+
function isPositionCandidate(account) {
|
|
47079
|
+
return account.programId === TOKEN_2022_PROGRAM_ID && account.amountRaw === "1" && account.decimals === 0;
|
|
47080
|
+
}
|
|
47081
|
+
function parseCloseArtifact(body) {
|
|
47082
|
+
if (!body || typeof body !== "object")
|
|
47083
|
+
return;
|
|
47084
|
+
const { transaction, accountKeys } = body;
|
|
47085
|
+
if (typeof transaction !== "string" || transaction.length === 0)
|
|
47086
|
+
return;
|
|
47087
|
+
if (!Array.isArray(accountKeys) || !accountKeys.every((key) => typeof key === "string" && key.length > 0))
|
|
47088
|
+
return;
|
|
47089
|
+
return { transaction, accountKeys };
|
|
47090
|
+
}
|
|
47091
|
+
var MINT_BASE_SIZE2 = 82;
|
|
47092
|
+
var ACCOUNT_TYPE_OFFSET2 = 165;
|
|
47093
|
+
var ACCOUNT_TYPE_MINT = 1;
|
|
47094
|
+
var MULTISIG_SIZE2 = 355;
|
|
47095
|
+
var TOKEN_ACCOUNT_SIZE2 = 165;
|
|
47096
|
+
var TOKEN_ACCOUNT_STATE_INITIALIZED = 1;
|
|
47097
|
+
var CONTROL_RANGES = [
|
|
47098
|
+
[0, 32, "mint"],
|
|
47099
|
+
[32, 64, "authority"],
|
|
47100
|
+
[72, 108, "delegate"],
|
|
47101
|
+
[108, 109, "state"],
|
|
47102
|
+
[121, 129, "delegated amount"],
|
|
47103
|
+
[129, 165, "close authority"]
|
|
47104
|
+
];
|
|
47105
|
+
var TOKEN_IX_CLOSE_ACCOUNT = 9;
|
|
47106
|
+
var ATA_IX_CREATE = 0;
|
|
47107
|
+
var ATA_IX_CREATE_IDEMPOTENT = 1;
|
|
47108
|
+
function isTokenProgram(owner) {
|
|
47109
|
+
return owner === TOKEN_PROGRAM_ID || owner === TOKEN_2022_PROGRAM_ID;
|
|
47110
|
+
}
|
|
47111
|
+
function isMintAccount(view) {
|
|
47112
|
+
if (view === null || !isTokenProgram(view.owner))
|
|
47113
|
+
return false;
|
|
47114
|
+
if (view.data.length === MINT_BASE_SIZE2)
|
|
47115
|
+
return true;
|
|
47116
|
+
if (view.data.length === MULTISIG_SIZE2)
|
|
47117
|
+
return false;
|
|
47118
|
+
return view.data.length > ACCOUNT_TYPE_OFFSET2 && view.data[ACCOUNT_TYPE_OFFSET2] === ACCOUNT_TYPE_MINT;
|
|
47119
|
+
}
|
|
47120
|
+
function isUnsigned(tx) {
|
|
47121
|
+
return tx.signatures.every((slot) => slot.every((byte) => byte === 0));
|
|
47122
|
+
}
|
|
47123
|
+
function bytesEqual2(a, b) {
|
|
47124
|
+
return a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
47125
|
+
}
|
|
47126
|
+
function allZero(bytes) {
|
|
47127
|
+
return bytes.every((byte) => byte === 0);
|
|
47128
|
+
}
|
|
47129
|
+
async function verifyCloseArtifact(input) {
|
|
47130
|
+
const { artifact, rpc: rpc2, tee, vault, nftMint, nftAccount } = input;
|
|
47131
|
+
const refuse2 = (reason) => ({ ok: false, reason });
|
|
47132
|
+
const allowedOwner = (address) => address === tee || address === vault;
|
|
47133
|
+
let tx;
|
|
47134
|
+
try {
|
|
47135
|
+
tx = decodeTransaction(decodeStrictBase64(artifact.transaction));
|
|
47136
|
+
} catch (error) {
|
|
47137
|
+
if (error instanceof TransactionDecodeError)
|
|
47138
|
+
return refuse2(`the close transaction is undecodable: ${error.message}`);
|
|
47139
|
+
return refuse2(`the close transaction could not be read: ${error instanceof Error ? error.message : error}`);
|
|
47140
|
+
}
|
|
47141
|
+
if (tx.message.version !== 0)
|
|
47142
|
+
return refuse2("the close transaction is not a v0 message");
|
|
47143
|
+
if (!isUnsigned(tx))
|
|
47144
|
+
return refuse2("the close transaction already carries a signature; close-build must return it unsigned");
|
|
47145
|
+
if (tx.message.numRequiredSignatures !== 1)
|
|
47146
|
+
return refuse2(`the close transaction requires ${tx.message.numRequiredSignatures} signers; a position close needs only the TEE wallet`);
|
|
47147
|
+
if (tx.message.staticKeys[0] !== tee)
|
|
47148
|
+
return refuse2(`the fee payer is ${tx.message.staticKeys[0]}, not the TEE wallet`);
|
|
47149
|
+
let compiled;
|
|
47150
|
+
try {
|
|
47151
|
+
compiled = await resolveCompiledKeys(tx.message, rpc2);
|
|
47152
|
+
} catch (error) {
|
|
47153
|
+
if (error instanceof LookupTableError)
|
|
47154
|
+
return refuse2(`a lookup table could not be resolved: ${error.message}`);
|
|
47155
|
+
return refuse2(`the lookup tables could not be read: ${error instanceof Error ? error.message : error}`);
|
|
47156
|
+
}
|
|
47157
|
+
if (compiled.keys.length !== artifact.accountKeys.length)
|
|
47158
|
+
return refuse2(`the server listed ${artifact.accountKeys.length} account keys and the transaction resolves to ${compiled.keys.length}`);
|
|
47159
|
+
for (const [i, key] of compiled.keys.entries()) {
|
|
47160
|
+
if (artifact.accountKeys[i] !== key)
|
|
47161
|
+
return refuse2(`account key ${i} is ${key} in the transaction and ${artifact.accountKeys[i]} in the server's ordered array`);
|
|
47162
|
+
}
|
|
47163
|
+
let dammCalls = 0;
|
|
47164
|
+
for (const [i, ix] of tx.message.instructions.entries()) {
|
|
47165
|
+
const program = compiled.keys[ix.programIdIndex];
|
|
47166
|
+
const account = (at) => compiled.keys[ix.accountIndexes[at] ?? -1];
|
|
47167
|
+
if (program === undefined || !CLOSE_PROGRAM_ALLOWLIST.has(program))
|
|
47168
|
+
return refuse2(`instruction ${i} calls ${program ?? "an unknown program"}, which a position close never does`);
|
|
47169
|
+
switch (program) {
|
|
47170
|
+
case DAMM_V2_PROGRAM_ID:
|
|
47171
|
+
dammCalls += 1;
|
|
47172
|
+
break;
|
|
47173
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
47174
|
+
break;
|
|
47175
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID: {
|
|
47176
|
+
const kind = ix.data.length === 0 ? ATA_IX_CREATE : ix.data.length === 1 ? ix.data[0] : -1;
|
|
47177
|
+
if (kind !== ATA_IX_CREATE && kind !== ATA_IX_CREATE_IDEMPOTENT)
|
|
47178
|
+
return refuse2(`instruction ${i} is an Associated Token instruction other than Create, which a close never needs`);
|
|
47179
|
+
if (account(0) !== tee || !allowedOwner(account(2) ?? ""))
|
|
47180
|
+
return refuse2(`instruction ${i} creates a token account for ${account(2) ?? "?"}, not the TEE wallet or its vault`);
|
|
47181
|
+
break;
|
|
47182
|
+
}
|
|
47183
|
+
case TOKEN_PROGRAM_ID:
|
|
47184
|
+
case TOKEN_2022_PROGRAM_ID: {
|
|
47185
|
+
if (ix.data[0] !== TOKEN_IX_CLOSE_ACCOUNT || ix.data.length !== 1)
|
|
47186
|
+
return refuse2(`instruction ${i} is a token instruction other than CloseAccount, which a close never issues at the top level`);
|
|
47187
|
+
if (account(1) !== tee || account(2) !== tee)
|
|
47188
|
+
return refuse2(`instruction ${i} closes a token account to ${account(1) ?? "?"}, not the TEE wallet`);
|
|
47189
|
+
break;
|
|
47190
|
+
}
|
|
47191
|
+
default:
|
|
47192
|
+
return refuse2(`instruction ${i} is a top-level System instruction, which a close never issues`);
|
|
47193
|
+
}
|
|
47194
|
+
}
|
|
47195
|
+
if (dammCalls === 0)
|
|
47196
|
+
return refuse2("the transaction calls no DAMM v2 instruction, so it cannot close a position");
|
|
47197
|
+
let simulation;
|
|
47198
|
+
try {
|
|
47199
|
+
simulation = await simulateWithSnapshots(rpc2, artifact.transaction, compiled);
|
|
47200
|
+
} catch (error) {
|
|
47201
|
+
return refuse2(`the simulation could not be run: ${error instanceof Error ? error.message : error}`);
|
|
47202
|
+
}
|
|
47203
|
+
if (simulation.result.err !== null && simulation.result.err !== undefined)
|
|
47204
|
+
return refuse2(`the simulation failed: ${JSON.stringify(simulation.result.err)}`);
|
|
47205
|
+
const deltas = computeDeltas(simulation.snapshots);
|
|
47206
|
+
const postState = new Map(simulation.snapshots.map((snapshot) => [snapshot.address, snapshot.after]));
|
|
47207
|
+
for (const token of deltas.tokens) {
|
|
47208
|
+
if (token.after > token.before && !allowedOwner(token.owner))
|
|
47209
|
+
return refuse2(`${token.owner} would receive ${token.after - token.before} raw of ${token.mint} in account ${token.account}`);
|
|
47210
|
+
}
|
|
47211
|
+
for (const sol of deltas.sol) {
|
|
47212
|
+
if (sol.after <= sol.before)
|
|
47213
|
+
continue;
|
|
47214
|
+
if (allowedOwner(sol.address))
|
|
47215
|
+
continue;
|
|
47216
|
+
const balance = tokenBalanceOf(postState.get(sol.address) ?? null);
|
|
47217
|
+
if (balance !== undefined && allowedOwner(balance.owner))
|
|
47218
|
+
continue;
|
|
47219
|
+
return refuse2(`${sol.address} would receive ${sol.after - sol.before} lamports`);
|
|
47220
|
+
}
|
|
47221
|
+
const nftSnapshot = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
|
|
47222
|
+
if (nftSnapshot === undefined)
|
|
47223
|
+
return refuse2(`the position NFT account ${nftAccount} is not written by this transaction`);
|
|
47224
|
+
const nftAfter = tokenBalanceOf(nftSnapshot.after);
|
|
47225
|
+
if (nftAfter !== undefined && nftAfter.amount !== 0n)
|
|
47226
|
+
return refuse2(`the position NFT account ${nftAccount} still holds ${nftAfter.amount} after the simulation`);
|
|
47227
|
+
for (const { address, before, after } of simulation.snapshots) {
|
|
47228
|
+
if (allowedOwner(address)) {
|
|
47229
|
+
if (after === null || after.owner !== SYSTEM_PROGRAM_ID || after.data.length !== 0)
|
|
47230
|
+
return refuse2(`${address === tee ? "the TEE wallet" : "the vault"} would no longer be a plain System account after this transaction`);
|
|
47231
|
+
}
|
|
47232
|
+
if (after === null)
|
|
47233
|
+
continue;
|
|
47234
|
+
if (before !== null && after.owner !== before.owner)
|
|
47235
|
+
return refuse2(`writable account ${address} would change owner from ${before.owner} to ${after.owner}`);
|
|
47236
|
+
if (before !== null) {
|
|
47237
|
+
const controlled = tokenBalanceOf(before);
|
|
47238
|
+
if (controlled !== undefined && allowedOwner(controlled.owner)) {
|
|
47239
|
+
if (after.data.length < TOKEN_ACCOUNT_SIZE2)
|
|
47240
|
+
return refuse2(`token account ${address} would no longer decode as a token account`);
|
|
47241
|
+
for (const [start, end, what] of CONTROL_RANGES) {
|
|
47242
|
+
if (!bytesEqual2(before.data.subarray(start, end), after.data.subarray(start, end)))
|
|
47243
|
+
return refuse2(`token account ${address} would change its ${what}, which a close never does`);
|
|
47244
|
+
}
|
|
47245
|
+
continue;
|
|
47246
|
+
}
|
|
47247
|
+
}
|
|
47248
|
+
const created = before === null ? tokenBalanceOf(after) : undefined;
|
|
47249
|
+
if (created !== undefined) {
|
|
47250
|
+
if (!allowedOwner(created.owner))
|
|
47251
|
+
return refuse2(`created token account ${address} would be controlled by ${created.owner}`);
|
|
47252
|
+
if (!allZero(after.data.subarray(72, 108)) || after.data[108] !== TOKEN_ACCOUNT_STATE_INITIALIZED || !allZero(after.data.subarray(121, 129)) || !allZero(after.data.subarray(129, 165)))
|
|
47253
|
+
return refuse2(`created token account ${address} would start with a delegate, a close authority, or a frozen state`);
|
|
47254
|
+
}
|
|
47255
|
+
}
|
|
47256
|
+
const infos = new Map;
|
|
47257
|
+
const infoOf = async (address) => {
|
|
47258
|
+
if (!infos.has(address))
|
|
47259
|
+
infos.set(address, await rpc2.getAccountInfo(address));
|
|
47260
|
+
return infos.get(address) ?? null;
|
|
47261
|
+
};
|
|
47262
|
+
const canonical = new Set;
|
|
47263
|
+
for (const key of compiled.keys) {
|
|
47264
|
+
const view = await infoOf(key);
|
|
47265
|
+
if (!isMintAccount(view) || view === null)
|
|
47266
|
+
continue;
|
|
47267
|
+
const mint = decodePubkey(key);
|
|
47268
|
+
const program = decodePubkey(view.owner);
|
|
47269
|
+
canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(tee), mint, program)));
|
|
47270
|
+
canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(vault), mint, program)));
|
|
47271
|
+
}
|
|
47272
|
+
for (const [i, key] of compiled.keys.entries()) {
|
|
47273
|
+
if (!compiled.isWritable[i])
|
|
47274
|
+
continue;
|
|
47275
|
+
const view = await infoOf(key);
|
|
47276
|
+
if (view === null) {
|
|
47277
|
+
if (canonical.has(key))
|
|
47278
|
+
continue;
|
|
47279
|
+
return refuse2(`writable account ${key} does not exist and is not a TEE or vault associated token account for a mint this transaction names`);
|
|
47280
|
+
}
|
|
47281
|
+
if (isTokenProgram(view.owner)) {
|
|
47282
|
+
if (key === nftMint)
|
|
47283
|
+
continue;
|
|
47284
|
+
const balance = tokenBalanceOf(view);
|
|
47285
|
+
if (balance === undefined)
|
|
47286
|
+
return refuse2(`writable account ${key} is owned by a token program but is not a token account`);
|
|
47287
|
+
if (allowedOwner(balance.owner) || key === nftAccount)
|
|
47288
|
+
continue;
|
|
47289
|
+
const authority = await infoOf(balance.owner);
|
|
47290
|
+
if (authority !== null && authority.owner === DAMM_V2_PROGRAM_ID)
|
|
47291
|
+
continue;
|
|
47292
|
+
return refuse2(`writable token account ${key} is controlled by ${balance.owner}, which is neither this wallet, its vault, nor a DAMM v2 pool`);
|
|
47293
|
+
}
|
|
47294
|
+
if (view.owner === SYSTEM_PROGRAM_ID) {
|
|
47295
|
+
if (allowedOwner(key))
|
|
47296
|
+
continue;
|
|
47297
|
+
return refuse2(`writable system account ${key} is neither the TEE wallet nor its vault`);
|
|
47298
|
+
}
|
|
47299
|
+
if (view.owner !== DAMM_V2_PROGRAM_ID)
|
|
47300
|
+
return refuse2(`writable account ${key} is owned by ${view.owner}, not DAMM v2`);
|
|
47301
|
+
}
|
|
47302
|
+
return { ok: true, tx, compiled, simulation };
|
|
47303
|
+
}
|
|
47304
|
+
function formatSol2(lamports) {
|
|
47305
|
+
const negative = lamports < 0n;
|
|
47306
|
+
const abs = negative ? -lamports : lamports;
|
|
47307
|
+
const whole = abs / 1000000000n;
|
|
47308
|
+
const frac = (abs % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
|
|
47309
|
+
return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""} SOL`;
|
|
47310
|
+
}
|
|
47311
|
+
function describeClose(input) {
|
|
47312
|
+
const { verdict, tee, vault, nftMint, nftAccount } = input;
|
|
47313
|
+
const { tx, compiled, simulation } = verdict;
|
|
47314
|
+
const lines = [];
|
|
47315
|
+
lines.push(`message v0, ${tx.message.instructions.length} instruction(s), ${compiled.keys.length} account(s)${tx.message.lookups.length > 0 ? ` (${tx.message.lookups.length} lookup table(s) resolved)` : ""}, blockhash ${tx.message.recentBlockhash}`);
|
|
47316
|
+
lines.push(`fee payer ${compiled.keys[0]} (this TEE wallet)`);
|
|
47317
|
+
lines.push(`position NFT mint ${nftMint}, account ${nftAccount}`);
|
|
47318
|
+
const programs = [...new Set(tx.message.instructions.map((ix) => compiled.keys[ix.programIdIndex] ?? "?"))];
|
|
47319
|
+
for (const program of programs)
|
|
47320
|
+
lines.push(`program ${program === DAMM_V2_PROGRAM_ID ? `Meteora DAMM v2 (${program})` : programNameOf(program)}`);
|
|
47321
|
+
const deltas = computeDeltas(simulation.snapshots);
|
|
47322
|
+
const who = (address) => address === tee ? "wallet" : address === vault ? "vault" : address;
|
|
47323
|
+
for (const address of [tee, vault]) {
|
|
47324
|
+
const sol = deltas.sol.find((delta) => delta.address === address);
|
|
47325
|
+
if (sol && sol.after !== sol.before)
|
|
47326
|
+
lines.push(`${who(address).padEnd(11)} ${formatSol2(sol.before)} -> ${formatSol2(sol.after)} (${sol.after >= sol.before ? "+" : ""}${formatSol2(sol.after - sol.before)})`);
|
|
47327
|
+
for (const token of deltas.tokens.filter((delta) => delta.owner === address && delta.after !== delta.before)) {
|
|
47328
|
+
lines.push(`${who(address).padEnd(11)} ${token.mint}: ${token.before} -> ${token.after} raw (${token.after >= token.before ? "+" : ""}${token.after - token.before}) in ${token.account}${token.account === nftAccount ? " (position NFT, burned)" : ""}`);
|
|
47329
|
+
}
|
|
47330
|
+
}
|
|
47331
|
+
const nft = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
|
|
47332
|
+
if (nft && nft.after === null)
|
|
47333
|
+
lines.push(`position NFT account ${nftAccount} is closed by this transaction`);
|
|
47334
|
+
const others = deltas.tokens.filter((token) => token.owner !== tee && token.owner !== vault && token.after !== token.before);
|
|
47335
|
+
for (const token of others)
|
|
47336
|
+
lines.push(`${token.owner === undefined ? "?" : "pool"} ${token.mint}: ${token.before} -> ${token.after} raw in ${token.account} (authority ${token.owner})`);
|
|
47337
|
+
lines.push("checked ordered keys, programs, simulation, positive deltas, control, writable accounts: all passed");
|
|
47338
|
+
lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
|
|
47339
|
+
return lines;
|
|
47340
|
+
}
|
|
47341
|
+
|
|
47342
|
+
// src/commands/tee.ts
|
|
45692
47343
|
init_profiles();
|
|
45693
47344
|
init_render();
|
|
45694
47345
|
init_solana_lite();
|
|
@@ -48994,6 +50645,11 @@ init_deps();
|
|
|
48994
50645
|
init_profiles();
|
|
48995
50646
|
init_render();
|
|
48996
50647
|
init_secret_store();
|
|
50648
|
+
function trustedCell(row) {
|
|
50649
|
+
if (row.revokedAt)
|
|
50650
|
+
return "-";
|
|
50651
|
+
return row.addedVia === "session" || typeof row.trustedAt === "number" ? "yes" : "no";
|
|
50652
|
+
}
|
|
48997
50653
|
var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
|
|
48998
50654
|
function isTeeRow(row) {
|
|
48999
50655
|
return row.profile !== undefined && TEE_PROFILES.has(row.profile);
|
|
@@ -49044,6 +50700,9 @@ var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade
|
|
|
49044
50700
|
var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
|
|
49045
50701
|
` + `This account's embedded wallet, shown above, can pay for a token trade too.
|
|
49046
50702
|
`;
|
|
50703
|
+
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.
|
|
50704
|
+
` + `Run: candle wallets trust <label|address|prefix*> (the device token, never an API key)
|
|
50705
|
+
`;
|
|
49047
50706
|
var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
|
|
49048
50707
|
`;
|
|
49049
50708
|
async function probeSignerStates(rows, store) {
|
|
@@ -49150,24 +50809,28 @@ Linked wallets (${linkedRows.length}):
|
|
|
49150
50809
|
`);
|
|
49151
50810
|
} else {
|
|
49152
50811
|
const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
|
|
49153
|
-
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
|
|
50812
|
+
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Trusted", "Signer"], linkedRows.map((wallet, index) => [
|
|
49154
50813
|
wallet._id,
|
|
49155
50814
|
wallet.chain,
|
|
49156
50815
|
wallet.address,
|
|
49157
50816
|
wallet.label ?? "-",
|
|
49158
50817
|
isTeeRow(wallet) ? "tee" : "linked",
|
|
49159
50818
|
wallet.revokedAt ? "yes" : "no",
|
|
50819
|
+
trustedCell(wallet),
|
|
49160
50820
|
cells[index] ?? "-"
|
|
49161
50821
|
]))}
|
|
49162
50822
|
`);
|
|
49163
50823
|
const anyNone = cells.includes("none");
|
|
49164
50824
|
const anyStale = cells.includes("stale");
|
|
49165
50825
|
const anyTee = linkedRows.some(isTeeRow);
|
|
49166
|
-
|
|
50826
|
+
const anyUntrusted = linkedRows.some((row) => trustedCell(row) === "no");
|
|
50827
|
+
if (anyNone || anyStale || anyTee || anyUntrusted)
|
|
49167
50828
|
deps.stdout.write(`
|
|
49168
50829
|
`);
|
|
49169
50830
|
if (anyTee)
|
|
49170
50831
|
deps.stdout.write(TEE_HINT);
|
|
50832
|
+
if (anyUntrusted)
|
|
50833
|
+
deps.stdout.write(UNTRUSTED_HINT);
|
|
49171
50834
|
if (anyNone)
|
|
49172
50835
|
deps.stdout.write(NONE_HINT);
|
|
49173
50836
|
if (anyStale)
|
|
@@ -49429,8 +51092,8 @@ function readDisableOutcome(body) {
|
|
|
49429
51092
|
var MIN_PASSPHRASE_LENGTH = 12;
|
|
49430
51093
|
var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
49431
51094
|
var RPC_URL_ENV2 = "CANDLE_SOLANA_RPC_URL";
|
|
49432
|
-
var
|
|
49433
|
-
var
|
|
51095
|
+
var CONFIRM_POLL_MS3 = 2000;
|
|
51096
|
+
var CONFIRM_MAX_POLLS3 = 45;
|
|
49434
51097
|
function refuseEnvPassphrase2(ctx) {
|
|
49435
51098
|
if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
|
|
49436
51099
|
return true;
|
|
@@ -49441,7 +51104,7 @@ function refuseEnvPassphrase2(ctx) {
|
|
|
49441
51104
|
}, ctx.json);
|
|
49442
51105
|
return false;
|
|
49443
51106
|
}
|
|
49444
|
-
function
|
|
51107
|
+
function usage4(ctx, line) {
|
|
49445
51108
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
49446
51109
|
return 2;
|
|
49447
51110
|
}
|
|
@@ -49661,7 +51324,7 @@ async function openExistingTeeStore(ctx, parsed) {
|
|
|
49661
51324
|
const { deps, json } = ctx;
|
|
49662
51325
|
const found = await readTeeStore(ctx, parsed);
|
|
49663
51326
|
if ("usage" in found)
|
|
49664
|
-
return { ok: false, code:
|
|
51327
|
+
return { ok: false, code: usage4(ctx, found.usage) };
|
|
49665
51328
|
const { path } = found;
|
|
49666
51329
|
if ("error" in found) {
|
|
49667
51330
|
writeLocalFailure(deps, {
|
|
@@ -49758,12 +51421,12 @@ async function teeNew(args, ctx) {
|
|
|
49758
51421
|
return 1;
|
|
49759
51422
|
const parsed = parseArgs(args, { valueFlags: ["--label", "--keystore"], pathFlags: ["--keystore"] });
|
|
49760
51423
|
if ("error" in parsed)
|
|
49761
|
-
return
|
|
51424
|
+
return usage4(ctx, parsed.error);
|
|
49762
51425
|
if (parsed.positionals.length > 0)
|
|
49763
|
-
return
|
|
51426
|
+
return usage4(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
49764
51427
|
const found = await readTeeStore(ctx, parsed);
|
|
49765
51428
|
if ("usage" in found)
|
|
49766
|
-
return
|
|
51429
|
+
return usage4(ctx, found.usage);
|
|
49767
51430
|
const { path } = found;
|
|
49768
51431
|
if ("error" in found) {
|
|
49769
51432
|
writeLocalFailure(deps, {
|
|
@@ -49840,15 +51503,15 @@ async function teeEnable(args, ctx) {
|
|
|
49840
51503
|
pathFlags: ["--keystore"]
|
|
49841
51504
|
});
|
|
49842
51505
|
if ("error" in parsed)
|
|
49843
|
-
return
|
|
51506
|
+
return usage4(ctx, parsed.error);
|
|
49844
51507
|
const [address, extra] = parsed.positionals;
|
|
49845
51508
|
if (!address || extra !== undefined) {
|
|
49846
|
-
return
|
|
51509
|
+
return usage4(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
|
|
49847
51510
|
}
|
|
49848
51511
|
const vaultFlag = parsed.values["--vault"];
|
|
49849
51512
|
const vaultKey = parsed.values["--vault-key"];
|
|
49850
51513
|
if (vaultFlag !== undefined && vaultKey !== undefined) {
|
|
49851
|
-
return
|
|
51514
|
+
return usage4(ctx, "Use either --vault or --vault-key, not both.");
|
|
49852
51515
|
}
|
|
49853
51516
|
let vault = vaultFlag;
|
|
49854
51517
|
if (vaultKey !== undefined) {
|
|
@@ -49858,7 +51521,7 @@ async function teeEnable(args, ctx) {
|
|
|
49858
51521
|
const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
|
|
49859
51522
|
const resolvedVault = vaultPathFor2(ctx, parsed);
|
|
49860
51523
|
if ("error" in resolvedVault)
|
|
49861
|
-
return
|
|
51524
|
+
return usage4(ctx, resolvedVault.error);
|
|
49862
51525
|
const vaultPath = resolvedVault.path;
|
|
49863
51526
|
const raw = await readVaultRaw2(vaultPath);
|
|
49864
51527
|
if (raw === null) {
|
|
@@ -49896,11 +51559,11 @@ async function teeEnable(args, ctx) {
|
|
|
49896
51559
|
}
|
|
49897
51560
|
}
|
|
49898
51561
|
if (!vault)
|
|
49899
|
-
return
|
|
51562
|
+
return usage4(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
|
|
49900
51563
|
if (!isSolanaAddress(vault))
|
|
49901
|
-
return
|
|
51564
|
+
return usage4(ctx, "--vault is not a valid Solana address.");
|
|
49902
51565
|
if (vault === address)
|
|
49903
|
-
return
|
|
51566
|
+
return usage4(ctx, "--vault must be a different address from the TEE wallet.");
|
|
49904
51567
|
const vaultOwned = await addressOwnedByVault(ctx, address);
|
|
49905
51568
|
if (vaultOwned === "usage")
|
|
49906
51569
|
return 2;
|
|
@@ -50037,20 +51700,20 @@ async function teeFund(args, ctx) {
|
|
|
50037
51700
|
return 1;
|
|
50038
51701
|
const parsed = parseArgs(args, { valueFlags: ["--amount", "--asset", "--keystore"], pathFlags: ["--keystore"] });
|
|
50039
51702
|
if ("error" in parsed)
|
|
50040
|
-
return
|
|
51703
|
+
return usage4(ctx, parsed.error);
|
|
50041
51704
|
const [address, extra] = parsed.positionals;
|
|
50042
51705
|
if (!address || extra !== undefined)
|
|
50043
|
-
return
|
|
51706
|
+
return usage4(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
|
|
50044
51707
|
const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
|
|
50045
51708
|
if (asset !== "SOL" && asset !== "USDC")
|
|
50046
|
-
return
|
|
51709
|
+
return usage4(ctx, "--asset must be SOL or USDC.");
|
|
50047
51710
|
const amount = parsed.values["--amount"];
|
|
50048
51711
|
if (!amount)
|
|
50049
|
-
return
|
|
51712
|
+
return usage4(ctx, "--amount <n> is required.");
|
|
50050
51713
|
const decimals = asset === "SOL" ? 9 : 6;
|
|
50051
51714
|
const raw = decimalToRaw(amount, decimals);
|
|
50052
51715
|
if (raw === null || raw === 0n)
|
|
50053
|
-
return
|
|
51716
|
+
return usage4(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
|
|
50054
51717
|
const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
|
|
50055
51718
|
if (!resolved.ok)
|
|
50056
51719
|
return resolved.code;
|
|
@@ -50117,10 +51780,10 @@ async function teeStatus(args, ctx) {
|
|
|
50117
51780
|
return 1;
|
|
50118
51781
|
const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
|
|
50119
51782
|
if ("error" in parsed)
|
|
50120
|
-
return
|
|
51783
|
+
return usage4(ctx, parsed.error);
|
|
50121
51784
|
const [address, extra] = parsed.positionals;
|
|
50122
51785
|
if (!address || extra !== undefined)
|
|
50123
|
-
return
|
|
51786
|
+
return usage4(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
|
|
50124
51787
|
const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
|
|
50125
51788
|
if (!resolved.ok)
|
|
50126
51789
|
return resolved.code;
|
|
@@ -50162,7 +51825,7 @@ async function teeStatus(args, ctx) {
|
|
|
50162
51825
|
if (rpcUrl2) {
|
|
50163
51826
|
const checked = rpcUrlFrom2(ctx, parsed);
|
|
50164
51827
|
if (typeof checked !== "string")
|
|
50165
|
-
return
|
|
51828
|
+
return usage4(ctx, checked.error);
|
|
50166
51829
|
const rpc2 = createSolanaRpc(checked, deps.fetch);
|
|
50167
51830
|
try {
|
|
50168
51831
|
const lamports = await rpc2.getBalance(address);
|
|
@@ -50243,10 +51906,10 @@ async function teeDisable(args, ctx) {
|
|
|
50243
51906
|
return 1;
|
|
50244
51907
|
const parsed = parseArgs(args, { valueFlags: ["--keystore"], pathFlags: ["--keystore"] });
|
|
50245
51908
|
if ("error" in parsed)
|
|
50246
|
-
return
|
|
51909
|
+
return usage4(ctx, parsed.error);
|
|
50247
51910
|
const [address, extra] = parsed.positionals;
|
|
50248
51911
|
if (!address || extra !== undefined)
|
|
50249
|
-
return
|
|
51912
|
+
return usage4(ctx, "Usage: candle tee disable <address>");
|
|
50250
51913
|
await printIdentity(ctx);
|
|
50251
51914
|
const openedActive = await openActiveTee(ctx, parsed, address, "read");
|
|
50252
51915
|
if (!openedActive.ok)
|
|
@@ -50341,6 +52004,9 @@ function refusalState(raw) {
|
|
|
50341
52004
|
async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions, pending, recordPending, clearPending) {
|
|
50342
52005
|
const blockhash = await rpc2.getLatestBlockhash();
|
|
50343
52006
|
const message = compileLegacyMessage({ feePayer, recentBlockhash: blockhash, instructions });
|
|
52007
|
+
return broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending);
|
|
52008
|
+
}
|
|
52009
|
+
async function broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending) {
|
|
50344
52010
|
const signatureBytes = signMessage(message, secret);
|
|
50345
52011
|
const signature = base58.encode(signatureBytes);
|
|
50346
52012
|
const wire = serializeSignedTransaction(message, signatureBytes);
|
|
@@ -50366,7 +52032,7 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
|
|
|
50366
52032
|
error: `send did not answer cleanly (${error instanceof Error ? error.message : error}); it may still land`
|
|
50367
52033
|
};
|
|
50368
52034
|
}
|
|
50369
|
-
for (let i = 0;i <
|
|
52035
|
+
for (let i = 0;i < CONFIRM_MAX_POLLS3; i++) {
|
|
50370
52036
|
let status;
|
|
50371
52037
|
try {
|
|
50372
52038
|
status = await rpc2.getSignatureStatus(signature);
|
|
@@ -50388,12 +52054,12 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
|
|
|
50388
52054
|
error: `transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}`
|
|
50389
52055
|
};
|
|
50390
52056
|
}
|
|
50391
|
-
await deps.sleep(
|
|
52057
|
+
await deps.sleep(CONFIRM_POLL_MS3);
|
|
50392
52058
|
}
|
|
50393
52059
|
return {
|
|
50394
52060
|
status: "uncertain",
|
|
50395
52061
|
signature,
|
|
50396
|
-
error: `transaction ${signature} was not finalized within ${
|
|
52062
|
+
error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS3 * CONFIRM_POLL_MS3 / 1000}s; it may still land${echoNote}`
|
|
50397
52063
|
};
|
|
50398
52064
|
}
|
|
50399
52065
|
async function teeSweep(args, ctx) {
|
|
@@ -50406,13 +52072,13 @@ async function teeSweep(args, ctx) {
|
|
|
50406
52072
|
pathFlags: ["--keystore"]
|
|
50407
52073
|
});
|
|
50408
52074
|
if ("error" in parsed)
|
|
50409
|
-
return
|
|
52075
|
+
return usage4(ctx, parsed.error);
|
|
50410
52076
|
const [address, extra] = parsed.positionals;
|
|
50411
52077
|
if (!address || extra !== undefined)
|
|
50412
|
-
return
|
|
52078
|
+
return usage4(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
|
|
50413
52079
|
const rpcUrl2 = rpcUrlFrom2(ctx, parsed);
|
|
50414
52080
|
if (typeof rpcUrl2 !== "string")
|
|
50415
|
-
return
|
|
52081
|
+
return usage4(ctx, rpcUrl2.error);
|
|
50416
52082
|
const emergency = parsed.booleans.has("--emergency");
|
|
50417
52083
|
const openedActive = await openActiveTee(ctx, parsed, address, "sign");
|
|
50418
52084
|
if (!openedActive.ok)
|
|
@@ -50615,6 +52281,116 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50615
52281
|
pendingStill.push(p);
|
|
50616
52282
|
signingBlocked = true;
|
|
50617
52283
|
}
|
|
52284
|
+
const candidateAccounts = new Set;
|
|
52285
|
+
const lpCloses = [];
|
|
52286
|
+
{
|
|
52287
|
+
let token2022Accounts = [];
|
|
52288
|
+
try {
|
|
52289
|
+
token2022Accounts = await rpc2.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID);
|
|
52290
|
+
} catch (error) {
|
|
52291
|
+
residuals.push({
|
|
52292
|
+
kind: "inventory",
|
|
52293
|
+
detail: `could not list Token-2022 accounts for position discovery: ${error instanceof Error ? error.message : error}`
|
|
52294
|
+
});
|
|
52295
|
+
}
|
|
52296
|
+
const candidates = token2022Accounts.filter(isPositionCandidate);
|
|
52297
|
+
for (const acct of candidates)
|
|
52298
|
+
candidateAccounts.add(acct.pubkey);
|
|
52299
|
+
const leftover = (kind, acct, detail) => residuals.push({ kind, detail, mint: acct.mint, account: acct.pubkey, amountRaw: acct.amountRaw });
|
|
52300
|
+
if (!emergency && candidates.length > 0) {
|
|
52301
|
+
if (!json)
|
|
52302
|
+
deps.stdout.write(` ${candidates.length} position NFT candidate(s) (Token-2022, amount 1, decimals 0)
|
|
52303
|
+
`);
|
|
52304
|
+
const verified = [];
|
|
52305
|
+
for (const acct of candidates) {
|
|
52306
|
+
if (signingBlocked)
|
|
52307
|
+
break;
|
|
52308
|
+
if (!apiKey || !entry.linkedWalletId) {
|
|
52309
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "no bound API key is available to build the close; the NFT was left in place");
|
|
52310
|
+
continue;
|
|
52311
|
+
}
|
|
52312
|
+
const result = await apiRequest(`/api/v1/agent/lp/positions/${encodeURIComponent(acct.mint)}/close-build`, {
|
|
52313
|
+
method: "POST",
|
|
52314
|
+
auth: "key",
|
|
52315
|
+
credentials: { apiKey },
|
|
52316
|
+
apiUrl,
|
|
52317
|
+
fetch: deps.fetch,
|
|
52318
|
+
env: deps.env,
|
|
52319
|
+
body: {}
|
|
52320
|
+
});
|
|
52321
|
+
if (!result.ok) {
|
|
52322
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `close-build ${result.code ?? `HTTP ${result.status}`}: ${result.message}; the NFT was left in place`);
|
|
52323
|
+
continue;
|
|
52324
|
+
}
|
|
52325
|
+
const artifact = parseCloseArtifact(result.body);
|
|
52326
|
+
if (artifact === undefined) {
|
|
52327
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "close-build answered without a transaction and an ordered account key array; the NFT was left in place");
|
|
52328
|
+
continue;
|
|
52329
|
+
}
|
|
52330
|
+
const verdict = await verifyCloseArtifact({
|
|
52331
|
+
artifact,
|
|
52332
|
+
rpc: rpc2,
|
|
52333
|
+
tee: address,
|
|
52334
|
+
vault,
|
|
52335
|
+
nftMint: acct.mint,
|
|
52336
|
+
nftAccount: acct.pubkey
|
|
52337
|
+
});
|
|
52338
|
+
if (!verdict.ok) {
|
|
52339
|
+
leftover(DAMM_CLOSE_TRANSACTION_REFUSED, acct, `${verdict.reason}; refused before signing`);
|
|
52340
|
+
continue;
|
|
52341
|
+
}
|
|
52342
|
+
verified.push({ acct, verdict });
|
|
52343
|
+
}
|
|
52344
|
+
for (const { acct, verdict } of verified) {
|
|
52345
|
+
if (signingBlocked) {
|
|
52346
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "not signed: an earlier transaction is still in flight");
|
|
52347
|
+
continue;
|
|
52348
|
+
}
|
|
52349
|
+
let blockhashValid;
|
|
52350
|
+
try {
|
|
52351
|
+
blockhashValid = await rpc2.isBlockhashValid(verdict.tx.message.recentBlockhash, "confirmed");
|
|
52352
|
+
} catch (error) {
|
|
52353
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `could not check the close build's blockhash: ${error instanceof Error ? error.message : error}; not signed`);
|
|
52354
|
+
continue;
|
|
52355
|
+
}
|
|
52356
|
+
if (!blockhashValid) {
|
|
52357
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "the close build's blockhash has expired; not signed, re-run the sweep for a fresh close");
|
|
52358
|
+
continue;
|
|
52359
|
+
}
|
|
52360
|
+
const display = describeClose({ verdict, tee: address, vault, nftMint: acct.mint, nftAccount: acct.pubkey });
|
|
52361
|
+
lpCloses.push({ mint: acct.mint, account: acct.pubkey, display });
|
|
52362
|
+
if (!json) {
|
|
52363
|
+
deps.stdout.write(` Close for position ${acct.mint} (verified, unsigned; about to sign):
|
|
52364
|
+
`);
|
|
52365
|
+
for (const line of display)
|
|
52366
|
+
deps.stdout.write(` ${line}
|
|
52367
|
+
`);
|
|
52368
|
+
}
|
|
52369
|
+
const pending = {
|
|
52370
|
+
kind: "lp-close",
|
|
52371
|
+
mint: acct.mint,
|
|
52372
|
+
account: acct.pubkey,
|
|
52373
|
+
amountRaw: acct.amountRaw
|
|
52374
|
+
};
|
|
52375
|
+
const outcome = await broadcastMessage(rpc2, deps, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending);
|
|
52376
|
+
if (outcome.status !== "finalized") {
|
|
52377
|
+
if (!settle2(outcome, pending, "lp-close"))
|
|
52378
|
+
signingBlocked = true;
|
|
52379
|
+
continue;
|
|
52380
|
+
}
|
|
52381
|
+
await retainReceipt({
|
|
52382
|
+
kind: "lp-close",
|
|
52383
|
+
mint: acct.mint,
|
|
52384
|
+
amountRaw: acct.amountRaw,
|
|
52385
|
+
signature: outcome.signature,
|
|
52386
|
+
finalizedAt: new Date(deps.now()).toISOString()
|
|
52387
|
+
});
|
|
52388
|
+
if (!json)
|
|
52389
|
+
deps.stdout.write(` closed LP position ${acct.mint} and withdrew its liquidity: ${outcome.signature}
|
|
52390
|
+
`);
|
|
52391
|
+
}
|
|
52392
|
+
}
|
|
52393
|
+
}
|
|
50618
52394
|
const tokenAccounts = [];
|
|
50619
52395
|
for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
|
|
50620
52396
|
try {
|
|
@@ -50631,6 +52407,13 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50631
52407
|
if (signingBlocked)
|
|
50632
52408
|
break;
|
|
50633
52409
|
const token2022 = acct.programId === TOKEN_2022_PROGRAM_ID;
|
|
52410
|
+
if (candidateAccounts.has(acct.pubkey)) {
|
|
52411
|
+
if (!emergency)
|
|
52412
|
+
continue;
|
|
52413
|
+
if (!json)
|
|
52414
|
+
deps.stdout.write(` ${acct.mint}: position NFT moved as-is to the vault (emergency; unwind it from a fresh TEE wallet)
|
|
52415
|
+
`);
|
|
52416
|
+
}
|
|
50634
52417
|
if (acct.state !== "initialized") {
|
|
50635
52418
|
residuals.push({
|
|
50636
52419
|
kind: token2022 && acct.state === "frozen" ? "TOKEN_2022_FROZEN" : "frozen-or-uninitialized",
|
|
@@ -50885,7 +52668,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50885
52668
|
signature: p.signature
|
|
50886
52669
|
})),
|
|
50887
52670
|
inventory,
|
|
50888
|
-
recordedOnServer
|
|
52671
|
+
recordedOnServer,
|
|
52672
|
+
...lpCloses.length > 0 ? { lpCloses } : {}
|
|
50889
52673
|
})}
|
|
50890
52674
|
`);
|
|
50891
52675
|
return finalState === "swept" ? 0 : 3;
|
|
@@ -50968,8 +52752,7 @@ function launchWarning(keyPrefix, toKey, rows) {
|
|
|
50968
52752
|
return `Warning: key ${keyPrefix} lacks launch:write; ${launchers.join(", ")} ${launchers.length === 1 ? "has" : "have"} allowLaunch but cannot launch under it.`;
|
|
50969
52753
|
}
|
|
50970
52754
|
var RELAY_SIGNER_LINE = "The relay signer does not move: trade these wallets from the machine that promoted them.";
|
|
50971
|
-
function
|
|
50972
|
-
const { deps, apiUrl, json } = ctx;
|
|
52755
|
+
function rebindFailureDetails(result, context) {
|
|
50973
52756
|
let code = result.code;
|
|
50974
52757
|
let message = result.message;
|
|
50975
52758
|
let suggestion;
|
|
@@ -50989,31 +52772,61 @@ function writeRebindFailure(ctx, result, context) {
|
|
|
50989
52772
|
} else if (result.code === "REBIND_STALE") {
|
|
50990
52773
|
suggestion = "Run the command again; the preview will show the current binding.";
|
|
50991
52774
|
}
|
|
52775
|
+
return {
|
|
52776
|
+
code: code ?? `HTTP ${result.status}`,
|
|
52777
|
+
message,
|
|
52778
|
+
...suggestion !== undefined ? { suggestion } : {},
|
|
52779
|
+
status: result.status
|
|
52780
|
+
};
|
|
52781
|
+
}
|
|
52782
|
+
function writeRebindFailure(ctx, result, context) {
|
|
52783
|
+
const { deps, apiUrl, json } = ctx;
|
|
52784
|
+
const { code, message, suggestion } = rebindFailureDetails(result, context);
|
|
50992
52785
|
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
|
|
50993
52786
|
if (json) {
|
|
50994
52787
|
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
50995
52788
|
`);
|
|
50996
52789
|
} else {
|
|
50997
|
-
deps.stderr.write(`${code
|
|
52790
|
+
deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
50998
52791
|
`);
|
|
50999
52792
|
}
|
|
51000
52793
|
return 1;
|
|
51001
52794
|
}
|
|
51002
|
-
async function
|
|
51003
|
-
const { deps, apiUrl
|
|
51004
|
-
|
|
51005
|
-
return { ok: true, keyPrefix: raw };
|
|
51006
|
-
const result = await apiRequest(KEYS_PATH2, {
|
|
52795
|
+
async function listAccountKeys(ctx, deviceToken) {
|
|
52796
|
+
const { deps, apiUrl } = ctx;
|
|
52797
|
+
return apiRequest(KEYS_PATH2, {
|
|
51007
52798
|
auth: "device",
|
|
51008
52799
|
credentials: { deviceToken },
|
|
51009
52800
|
apiUrl,
|
|
51010
52801
|
fetch: deps.fetch,
|
|
51011
52802
|
env: deps.env
|
|
51012
52803
|
});
|
|
51013
|
-
|
|
51014
|
-
|
|
52804
|
+
}
|
|
52805
|
+
async function postRebind(ctx, deviceToken, body) {
|
|
52806
|
+
const { deps, apiUrl } = ctx;
|
|
52807
|
+
return apiRequest(REBIND_PATH, {
|
|
52808
|
+
method: "POST",
|
|
52809
|
+
body,
|
|
52810
|
+
auth: "device",
|
|
52811
|
+
credentials: { deviceToken },
|
|
52812
|
+
apiUrl,
|
|
52813
|
+
fetch: deps.fetch,
|
|
52814
|
+
env: deps.env
|
|
52815
|
+
});
|
|
52816
|
+
}
|
|
52817
|
+
async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
|
|
52818
|
+
const { deps, json } = ctx;
|
|
52819
|
+
if (KEY_PREFIX_RE.test(raw))
|
|
52820
|
+
return { ok: true, keyPrefix: raw };
|
|
52821
|
+
let listed = opts.keys;
|
|
52822
|
+
if (listed === undefined) {
|
|
52823
|
+
const result = await listAccountKeys(ctx, deviceToken);
|
|
52824
|
+
if (!result.ok) {
|
|
52825
|
+
return { ok: false, code: writeRebindFailure(ctx, result, {}) };
|
|
52826
|
+
}
|
|
52827
|
+
listed = result.body?.keys ?? [];
|
|
51015
52828
|
}
|
|
51016
|
-
const keys =
|
|
52829
|
+
const keys = listed.filter((key) => !key.revokedAt);
|
|
51017
52830
|
const matches = keys.filter((key) => key.label === raw);
|
|
51018
52831
|
if (matches.length === 1)
|
|
51019
52832
|
return { ok: true, keyPrefix: matches[0].keyPrefix };
|
|
@@ -51071,15 +52884,7 @@ async function teeRebind(args, ctx) {
|
|
|
51071
52884
|
const target = await resolveTargetKey(ctx, deviceToken, toKey);
|
|
51072
52885
|
if (!target.ok)
|
|
51073
52886
|
return target.code;
|
|
51074
|
-
const call = (body) =>
|
|
51075
|
-
method: "POST",
|
|
51076
|
-
body,
|
|
51077
|
-
auth: "device",
|
|
51078
|
-
credentials: { deviceToken },
|
|
51079
|
-
apiUrl,
|
|
51080
|
-
fetch: deps.fetch,
|
|
51081
|
-
env: deps.env
|
|
51082
|
-
});
|
|
52887
|
+
const call = (body) => postRebind(ctx, deviceToken, body);
|
|
51083
52888
|
const preview = await call({
|
|
51084
52889
|
dryRun: true,
|
|
51085
52890
|
toKeyPrefix: target.keyPrefix,
|
|
@@ -51221,6 +53026,150 @@ async function teeRebinds(args, ctx) {
|
|
|
51221
53026
|
return 0;
|
|
51222
53027
|
}
|
|
51223
53028
|
|
|
53029
|
+
// src/commands/transfer.ts
|
|
53030
|
+
init_args();
|
|
53031
|
+
init_render();
|
|
53032
|
+
init_trading();
|
|
53033
|
+
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]";
|
|
53034
|
+
var TRANSFER_ASSETS = Object.keys(BASES);
|
|
53035
|
+
var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
53036
|
+
async function readLinkedWallets(ctx, key) {
|
|
53037
|
+
const rows = [];
|
|
53038
|
+
let cursor = null;
|
|
53039
|
+
const seen = new Set;
|
|
53040
|
+
for (let page = 0;page < 25; page++) {
|
|
53041
|
+
const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
|
|
53042
|
+
const body = await request(ctx, key, `/api/v1/agent/wallets${query}`);
|
|
53043
|
+
if (!Array.isArray(body.page))
|
|
53044
|
+
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
53045
|
+
rows.push(...body.page);
|
|
53046
|
+
if (body.isDone !== false || typeof body.continueCursor !== "string" || seen.has(body.continueCursor))
|
|
53047
|
+
return rows;
|
|
53048
|
+
seen.add(body.continueCursor);
|
|
53049
|
+
cursor = body.continueCursor;
|
|
53050
|
+
}
|
|
53051
|
+
return rows;
|
|
53052
|
+
}
|
|
53053
|
+
async function resolveDestination(ctx, key, source, to) {
|
|
53054
|
+
if (to === "vault") {
|
|
53055
|
+
const lifecycle = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/lifecycle`);
|
|
53056
|
+
const vault = lifecycle.vaultDestination;
|
|
53057
|
+
if (typeof vault !== "string" || vault.length === 0)
|
|
53058
|
+
throw new TradingError("VAULT_NOT_PINNED", "This wallet has no pinned vault recorded on Candle, so --to vault names nothing. Pass the address instead.");
|
|
53059
|
+
return { kind: "vault", address: vault };
|
|
53060
|
+
}
|
|
53061
|
+
const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === "solana" && row.revokedAt === undefined && row.address !== source.address);
|
|
53062
|
+
const matches = rows.filter((row) => row.label === to || row._id === to || row.address === to);
|
|
53063
|
+
if (matches.length > 1)
|
|
53064
|
+
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.`);
|
|
53065
|
+
const match = matches[0];
|
|
53066
|
+
if (match)
|
|
53067
|
+
return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
|
|
53068
|
+
if (to === source.address)
|
|
53069
|
+
throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
|
|
53070
|
+
if (BASE58_ADDRESS.test(to))
|
|
53071
|
+
return { kind: "address", address: to };
|
|
53072
|
+
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("; ")}.`);
|
|
53073
|
+
}
|
|
53074
|
+
function describeDestination(destination) {
|
|
53075
|
+
switch (destination.kind) {
|
|
53076
|
+
case "vault":
|
|
53077
|
+
return `this wallet's pinned vault ${destination.address}`;
|
|
53078
|
+
case "linked":
|
|
53079
|
+
return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
|
|
53080
|
+
case "address":
|
|
53081
|
+
return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
|
|
53082
|
+
}
|
|
53083
|
+
}
|
|
53084
|
+
async function confirmTransfer(ctx, lines, yes) {
|
|
53085
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
53086
|
+
for (const line of lines)
|
|
53087
|
+
output.write(`${safeText(line)}
|
|
53088
|
+
`);
|
|
53089
|
+
if (yes)
|
|
53090
|
+
return true;
|
|
53091
|
+
if (!ctx.deps.isTTY.stdin)
|
|
53092
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary transfer prompt.");
|
|
53093
|
+
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
53094
|
+
}
|
|
53095
|
+
async function transfer(args, ctx) {
|
|
53096
|
+
const parsed = parseArgs(args, {
|
|
53097
|
+
valueFlags: ["--wallet", "--to", "--asset", "--mint", "--amount", "--rpc-url"],
|
|
53098
|
+
booleanFlags: ["--yes"]
|
|
53099
|
+
});
|
|
53100
|
+
if ("error" in parsed) {
|
|
53101
|
+
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
53102
|
+
return 2;
|
|
53103
|
+
}
|
|
53104
|
+
const flags = parsed.values;
|
|
53105
|
+
const asset = flags["--asset"]?.toUpperCase();
|
|
53106
|
+
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"])) {
|
|
53107
|
+
writeUsageFailure(ctx.deps, USAGE2, ctx.json);
|
|
53108
|
+
return 2;
|
|
53109
|
+
}
|
|
53110
|
+
const to = flags["--to"];
|
|
53111
|
+
const isMax = flags["--amount"] === "max";
|
|
53112
|
+
try {
|
|
53113
|
+
if (!isMax)
|
|
53114
|
+
rawAmount(flags["--amount"], 18);
|
|
53115
|
+
const key = await tradingKey(ctx);
|
|
53116
|
+
const payer = await tradingPayer(ctx, key, flags["--wallet"], "transfer:write");
|
|
53117
|
+
if (payer.kind === "embedded")
|
|
53118
|
+
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.");
|
|
53119
|
+
if (!payer.scopes.includes("transfer:bound"))
|
|
53120
|
+
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");
|
|
53121
|
+
const wallet = payer.wallet;
|
|
53122
|
+
const destination = await resolveDestination(ctx, key, wallet, to);
|
|
53123
|
+
const label = asset ?? flags["--mint"];
|
|
53124
|
+
let amountRaw;
|
|
53125
|
+
if (isMax)
|
|
53126
|
+
amountRaw = "max";
|
|
53127
|
+
else {
|
|
53128
|
+
const decimals = asset ? BASES[asset]?.decimals ?? 9 : await decimalsFor(ctx, flags["--mint"], flags["--rpc-url"]);
|
|
53129
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
53130
|
+
}
|
|
53131
|
+
const amountText = isMax ? `the full spendable balance of ${label}` : `${flags["--amount"]} ${label}`;
|
|
53132
|
+
const confirmed = await confirmTransfer(ctx, [
|
|
53133
|
+
`Transfer ${amountText} from ${wallet.address} (${wallet.id}) to ${describeDestination(destination)}`,
|
|
53134
|
+
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."
|
|
53135
|
+
], parsed.booleans.has("--yes"));
|
|
53136
|
+
if (!confirmed)
|
|
53137
|
+
return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet.id, destination });
|
|
53138
|
+
const built = await request(ctx, key, "/api/v1/agent/transfer/build", {
|
|
53139
|
+
walletId: wallet.id,
|
|
53140
|
+
chain: "solana",
|
|
53141
|
+
...asset ? { asset } : { mint: flags["--mint"] },
|
|
53142
|
+
amountRaw,
|
|
53143
|
+
to: destination.address
|
|
53144
|
+
});
|
|
53145
|
+
const transferId = built.transferId;
|
|
53146
|
+
const unsigned = built.unsignedTransactionsBase64;
|
|
53147
|
+
if (typeof transferId !== "string" || !Array.isArray(unsigned) || unsigned.length !== 1 || typeof unsigned[0] !== "string")
|
|
53148
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return one unsigned transfer transaction; nothing was signed.");
|
|
53149
|
+
if (typeof built.payerAddress === "string" && built.payerAddress !== wallet.address)
|
|
53150
|
+
throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the requested payer; nothing was signed.");
|
|
53151
|
+
const builtAmount = typeof built.amountRaw === "string" ? built.amountRaw : amountRaw;
|
|
53152
|
+
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"}` : ""}.
|
|
53153
|
+
`);
|
|
53154
|
+
const signed = await relaySign(ctx, key, wallet, unsigned[0]);
|
|
53155
|
+
const result = await request(ctx, key, "/api/v1/agent/transfer/submit", {
|
|
53156
|
+
transferId,
|
|
53157
|
+
signedTransactionsBase64: [signed]
|
|
53158
|
+
});
|
|
53159
|
+
const receipt = {
|
|
53160
|
+
...result,
|
|
53161
|
+
transferId,
|
|
53162
|
+
walletId: wallet.id,
|
|
53163
|
+
wallet: safeText(wallet.address),
|
|
53164
|
+
destination,
|
|
53165
|
+
...typeof built.destinationKind === "string" ? { destinationKind: built.destinationKind } : {}
|
|
53166
|
+
};
|
|
53167
|
+
return printTradingResult(ctx, receipt);
|
|
53168
|
+
} catch (error) {
|
|
53169
|
+
return tradingFailure(ctx, error);
|
|
53170
|
+
}
|
|
53171
|
+
}
|
|
53172
|
+
|
|
51224
53173
|
// src/commands/update.ts
|
|
51225
53174
|
init_args();
|
|
51226
53175
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -52793,10 +54742,15 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
52793
54742
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
52794
54743
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
52795
54744
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
52796
|
-
|
|
52797
|
-
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)."
|
|
54745
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
52798
54746
|
});
|
|
52799
54747
|
const vault = hold(opened.vault);
|
|
54748
|
+
const openedWith = {
|
|
54749
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
54750
|
+
envelopeId: opened.factor.envelopeId
|
|
54751
|
+
};
|
|
54752
|
+
if (opened.factor.kind === "security-key")
|
|
54753
|
+
opened.factor.session.pin = undefined;
|
|
52800
54754
|
const vaultId = vault.file.vaultId;
|
|
52801
54755
|
let registered;
|
|
52802
54756
|
try {
|
|
@@ -52894,7 +54848,8 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
52894
54848
|
saltDerivation: "platform",
|
|
52895
54849
|
helper: envelope.helper,
|
|
52896
54850
|
recoverableFactors: recoverable,
|
|
52897
|
-
verified: true
|
|
54851
|
+
verified: true,
|
|
54852
|
+
openedWith
|
|
52898
54853
|
});
|
|
52899
54854
|
return 0;
|
|
52900
54855
|
}
|
|
@@ -52917,6 +54872,7 @@ ${PASSKEY_NOTE}
|
|
|
52917
54872
|
}
|
|
52918
54873
|
|
|
52919
54874
|
// src/commands/vault-factor-security-key.ts
|
|
54875
|
+
init_esm();
|
|
52920
54876
|
init_crypto();
|
|
52921
54877
|
init_errors();
|
|
52922
54878
|
init_fido2();
|
|
@@ -53040,20 +54996,84 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
53040
54996
|
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
53041
54997
|
const file = parseVaultFile(raw);
|
|
53042
54998
|
const envelopeId = freshEnvelopeId();
|
|
53043
|
-
const
|
|
53044
|
-
|
|
53045
|
-
|
|
53046
|
-
|
|
53047
|
-
|
|
53048
|
-
|
|
54999
|
+
const vaultId = file.vaultId;
|
|
55000
|
+
const keysInHeader = file.envelopes.filter(isCtap2Envelope);
|
|
55001
|
+
const { helperPath, info } = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55002
|
+
assertHelperExcludeList(info, helperPath, keysInHeader.length);
|
|
55003
|
+
const attached = keysInHeader.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: keysInHeader }) : undefined;
|
|
55004
|
+
const excludeCountNow = keysInHeader.length;
|
|
55005
|
+
const addingLine = (device) => deps.stderr.write(`Adding: ${device.product || "security key"} (--device ${device.deviceId}), a different key from the one that opened the vault.
|
|
55006
|
+
`);
|
|
55007
|
+
let session;
|
|
55008
|
+
let enrolling;
|
|
55009
|
+
let deferredAfter;
|
|
53049
55010
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
53050
55011
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
53051
55012
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
53052
|
-
|
|
53053
|
-
|
|
55013
|
+
among: factorAddAmong(file.envelopes),
|
|
55014
|
+
attached,
|
|
55015
|
+
onFactorChosen: async (chosen) => {
|
|
55016
|
+
if (chosen.kind === "passphrase") {
|
|
55017
|
+
session = await openSecurityKeySession(deps, {
|
|
55018
|
+
vaultId,
|
|
55019
|
+
envelopeId,
|
|
55020
|
+
deviceFlag: ctx.vaultDevice,
|
|
55021
|
+
requireFeatures: true,
|
|
55022
|
+
beforePin: (device) => {
|
|
55023
|
+
refuseIfEnrolled(device, attached);
|
|
55024
|
+
assertExcludeListFits(device, excludeCountNow);
|
|
55025
|
+
}
|
|
55026
|
+
});
|
|
55027
|
+
return;
|
|
55028
|
+
}
|
|
55029
|
+
if (chosen.kind !== "security-key")
|
|
55030
|
+
return;
|
|
55031
|
+
const plan = planDevices(ctx, chosen.envelope, info.devices, attached);
|
|
55032
|
+
if (plan.enrolling !== undefined) {
|
|
55033
|
+
refuseIfEnrolled(plan.enrolling, attached);
|
|
55034
|
+
assertDeviceServesFactor(plan.enrolling);
|
|
55035
|
+
assertExcludeListFits(plan.enrolling, excludeCountNow);
|
|
55036
|
+
enrolling = plan.enrolling;
|
|
55037
|
+
addingLine(plan.enrolling);
|
|
55038
|
+
} else {
|
|
55039
|
+
deferredAfter = plan.opening;
|
|
55040
|
+
}
|
|
55041
|
+
return { preferDevice: plan.opening.deviceId };
|
|
55042
|
+
}
|
|
53054
55043
|
});
|
|
53055
55044
|
const vault = hold(opened.vault);
|
|
53056
|
-
const
|
|
55045
|
+
const openedWith = {
|
|
55046
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55047
|
+
envelopeId: opened.factor.envelopeId
|
|
55048
|
+
};
|
|
55049
|
+
if (opened.factor.kind === "security-key")
|
|
55050
|
+
opened.factor.session.pin = undefined;
|
|
55051
|
+
const enrolledKeys = vault.file.envelopes.filter(isCtap2Envelope);
|
|
55052
|
+
const excludeCredentialIds = enrolledKeys.map((envelope2) => base64.encode(unb64u(envelope2.credentialId, "credentialId")));
|
|
55053
|
+
if (session === undefined) {
|
|
55054
|
+
if (deferredAfter !== undefined) {
|
|
55055
|
+
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.`);
|
|
55056
|
+
const again = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55057
|
+
const probeAgain = enrolledKeys.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: enrolledKeys }) : undefined;
|
|
55058
|
+
const inserted = chooseAfterInsert(ctx, again.info.devices, probeAgain);
|
|
55059
|
+
refuseIfEnrolled(inserted, probeAgain);
|
|
55060
|
+
assertDeviceServesFactor(inserted);
|
|
55061
|
+
assertExcludeListFits(inserted, excludeCredentialIds.length);
|
|
55062
|
+
addingLine(inserted);
|
|
55063
|
+
enrolling = inserted;
|
|
55064
|
+
}
|
|
55065
|
+
if (enrolling === undefined)
|
|
55066
|
+
throw new Error("factor add security-key: no enrolling device was decided");
|
|
55067
|
+
const target = enrolling;
|
|
55068
|
+
session = await openSecurityKeySession(deps, {
|
|
55069
|
+
vaultId,
|
|
55070
|
+
envelopeId,
|
|
55071
|
+
deviceFlag: target.deviceId,
|
|
55072
|
+
requireFeatures: true,
|
|
55073
|
+
pinPrompt: (device) => `PIN for the key being added, ${device.product || "the security key"} (input hidden): `
|
|
55074
|
+
});
|
|
55075
|
+
}
|
|
55076
|
+
const registered = await registerCredential(deps, session, { vaultId, envelopeId, excludeCredentialIds });
|
|
53057
55077
|
if (registered.backupEligible) {
|
|
53058
55078
|
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.`, {
|
|
53059
55079
|
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."
|
|
@@ -53110,7 +55130,8 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
53110
55130
|
backupState: envelope.backupState,
|
|
53111
55131
|
userVerification: "required",
|
|
53112
55132
|
recoverableFactors: recoverable,
|
|
53113
|
-
verified: true
|
|
55133
|
+
verified: true,
|
|
55134
|
+
openedWith
|
|
53114
55135
|
});
|
|
53115
55136
|
return 0;
|
|
53116
55137
|
}
|
|
@@ -53129,6 +55150,76 @@ ${SECURITY_KEY_PAIR_NOTE}
|
|
|
53129
55150
|
`);
|
|
53130
55151
|
return 0;
|
|
53131
55152
|
}
|
|
55153
|
+
function refuseIfEnrolled(device, attached) {
|
|
55154
|
+
const entry = attached?.devices?.find((candidate) => candidate.deviceId === device.deviceId);
|
|
55155
|
+
if (entry === undefined || attached?.credentials === undefined)
|
|
55156
|
+
return;
|
|
55157
|
+
for (const [envelopeId, credentialId] of attached.credentials) {
|
|
55158
|
+
if (entry.present.includes(credentialId))
|
|
55159
|
+
throw alreadyEnrolled(device, envelopeId);
|
|
55160
|
+
}
|
|
55161
|
+
}
|
|
55162
|
+
function planDevices(ctx, opener, devices, attached) {
|
|
55163
|
+
const platform = ctx.deps.platform;
|
|
55164
|
+
if (devices.length === 0) {
|
|
55165
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key is attached.", {
|
|
55166
|
+
suggestion: "Plug the key in and run the command again. No other factor is substituted."
|
|
55167
|
+
});
|
|
55168
|
+
}
|
|
55169
|
+
const named = ctx.vaultDevice;
|
|
55170
|
+
const openerCredential = attached?.credentials?.get(opener.id);
|
|
55171
|
+
const holders = openerCredential === undefined ? [] : (attached?.devices ?? []).filter((device) => device.present.includes(openerCredential));
|
|
55172
|
+
let opening;
|
|
55173
|
+
if (holders.length === 1) {
|
|
55174
|
+
opening = devices.find((device) => device.deviceId === holders[0].deviceId);
|
|
55175
|
+
}
|
|
55176
|
+
if (opening === undefined) {
|
|
55177
|
+
const candidates = devices.filter((device) => device.deviceId !== named);
|
|
55178
|
+
if (candidates.length === 1)
|
|
55179
|
+
opening = candidates[0];
|
|
55180
|
+
else if (devices.length === 1)
|
|
55181
|
+
opening = devices[0];
|
|
55182
|
+
}
|
|
55183
|
+
if (opening === undefined) {
|
|
55184
|
+
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." });
|
|
55185
|
+
}
|
|
55186
|
+
if (!opening.readable)
|
|
55187
|
+
selectDevice([opening], undefined, platform);
|
|
55188
|
+
const others = devices.filter((device) => device.deviceId !== opening.deviceId);
|
|
55189
|
+
if (named !== undefined) {
|
|
55190
|
+
if (named === opening.deviceId)
|
|
55191
|
+
throw alreadyEnrolled(opening, opener.id);
|
|
55192
|
+
return { opening, enrolling: selectDevice(others, named, platform) };
|
|
55193
|
+
}
|
|
55194
|
+
if (others.length === 1)
|
|
55195
|
+
return { opening, enrolling: selectDevice(others, undefined, platform) };
|
|
55196
|
+
if (others.length > 1) {
|
|
55197
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
55198
|
+
`);
|
|
55199
|
+
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:
|
|
55200
|
+
${listing}`, { suggestion: "Run again with --device <id> naming the key to add." });
|
|
55201
|
+
}
|
|
55202
|
+
return { opening, enrolling: undefined };
|
|
55203
|
+
}
|
|
55204
|
+
function chooseAfterInsert(ctx, devices, probe) {
|
|
55205
|
+
const platform = ctx.deps.platform;
|
|
55206
|
+
if (devices.length === 0) {
|
|
55207
|
+
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." });
|
|
55208
|
+
}
|
|
55209
|
+
if (devices.length === 1)
|
|
55210
|
+
return selectDevice(devices, undefined, platform);
|
|
55211
|
+
const holdsVault = (device) => (probe?.devices?.find((entry) => entry.deviceId === device.deviceId)?.present.length ?? 0) > 0;
|
|
55212
|
+
const others = devices.filter((device) => !holdsVault(device));
|
|
55213
|
+
if (others.length === 1)
|
|
55214
|
+
return selectDevice(others, undefined, platform);
|
|
55215
|
+
if (others.length === 0) {
|
|
55216
|
+
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." });
|
|
55217
|
+
}
|
|
55218
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
55219
|
+
`);
|
|
55220
|
+
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:
|
|
55221
|
+
${listing}`, { suggestion: "Run the command again with only the key to add inserted when asked. Nothing was written." });
|
|
55222
|
+
}
|
|
53132
55223
|
|
|
53133
55224
|
// src/commands/vault-factor-touch-id.ts
|
|
53134
55225
|
init_enclave();
|
|
@@ -53167,10 +55258,15 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
53167
55258
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
53168
55259
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
53169
55260
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
53170
|
-
|
|
53171
|
-
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)."
|
|
55261
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
53172
55262
|
});
|
|
53173
55263
|
const vault = hold(opened.vault);
|
|
55264
|
+
const openedWith = {
|
|
55265
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55266
|
+
envelopeId: opened.factor.envelopeId
|
|
55267
|
+
};
|
|
55268
|
+
if (opened.factor.kind === "security-key")
|
|
55269
|
+
opened.factor.session.pin = undefined;
|
|
53174
55270
|
const vaultId = vault.file.vaultId;
|
|
53175
55271
|
const keyTag = keyTagFor(vaultId, envelopeId);
|
|
53176
55272
|
let created;
|
|
@@ -53254,7 +55350,8 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
53254
55350
|
helper: { teamId: session.identity.teamId, bundleId: session.identity.bundleId, minVersion: session.version },
|
|
53255
55351
|
accessControl: "biometryCurrentSet",
|
|
53256
55352
|
recoverableFactors: recoverable,
|
|
53257
|
-
verified: true
|
|
55353
|
+
verified: true,
|
|
55354
|
+
openedWith
|
|
53258
55355
|
});
|
|
53259
55356
|
return 0;
|
|
53260
55357
|
}
|
|
@@ -53960,9 +56057,18 @@ function decimalToRaw2(decimal, decimals) {
|
|
|
53960
56057
|
}
|
|
53961
56058
|
function assertVaultSigner(entry) {
|
|
53962
56059
|
if (entry.role !== "vault") {
|
|
53963
|
-
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is a
|
|
56060
|
+
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is not a vault key, and vault fund signs from vault keys only (ED-10).`, {
|
|
56061
|
+
suggestion: `To move funds out of a promoted wallet, use: candle vault transfer <to> --from ${entry.label ?? entry.address}`
|
|
56062
|
+
});
|
|
53964
56063
|
}
|
|
53965
56064
|
}
|
|
56065
|
+
function assertTransferSigner(entry) {
|
|
56066
|
+
if (entry.role === "vault" || entry.role === "tee-wallet")
|
|
56067
|
+
return;
|
|
56068
|
+
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is an external wallet and cannot sign vault transfer shapes (ED-10).`, {
|
|
56069
|
+
suggestion: `Move funds out of an external wallet with: candle external sweep ${entry.label} --to <vault> --rpc-url <url>`
|
|
56070
|
+
});
|
|
56071
|
+
}
|
|
53966
56072
|
function namedSendFailure(plan) {
|
|
53967
56073
|
if (!plan.token)
|
|
53968
56074
|
return;
|
|
@@ -54707,9 +56813,9 @@ init_render();
|
|
|
54707
56813
|
init_solana_lite();
|
|
54708
56814
|
init_store();
|
|
54709
56815
|
init_vault_support();
|
|
54710
|
-
var
|
|
56816
|
+
var CHUNK2 = 100;
|
|
54711
56817
|
var LAMPORTS_PER_SOL = 1000000000n;
|
|
54712
|
-
function
|
|
56818
|
+
function formatSol3(lamports) {
|
|
54713
56819
|
const whole = lamports / LAMPORTS_PER_SOL;
|
|
54714
56820
|
const fraction = (lamports % LAMPORTS_PER_SOL).toString().padStart(9, "0").replace(/0+$/, "");
|
|
54715
56821
|
return fraction === "" ? whole.toString() : `${whole}.${fraction}`;
|
|
@@ -54719,8 +56825,8 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
|
|
|
54719
56825
|
const lamports = new Map;
|
|
54720
56826
|
const unavailable = [];
|
|
54721
56827
|
let failure;
|
|
54722
|
-
for (let at = 0;at < addresses.length; at +=
|
|
54723
|
-
const chunk = addresses.slice(at, at +
|
|
56828
|
+
for (let at = 0;at < addresses.length; at += CHUNK2) {
|
|
56829
|
+
const chunk = addresses.slice(at, at + CHUNK2);
|
|
54724
56830
|
try {
|
|
54725
56831
|
const accounts = await rpc2.getMultipleAccounts(chunk);
|
|
54726
56832
|
for (const [i, address] of chunk.entries())
|
|
@@ -54771,7 +56877,7 @@ async function vaultList(args, ctx) {
|
|
|
54771
56877
|
const all = vault.index.entries;
|
|
54772
56878
|
const matched = filter === undefined ? all : all.filter((entry) => matches(entry, filter));
|
|
54773
56879
|
const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
|
|
54774
|
-
const requests = Math.ceil(solana.length /
|
|
56880
|
+
const requests = Math.ceil(solana.length / CHUNK2);
|
|
54775
56881
|
const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
|
|
54776
56882
|
if (balances && solana.length > 0 && rpcHost !== undefined) {
|
|
54777
56883
|
deps.stderr.write(`Reading SOL for ${solana.length} addresses from ${rpcHost}, in ${requests} ${requests === 1 ? "request" : "requests"}. That endpoint sees all ${solana.length} together.
|
|
@@ -54833,7 +56939,7 @@ async function vaultList(args, ctx) {
|
|
|
54833
56939
|
const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
|
|
54834
56940
|
if (balances) {
|
|
54835
56941
|
const held = lamports.get(entry.address);
|
|
54836
|
-
row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" :
|
|
56942
|
+
row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
|
|
54837
56943
|
}
|
|
54838
56944
|
return row;
|
|
54839
56945
|
});
|
|
@@ -54842,9 +56948,9 @@ ${renderTable(headers, rows)}
|
|
|
54842
56948
|
`);
|
|
54843
56949
|
if (balances) {
|
|
54844
56950
|
deps.stdout.write(complete ? `
|
|
54845
|
-
total ${
|
|
56951
|
+
total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
|
|
54846
56952
|
` : `
|
|
54847
|
-
total ${
|
|
56953
|
+
total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
|
|
54848
56954
|
`);
|
|
54849
56955
|
}
|
|
54850
56956
|
return complete ? 0 : 3;
|
|
@@ -54870,6 +56976,7 @@ async function vaultPhrase(args, ctx) {
|
|
|
54870
56976
|
init_esm();
|
|
54871
56977
|
init_args();
|
|
54872
56978
|
init_deps();
|
|
56979
|
+
init_profiles();
|
|
54873
56980
|
init_render();
|
|
54874
56981
|
init_solana_lite();
|
|
54875
56982
|
|
|
@@ -54976,12 +57083,299 @@ function restoreRefusedFailure(init, error) {
|
|
|
54976
57083
|
// src/commands/vault-promote.ts
|
|
54977
57084
|
init_errors();
|
|
54978
57085
|
init_promote_support();
|
|
57086
|
+
// src/vault/promote-to-key.ts
|
|
57087
|
+
init_deps();
|
|
57088
|
+
init_render();
|
|
57089
|
+
var REBIND_CHUNK = 200;
|
|
57090
|
+
var SELECTED_SCOPE_LIMIT = 50;
|
|
57091
|
+
function targetKeyRefusal(row, keyPrefix, now) {
|
|
57092
|
+
const written = "Nothing was written.";
|
|
57093
|
+
if (row === undefined) {
|
|
57094
|
+
return {
|
|
57095
|
+
code: "REBIND_KEY_NOT_FOUND",
|
|
57096
|
+
message: `No key ${keyPrefix} on this account. ${written}`,
|
|
57097
|
+
suggestion: "The target must be a key on the same account: candle keys list"
|
|
57098
|
+
};
|
|
57099
|
+
}
|
|
57100
|
+
if (row.revokedAt !== undefined) {
|
|
57101
|
+
return {
|
|
57102
|
+
code: "REBIND_TARGET_INVALID",
|
|
57103
|
+
message: `The target key ${keyPrefix} is revoked. ${written}`,
|
|
57104
|
+
suggestion: "Pick an active key: candle keys list"
|
|
57105
|
+
};
|
|
57106
|
+
}
|
|
57107
|
+
if (row.expiresAt !== undefined && row.expiresAt !== null && row.expiresAt <= now) {
|
|
57108
|
+
return {
|
|
57109
|
+
code: "REBIND_TARGET_INVALID",
|
|
57110
|
+
message: `The target key ${keyPrefix} has expired. ${written}`,
|
|
57111
|
+
suggestion: "Pick an active key: candle keys list"
|
|
57112
|
+
};
|
|
57113
|
+
}
|
|
57114
|
+
if (row.environment !== "production") {
|
|
57115
|
+
return {
|
|
57116
|
+
code: "REBIND_TARGET_INVALID",
|
|
57117
|
+
message: `The target key ${keyPrefix} is a test key; the trade rail refuses test keys. ${written}`,
|
|
57118
|
+
suggestion: "Pick a production key: candle keys list"
|
|
57119
|
+
};
|
|
57120
|
+
}
|
|
57121
|
+
if (!row.scopes.includes("swap:write")) {
|
|
57122
|
+
return {
|
|
57123
|
+
code: "REBIND_TARGET_INVALID",
|
|
57124
|
+
message: `The target key ${keyPrefix} lacks swap:write, which a TEE wallet needs to trade. ${written}`,
|
|
57125
|
+
suggestion: "Pick a key with swap:write, or mint one: candle keys create"
|
|
57126
|
+
};
|
|
57127
|
+
}
|
|
57128
|
+
return null;
|
|
57129
|
+
}
|
|
57130
|
+
function tradeReadinessOf(row) {
|
|
57131
|
+
const hasTxLimit = row.txLimit !== null && row.txLimit !== undefined;
|
|
57132
|
+
const limits = row.spendLimits ?? [];
|
|
57133
|
+
const cap = (asset) => limits.some((limit) => limit.asset === asset.toLowerCase() || limit.asset === asset.toUpperCase());
|
|
57134
|
+
const sol = cap("sol");
|
|
57135
|
+
const usdc = cap("usdc");
|
|
57136
|
+
const missingCaps = [];
|
|
57137
|
+
if (!hasTxLimit)
|
|
57138
|
+
missingCaps.push("txLimit");
|
|
57139
|
+
if (!sol)
|
|
57140
|
+
missingCaps.push("spendLimits.sol");
|
|
57141
|
+
if (!usdc)
|
|
57142
|
+
missingCaps.push("spendLimits.usdc");
|
|
57143
|
+
return { tradeReady: { sol: hasTxLimit && sol, usdc: hasTxLimit && usdc }, missingCaps };
|
|
57144
|
+
}
|
|
57145
|
+
function targetWarnings(target) {
|
|
57146
|
+
return capWarnings(target.keyPrefix, {
|
|
57147
|
+
keyPrefix: target.keyPrefix,
|
|
57148
|
+
label: target.label,
|
|
57149
|
+
paused: target.paused,
|
|
57150
|
+
walletScope: target.walletScope,
|
|
57151
|
+
tradeReady: target.tradeReady,
|
|
57152
|
+
missingCaps: target.missingCaps,
|
|
57153
|
+
launchScope: target.launchScope
|
|
57154
|
+
});
|
|
57155
|
+
}
|
|
57156
|
+
async function preflightToKey(ctx, raw, opts) {
|
|
57157
|
+
const { deps, json } = ctx;
|
|
57158
|
+
const refuse2 = (failure) => {
|
|
57159
|
+
writeLocalFailure(deps, failure, json);
|
|
57160
|
+
return { ok: false, exit: 1 };
|
|
57161
|
+
};
|
|
57162
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
57163
|
+
if (!deviceToken)
|
|
57164
|
+
return refuse2(DEVICE_TOKEN_REQUIRED);
|
|
57165
|
+
const listing = await listAccountKeys(ctx, deviceToken);
|
|
57166
|
+
if (!listing.ok) {
|
|
57167
|
+
return refuse2({
|
|
57168
|
+
code: listing.code ?? `HTTP ${listing.status}`,
|
|
57169
|
+
message: `Could not read this account's keys to check --to-key (${listing.status === 0 ? listing.message : `HTTP ${listing.status}`}). Nothing was written.`,
|
|
57170
|
+
suggestion: "Check the device token with: candle auth status"
|
|
57171
|
+
});
|
|
57172
|
+
}
|
|
57173
|
+
const keys = listing.body?.keys ?? [];
|
|
57174
|
+
const resolved = await resolveTargetKey(ctx, deviceToken, raw, { keys });
|
|
57175
|
+
if (!resolved.ok)
|
|
57176
|
+
return { ok: false, exit: resolved.code };
|
|
57177
|
+
const keyPrefix = resolved.keyPrefix;
|
|
57178
|
+
const row = keys.find((key) => key.keyPrefix === keyPrefix);
|
|
57179
|
+
const refusal = targetKeyRefusal(row, keyPrefix, deps.now());
|
|
57180
|
+
if (refusal !== null)
|
|
57181
|
+
return refuse2(refusal);
|
|
57182
|
+
const target = row;
|
|
57183
|
+
const walletScope = target.walletScope === "selected" ? "selected" : "all";
|
|
57184
|
+
if (walletScope === "selected") {
|
|
57185
|
+
const room = await readSelectedScopeRoom(ctx, keyPrefix);
|
|
57186
|
+
if (room.ok) {
|
|
57187
|
+
const moving = opts.labels.filter((label) => !room.heldLabels.has(label)).length;
|
|
57188
|
+
if (room.held + moving > SELECTED_SCOPE_LIMIT) {
|
|
57189
|
+
return refuse2({
|
|
57190
|
+
code: "REBIND_SCOPE_FULL",
|
|
57191
|
+
message: `Key ${keyPrefix} is scoped to selected wallets and holds ${room.held} of ${SELECTED_SCOPE_LIMIT}; the ${moving} this run would move do not fit. Nothing was written.`,
|
|
57192
|
+
suggestion: "Widen the key's wallet scope from the portal, or name a key with room."
|
|
57193
|
+
});
|
|
57194
|
+
}
|
|
57195
|
+
} else {
|
|
57196
|
+
deps.stderr.write(`Could not read key ${keyPrefix}'s wallet set (${room.reason}); its selected-scope room is checked at the rebind.
|
|
57197
|
+
`);
|
|
57198
|
+
}
|
|
57199
|
+
}
|
|
57200
|
+
const readiness = tradeReadinessOf(target);
|
|
57201
|
+
return {
|
|
57202
|
+
ok: true,
|
|
57203
|
+
deviceToken,
|
|
57204
|
+
target: {
|
|
57205
|
+
keyPrefix,
|
|
57206
|
+
label: typeof target.label === "string" && target.label.length > 0 ? target.label : null,
|
|
57207
|
+
walletScope,
|
|
57208
|
+
paused: target.pausedAt !== undefined && target.pausedAt !== null,
|
|
57209
|
+
launchScope: target.scopes.includes("launch:write"),
|
|
57210
|
+
...readiness
|
|
57211
|
+
}
|
|
57212
|
+
};
|
|
57213
|
+
}
|
|
57214
|
+
async function readSelectedScopeRoom(ctx, keyPrefix) {
|
|
57215
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
57216
|
+
if (!apiKey)
|
|
57217
|
+
return { ok: false, reason: "no API key" };
|
|
57218
|
+
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, {
|
|
57219
|
+
auth: "key",
|
|
57220
|
+
credentials: { apiKey },
|
|
57221
|
+
apiUrl: ctx.apiUrl,
|
|
57222
|
+
fetch: ctx.deps.fetch,
|
|
57223
|
+
env: ctx.deps.env
|
|
57224
|
+
});
|
|
57225
|
+
if (!result.ok)
|
|
57226
|
+
return { ok: false, reason: result.status === 0 ? result.message : `HTTP ${result.status}` };
|
|
57227
|
+
const wallets2 = result.body?.wallets;
|
|
57228
|
+
if (!Array.isArray(wallets2))
|
|
57229
|
+
return { ok: false, reason: "no wallet list in the response" };
|
|
57230
|
+
const heldLabels = new Set;
|
|
57231
|
+
for (const wallet of wallets2)
|
|
57232
|
+
if (typeof wallet?.label === "string")
|
|
57233
|
+
heldLabels.add(wallet.label);
|
|
57234
|
+
return { ok: true, held: wallets2.length, heldLabels };
|
|
57235
|
+
}
|
|
57236
|
+
function chunkWallets(wallets2, size = REBIND_CHUNK) {
|
|
57237
|
+
const out = [];
|
|
57238
|
+
for (let at = 0;at < wallets2.length; at += size)
|
|
57239
|
+
out.push(wallets2.slice(at, at + size));
|
|
57240
|
+
return out;
|
|
57241
|
+
}
|
|
57242
|
+
async function rebindPromoted(ctx, deviceToken, toKeyPrefix, wallets2) {
|
|
57243
|
+
const run = { ok: true, requests: 0, outcomes: new Map };
|
|
57244
|
+
for (const wallet of wallets2)
|
|
57245
|
+
run.outcomes.set(wallet.id, { state: "not-reached" });
|
|
57246
|
+
const chunks = chunkWallets(wallets2);
|
|
57247
|
+
for (const [at, chunk] of chunks.entries()) {
|
|
57248
|
+
run.requests += 1;
|
|
57249
|
+
const preview = await postRebind(ctx, deviceToken, {
|
|
57250
|
+
dryRun: true,
|
|
57251
|
+
toKeyPrefix,
|
|
57252
|
+
wallets: chunk.map((wallet) => wallet.id)
|
|
57253
|
+
});
|
|
57254
|
+
if (!preview.ok) {
|
|
57255
|
+
run.ok = false;
|
|
57256
|
+
run.failure = { ...rebindFailureDetails(preview, {}), chunk: at + 1 };
|
|
57257
|
+
return run;
|
|
57258
|
+
}
|
|
57259
|
+
const shown = preview.body;
|
|
57260
|
+
if (run.toKey === undefined)
|
|
57261
|
+
run.toKey = shown.toKey;
|
|
57262
|
+
for (const row of shown.unchanged)
|
|
57263
|
+
run.outcomes.set(row.id, { state: "unchanged" });
|
|
57264
|
+
const moving = shown.rebound;
|
|
57265
|
+
if (moving.length === 0)
|
|
57266
|
+
continue;
|
|
57267
|
+
run.requests += 1;
|
|
57268
|
+
const committed = await postRebind(ctx, deviceToken, {
|
|
57269
|
+
toKeyPrefix,
|
|
57270
|
+
walletIds: moving.map((row) => row.id),
|
|
57271
|
+
expect: Object.fromEntries(moving.map((row) => [row.id, row.fromKeyPrefix]))
|
|
57272
|
+
});
|
|
57273
|
+
if (!committed.ok) {
|
|
57274
|
+
run.ok = false;
|
|
57275
|
+
run.failure = {
|
|
57276
|
+
...rebindFailureDetails(committed, {
|
|
57277
|
+
fromKeyPrefixes: Array.from(new Set(moving.map((row) => row.fromKeyPrefix)))
|
|
57278
|
+
}),
|
|
57279
|
+
chunk: at + 1
|
|
57280
|
+
};
|
|
57281
|
+
for (const row of moving)
|
|
57282
|
+
run.outcomes.set(row.id, { state: "failed" });
|
|
57283
|
+
return run;
|
|
57284
|
+
}
|
|
57285
|
+
const result = committed.body;
|
|
57286
|
+
for (const row of result.rebound) {
|
|
57287
|
+
run.outcomes.set(row.id, { state: "rebound", ...row.auditId !== undefined ? { auditId: row.auditId } : {} });
|
|
57288
|
+
}
|
|
57289
|
+
for (const row of result.unchanged)
|
|
57290
|
+
run.outcomes.set(row.id, { state: "unchanged" });
|
|
57291
|
+
}
|
|
57292
|
+
return run;
|
|
57293
|
+
}
|
|
57294
|
+
function finishingCommands(addresses, toKeyPrefix) {
|
|
57295
|
+
return chunkWallets(addresses).map((chunk) => `candle tee rebind ${chunk.join(" ")} --to-key ${toKeyPrefix}`);
|
|
57296
|
+
}
|
|
57297
|
+
function pendingWallets(wallets2, run) {
|
|
57298
|
+
return wallets2.filter((wallet) => {
|
|
57299
|
+
const state = run?.outcomes.get(wallet.id)?.state;
|
|
57300
|
+
return state !== "rebound" && state !== "unchanged";
|
|
57301
|
+
});
|
|
57302
|
+
}
|
|
57303
|
+
function renderRebindReport(input) {
|
|
57304
|
+
const { target, wallets: wallets2, run, notRebindable } = input;
|
|
57305
|
+
const lines = [];
|
|
57306
|
+
const name = (wallet) => `${wallet.label} (${wallet.address})`;
|
|
57307
|
+
if (run !== undefined) {
|
|
57308
|
+
const rebound = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "rebound").length;
|
|
57309
|
+
const unchanged = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "unchanged").length;
|
|
57310
|
+
if (rebound > 0 || unchanged > 0) {
|
|
57311
|
+
lines.push(`✓ ${rebound} wallet${rebound === 1 ? "" : "s"} moved to key ${target.keyPrefix}${unchanged > 0 ? ` (${unchanged} already there)` : ""} in ${run.requests} request${run.requests === 1 ? "" : "s"}.`);
|
|
57312
|
+
}
|
|
57313
|
+
if (run.failure !== undefined) {
|
|
57314
|
+
lines.push(`Rebind ${run.failure.code}: ${run.failure.message}${run.failure.suggestion ? ` ${run.failure.suggestion}` : ""}`);
|
|
57315
|
+
}
|
|
57316
|
+
}
|
|
57317
|
+
const pending = pendingWallets(wallets2, run);
|
|
57318
|
+
if (pending.length > 0) {
|
|
57319
|
+
lines.push(`${pending.length} promoted wallet${pending.length === 1 ? " is" : "s are"} still bound to the calling key ${input.callingKeyPrefix}, not ${target.keyPrefix}:`, ...pending.map((wallet) => ` ${name(wallet)}`), "Finish with:", ...finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix).map((command) => ` ${command}`));
|
|
57320
|
+
}
|
|
57321
|
+
if (notRebindable.length > 0) {
|
|
57322
|
+
lines.push(`${notRebindable.length} wallet${notRebindable.length === 1 ? "" : "s"} cannot be rebound yet (only a verified-active wallet moves):`, ...notRebindable.map((wallet) => ` ${name(wallet)}: ${wallet.reason}`));
|
|
57323
|
+
}
|
|
57324
|
+
return lines.join(`
|
|
57325
|
+
`);
|
|
57326
|
+
}
|
|
57327
|
+
function walletRebindJson(wallet, run) {
|
|
57328
|
+
if (wallet.id === null || wallet.remoteAuthority !== "verified-active") {
|
|
57329
|
+
return {
|
|
57330
|
+
state: "not-rebindable",
|
|
57331
|
+
reason: wallet.id === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
|
|
57332
|
+
};
|
|
57333
|
+
}
|
|
57334
|
+
const outcome = run?.outcomes.get(wallet.id);
|
|
57335
|
+
if (outcome === undefined)
|
|
57336
|
+
return { state: "not-reached" };
|
|
57337
|
+
return { state: outcome.state, ...outcome.auditId !== undefined ? { auditId: outcome.auditId } : {} };
|
|
57338
|
+
}
|
|
57339
|
+
function finalBoundKey(wallet, run, toKeyPrefix) {
|
|
57340
|
+
const state = wallet.id === null ? undefined : run?.outcomes.get(wallet.id)?.state;
|
|
57341
|
+
return state === "rebound" || state === "unchanged" ? toKeyPrefix : wallet.importedTo;
|
|
57342
|
+
}
|
|
57343
|
+
function rebindJson(input) {
|
|
57344
|
+
const { target, wallets: wallets2, run } = input;
|
|
57345
|
+
const pending = pendingWallets(wallets2, run);
|
|
57346
|
+
const count = (state) => wallets2.filter((wallet) => run?.outcomes.get(wallet.id)?.state === state).length;
|
|
57347
|
+
return {
|
|
57348
|
+
toKey: {
|
|
57349
|
+
keyPrefix: target.keyPrefix,
|
|
57350
|
+
label: target.label,
|
|
57351
|
+
tradeReady: run?.toKey?.tradeReady ?? target.tradeReady,
|
|
57352
|
+
missingCaps: run?.toKey?.missingCaps ?? target.missingCaps
|
|
57353
|
+
},
|
|
57354
|
+
rebind: {
|
|
57355
|
+
ok: run !== undefined && run.ok && input.notRebindable.length === 0,
|
|
57356
|
+
reached: run !== undefined,
|
|
57357
|
+
requests: run?.requests ?? 0,
|
|
57358
|
+
rebound: count("rebound"),
|
|
57359
|
+
unchanged: count("unchanged"),
|
|
57360
|
+
pending: pending.map((wallet) => wallet.address),
|
|
57361
|
+
notRebindable: input.notRebindable.length,
|
|
57362
|
+
...run?.failure !== undefined ? {
|
|
57363
|
+
code: run.failure.code,
|
|
57364
|
+
message: run.failure.message,
|
|
57365
|
+
...run.failure.suggestion !== undefined ? { suggestion: run.failure.suggestion } : {}
|
|
57366
|
+
} : {},
|
|
57367
|
+
finishWith: finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix)
|
|
57368
|
+
}
|
|
57369
|
+
};
|
|
57370
|
+
}
|
|
57371
|
+
|
|
57372
|
+
// src/commands/vault-promote.ts
|
|
54979
57373
|
init_signer_roles();
|
|
54980
57374
|
init_store();
|
|
54981
57375
|
init_vault_support();
|
|
54982
57376
|
async function vaultPromote(args, ctx) {
|
|
54983
57377
|
const parsed = parseArgs(args, {
|
|
54984
|
-
valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore"],
|
|
57378
|
+
valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore", "--to-key"],
|
|
54985
57379
|
booleanFlags: ["--accept-unknown-exposure", "--accept-older-copy"],
|
|
54986
57380
|
pathFlags: ["--keystore"]
|
|
54987
57381
|
});
|
|
@@ -54995,21 +57389,73 @@ async function vaultPromote(args, ctx) {
|
|
|
54995
57389
|
return usage(ctx, "Use either --from or --in-place, not both.");
|
|
54996
57390
|
}
|
|
54997
57391
|
if (fromLabel === undefined && inPlaceLabel === undefined) {
|
|
54998
|
-
return usage(ctx, "Usage: candle vault promote --from <vault-key-label> | --in-place <vault-key-label> --sweep-to <label> --rpc-url <url>");
|
|
57392
|
+
return usage(ctx, "Usage: candle vault promote --from <vault-key-label> | --in-place <vault-key-label> --sweep-to <label> --rpc-url <url> [--to-key <label|prefix>]");
|
|
54999
57393
|
}
|
|
57394
|
+
const toKeyRaw = parsed.values["--to-key"];
|
|
57395
|
+
if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
|
|
57396
|
+
return usage(ctx, "--to-key needs a label or a prefix.");
|
|
55000
57397
|
if (!refuseEnvPassphrase(ctx))
|
|
55001
57398
|
return 1;
|
|
55002
57399
|
if (!requireTty(ctx, "vault promote"))
|
|
55003
57400
|
return 1;
|
|
55004
|
-
if (fromLabel !== undefined) {
|
|
55005
|
-
|
|
55006
|
-
|
|
57401
|
+
if (fromLabel !== undefined && parsed.values["--sweep-to"] !== undefined) {
|
|
57402
|
+
return usage(ctx, "Fresh-key promote takes --from, not --sweep-to.");
|
|
57403
|
+
}
|
|
57404
|
+
let toKey;
|
|
57405
|
+
if (toKeyRaw !== undefined) {
|
|
57406
|
+
const label = parsed.values["--label"] ?? inPlaceLabel ?? (fromLabel !== undefined ? "<fresh>" : undefined);
|
|
57407
|
+
const preflight = await preflightToKey(ctx, toKeyRaw, { labels: label !== undefined ? [label] : [] });
|
|
57408
|
+
if (!preflight.ok)
|
|
57409
|
+
return preflight.exit;
|
|
57410
|
+
toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
|
|
57411
|
+
}
|
|
57412
|
+
if (fromLabel !== undefined)
|
|
57413
|
+
return promoteFresh(ctx, parsed, fromLabel, toKey);
|
|
57414
|
+
return promoteInPlace(ctx, parsed, inPlaceLabel, toKey);
|
|
57415
|
+
}
|
|
57416
|
+
async function rebindAfterImport(ctx, toKey, wallet, callingKeyPrefix) {
|
|
57417
|
+
const rebindable = wallet.linkedWalletId !== null && wallet.remoteAuthority === "verified-active" ? [{ id: wallet.linkedWalletId, address: wallet.address, label: wallet.label }] : [];
|
|
57418
|
+
const notRebindable = rebindable.length === 0 ? [
|
|
57419
|
+
{
|
|
57420
|
+
address: wallet.address,
|
|
57421
|
+
label: wallet.label,
|
|
57422
|
+
reason: wallet.linkedWalletId === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
|
|
57423
|
+
}
|
|
57424
|
+
] : [];
|
|
57425
|
+
const run = rebindable.length > 0 ? await rebindPromoted(ctx, toKey.deviceToken, toKey.target.keyPrefix, rebindable) : undefined;
|
|
57426
|
+
const input = { target: toKey.target, callingKeyPrefix, wallets: rebindable, run, notRebindable };
|
|
57427
|
+
const report = renderRebindReport(input);
|
|
57428
|
+
if (report.length > 0)
|
|
57429
|
+
ctx.deps.stderr.write(`${report}
|
|
57430
|
+
`);
|
|
57431
|
+
const id = wallet.linkedWalletId;
|
|
57432
|
+
return {
|
|
57433
|
+
exit: run !== undefined && !run.ok ? 1 : 0,
|
|
57434
|
+
json: {
|
|
57435
|
+
...rebindJson(input),
|
|
57436
|
+
boundKeyPrefix: finalBoundKey({ id, importedTo: wallet.importedTo }, run, toKey.target.keyPrefix),
|
|
57437
|
+
walletRebind: walletRebindJson({ id, remoteAuthority: wallet.remoteAuthority }, run)
|
|
55007
57438
|
}
|
|
55008
|
-
|
|
57439
|
+
};
|
|
57440
|
+
}
|
|
57441
|
+
async function confirmResumeRebind(ctx, toKey) {
|
|
57442
|
+
const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
|
|
57443
|
+
ctx.deps.stderr.write(`${[
|
|
57444
|
+
`This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
|
|
57445
|
+
...targetWarnings(toKey.target)
|
|
57446
|
+
].join(`
|
|
57447
|
+
`)}
|
|
57448
|
+
`);
|
|
57449
|
+
const typed = await ctx.deps.promptLine(`Type ${CONFIRM_WORD} to move this wallet to ${toKey.target.keyPrefix}: `);
|
|
57450
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
57451
|
+
throw new VaultError("PROMOTE_NOT_ACKNOWLEDGED", `The acknowledgement is the word ${CONFIRM_WORD}; nothing was moved, and nothing was written.`, { suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.` });
|
|
55009
57452
|
}
|
|
55010
|
-
return promoteInPlace(ctx, parsed, inPlaceLabel);
|
|
55011
57453
|
}
|
|
55012
|
-
async function
|
|
57454
|
+
async function callingKeyPrefixFor(ctx) {
|
|
57455
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
57456
|
+
return (apiKey !== undefined ? apiKeyPrefix(apiKey) : undefined) ?? "(the calling key)";
|
|
57457
|
+
}
|
|
57458
|
+
async function promoteFresh(ctx, parsed, fromLabel, toKey) {
|
|
55013
57459
|
if ("error" in parsed)
|
|
55014
57460
|
return usage(ctx, parsed.error);
|
|
55015
57461
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
@@ -55088,10 +57534,19 @@ async function promoteFresh(ctx, parsed, fromLabel) {
|
|
|
55088
57534
|
addKeys: [blob]
|
|
55089
57535
|
}, ctx.deps));
|
|
55090
57536
|
await confirmLastSix(ctx, destination.address, "the sweep vault destination");
|
|
57537
|
+
if (toKey !== undefined) {
|
|
57538
|
+
const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
|
|
57539
|
+
ctx.deps.stderr.write(`${[
|
|
57540
|
+
`This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
|
|
57541
|
+
...targetWarnings(toKey.target)
|
|
57542
|
+
].join(`
|
|
57543
|
+
`)}
|
|
57544
|
+
`);
|
|
57545
|
+
}
|
|
55091
57546
|
const privateKey = base58.encode(secret64);
|
|
55092
57547
|
wipe(secret64);
|
|
55093
57548
|
const importCount = { n: 0 };
|
|
55094
|
-
const { exit: code } = await runTeeImport(ctx, {
|
|
57549
|
+
const { exit: code, submitted } = await runTeeImport(ctx, {
|
|
55095
57550
|
address,
|
|
55096
57551
|
privateKey,
|
|
55097
57552
|
label: entry.label,
|
|
@@ -55110,22 +57565,35 @@ async function promoteFresh(ctx, parsed, fromLabel) {
|
|
|
55110
57565
|
if (target === undefined) {
|
|
55111
57566
|
throw new VaultError("VAULT_INDEX_INVALID", `Entry ${keyId} missing after import.`);
|
|
55112
57567
|
}
|
|
57568
|
+
let rebound;
|
|
57569
|
+
if (toKey !== undefined) {
|
|
57570
|
+
const importedTo = submitted?.boundKeyPrefix ?? null;
|
|
57571
|
+
rebound = await rebindAfterImport(ctx, toKey, {
|
|
57572
|
+
linkedWalletId: target.linkedWalletId ?? null,
|
|
57573
|
+
address,
|
|
57574
|
+
label: entry.label,
|
|
57575
|
+
remoteAuthority: target.tee?.remoteAuthority ?? submitted?.remoteAuthority ?? null,
|
|
57576
|
+
importedTo
|
|
57577
|
+
}, await callingKeyPrefixFor(ctx));
|
|
57578
|
+
}
|
|
57579
|
+
const exit = Math.max(code, rebound?.exit ?? 0);
|
|
55113
57580
|
if (ctx.json) {
|
|
55114
57581
|
writeJson(ctx.deps, {
|
|
55115
|
-
ok:
|
|
57582
|
+
ok: rebound === undefined || rebound.exit === 0,
|
|
55116
57583
|
mode: "fresh",
|
|
55117
57584
|
address,
|
|
55118
57585
|
label: entry.label,
|
|
55119
57586
|
vaultDestination: destination.address,
|
|
55120
57587
|
lifecycle: target.tee?.lifecycle,
|
|
55121
57588
|
linkedWalletId: target.linkedWalletId ?? null,
|
|
55122
|
-
importCalls: importCount.n
|
|
57589
|
+
importCalls: importCount.n,
|
|
57590
|
+
...rebound?.json ?? {}
|
|
55123
57591
|
});
|
|
55124
57592
|
} else {
|
|
55125
57593
|
ctx.deps.stdout.write(`Promoted fresh TEE wallet ${address} (sweep to ${destination.address}).
|
|
55126
57594
|
`);
|
|
55127
57595
|
}
|
|
55128
|
-
return
|
|
57596
|
+
return exit;
|
|
55129
57597
|
});
|
|
55130
57598
|
}
|
|
55131
57599
|
async function refuseWithoutRoom(ctx) {
|
|
@@ -55147,7 +57615,7 @@ async function reopenFromDisk(path, reopen, previous) {
|
|
|
55147
57615
|
});
|
|
55148
57616
|
return reopen(path, raw);
|
|
55149
57617
|
}
|
|
55150
|
-
async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
57618
|
+
async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
|
|
55151
57619
|
if ("error" in parsed)
|
|
55152
57620
|
return usage(ctx, parsed.error);
|
|
55153
57621
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
@@ -55174,7 +57642,9 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55174
57642
|
if (sweepTo !== undefined) {
|
|
55175
57643
|
return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
|
|
55176
57644
|
}
|
|
55177
|
-
|
|
57645
|
+
if (toKey !== undefined)
|
|
57646
|
+
await confirmResumeRebind(ctx, toKey);
|
|
57647
|
+
const resumed = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
|
|
55178
57648
|
confirmAccount: (account) => confirmLastSix(ctx, account, "that account"),
|
|
55179
57649
|
confirmDestination: async (destination2) => {
|
|
55180
57650
|
try {
|
|
@@ -55183,9 +57653,36 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55183
57653
|
} catch {
|
|
55184
57654
|
return false;
|
|
55185
57655
|
}
|
|
55186
|
-
}
|
|
57656
|
+
},
|
|
57657
|
+
report: toKey !== undefined ? "return" : "write"
|
|
55187
57658
|
});
|
|
55188
|
-
|
|
57659
|
+
if (toKey === undefined)
|
|
57660
|
+
return resumed.exit;
|
|
57661
|
+
if (resumed.failure !== undefined || resumed.exit !== 0) {
|
|
57662
|
+
writeLocalFailure(ctx.deps, resumed.failure ?? { code: "PROMOTE_OUTCOME_UNRESOLVED", message: "The resume did not complete." }, ctx.json);
|
|
57663
|
+
return resumed.exit;
|
|
57664
|
+
}
|
|
57665
|
+
const rebound2 = await rebindAfterImport(ctx, toKey, {
|
|
57666
|
+
linkedWalletId: resumed.adopted?.linkedWalletId ?? null,
|
|
57667
|
+
address: existing.address,
|
|
57668
|
+
label: existing.label,
|
|
57669
|
+
remoteAuthority: resumed.adopted?.remoteAuthority ?? null,
|
|
57670
|
+
importedTo: existing.tee?.boundKeyPrefix ?? null
|
|
57671
|
+
}, await callingKeyPrefixFor(ctx));
|
|
57672
|
+
if (ctx.json) {
|
|
57673
|
+
writeJson(ctx.deps, {
|
|
57674
|
+
ok: rebound2.exit === 0,
|
|
57675
|
+
mode: "resume",
|
|
57676
|
+
address: existing.address,
|
|
57677
|
+
lifecycle: "enabled",
|
|
57678
|
+
importCalls: 0,
|
|
57679
|
+
...rebound2.json
|
|
57680
|
+
});
|
|
57681
|
+
} else {
|
|
57682
|
+
ctx.deps.stdout.write(`Resumed ${existing.address}: grant adopted; no re-import.
|
|
57683
|
+
`);
|
|
57684
|
+
}
|
|
57685
|
+
return rebound2.exit;
|
|
55189
57686
|
}
|
|
55190
57687
|
if (sweepTo === undefined || rpcUrl2 === undefined) {
|
|
55191
57688
|
return usage(ctx, "Usage: candle vault promote --in-place <label> --sweep-to <label> --rpc-url <url> [--label] [--accept-unknown-exposure]");
|
|
@@ -55197,7 +57694,12 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55197
57694
|
return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
|
|
55198
57695
|
}
|
|
55199
57696
|
await refuseWithoutRoom(ctx);
|
|
55200
|
-
const
|
|
57697
|
+
const live = await readControlledBy(ctx);
|
|
57698
|
+
const controlledBy = toKey === undefined ? live : withToKey(live, {
|
|
57699
|
+
keyPrefix: toKey.target.keyPrefix,
|
|
57700
|
+
label: toKey.target.label,
|
|
57701
|
+
warnings: targetWarnings(toKey.target)
|
|
57702
|
+
});
|
|
55201
57703
|
const rpc2 = createSolanaRpc(rpcUrl2, ctx.deps.fetch);
|
|
55202
57704
|
const host = new URL(rpcUrl2).host;
|
|
55203
57705
|
await displayHoldings(ctx, rpc2, first.subject.address);
|
|
@@ -55267,24 +57769,37 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55267
57769
|
writeLocalFailure(ctx.deps, failure, ctx.json);
|
|
55268
57770
|
return code;
|
|
55269
57771
|
}
|
|
57772
|
+
let rebound;
|
|
57773
|
+
if (toKey !== undefined && imported.submitted !== undefined) {
|
|
57774
|
+
const importedTo = imported.submitted.boundKeyPrefix ?? null;
|
|
57775
|
+
rebound = await rebindAfterImport(ctx, toKey, {
|
|
57776
|
+
linkedWalletId: imported.submitted.id ?? null,
|
|
57777
|
+
address: subject.address,
|
|
57778
|
+
label: parsed.values["--label"] ?? subject.label,
|
|
57779
|
+
remoteAuthority: imported.submitted.remoteAuthority ?? null,
|
|
57780
|
+
importedTo
|
|
57781
|
+
}, controlledBy.keyPrefix);
|
|
57782
|
+
}
|
|
57783
|
+
const exit = Math.max(code, rebound?.exit ?? 0);
|
|
55270
57784
|
if (ctx.json) {
|
|
55271
57785
|
const reopened = hold(await reopenFromDisk(path, opened.reopen, vault));
|
|
55272
57786
|
const updated = reopened.index.entries.find((e) => e.id === subject.id);
|
|
55273
57787
|
writeJson(ctx.deps, {
|
|
55274
|
-
ok: code === 0 || code === 3,
|
|
57788
|
+
ok: (code === 0 || code === 3) && (rebound === undefined || rebound.exit === 0),
|
|
55275
57789
|
mode: "in-place",
|
|
55276
57790
|
address: subject.address,
|
|
55277
57791
|
vaultDestination: destination.address,
|
|
55278
57792
|
lifecycle: updated?.tee?.lifecycle ?? null,
|
|
55279
57793
|
linkedWalletId: updated?.linkedWalletId ?? null,
|
|
55280
57794
|
controlledBy: controlledByJson(controlledBy),
|
|
55281
|
-
authorities: authoritiesJson(roles)
|
|
57795
|
+
authorities: authoritiesJson(roles),
|
|
57796
|
+
...rebound?.json ?? {}
|
|
55282
57797
|
});
|
|
55283
57798
|
} else if (code === 0 || code === 3) {
|
|
55284
57799
|
ctx.deps.stdout.write(`Promoted ${subject.address} in place (sweep to ${destination.address}). This address never returns to cold.
|
|
55285
57800
|
`);
|
|
55286
57801
|
}
|
|
55287
|
-
return
|
|
57802
|
+
return exit;
|
|
55288
57803
|
});
|
|
55289
57804
|
}
|
|
55290
57805
|
async function resumePromote(ctx, vault, entry, reopen, path, hold, confirmations) {
|
|
@@ -55870,12 +58385,12 @@ function parseValueUsdCell(raw) {
|
|
|
55870
58385
|
const value = Number(cleaned);
|
|
55871
58386
|
return Number.isFinite(value) ? value : undefined;
|
|
55872
58387
|
}
|
|
55873
|
-
function
|
|
58388
|
+
function formatUsd2(value) {
|
|
55874
58389
|
const [whole, fraction] = value.toFixed(2).split(".");
|
|
55875
58390
|
const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
55876
58391
|
return `$${grouped}.${fraction ?? "00"}`;
|
|
55877
58392
|
}
|
|
55878
|
-
function
|
|
58393
|
+
function formatSol4(lamports) {
|
|
55879
58394
|
const whole = lamports / 1000000000n;
|
|
55880
58395
|
const fraction = lamports % 1000000000n;
|
|
55881
58396
|
return `${whole}.${fraction.toString().padStart(9, "0").slice(0, 6)}`;
|
|
@@ -55904,14 +58419,37 @@ init_promote_support();
|
|
|
55904
58419
|
init_signer_roles();
|
|
55905
58420
|
init_store();
|
|
55906
58421
|
init_vault_support();
|
|
55907
|
-
var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings] [--accept-unknown-exposure]";
|
|
58422
|
+
var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--to-key <label|prefix>] [--token-holdings] [--accept-unknown-exposure]";
|
|
55908
58423
|
var rowObserver = null;
|
|
58424
|
+
function splitRebindable(results) {
|
|
58425
|
+
const wallets2 = [];
|
|
58426
|
+
const notRebindable = [];
|
|
58427
|
+
for (const row of results) {
|
|
58428
|
+
if (row.linkedWalletId !== null && row.remoteAuthority === "verified-active") {
|
|
58429
|
+
wallets2.push({ id: row.linkedWalletId, address: row.address, label: row.label });
|
|
58430
|
+
} else {
|
|
58431
|
+
notRebindable.push({
|
|
58432
|
+
address: row.address,
|
|
58433
|
+
label: row.label,
|
|
58434
|
+
reason: row.linkedWalletId === null ? `no linked wallet id (${row.lifecycle})` : `remote authority is ${row.remoteAuthority ?? "unknown"}, not verified-active`
|
|
58435
|
+
});
|
|
58436
|
+
}
|
|
58437
|
+
}
|
|
58438
|
+
return { wallets: wallets2, notRebindable };
|
|
58439
|
+
}
|
|
58440
|
+
function keysWithRebind(results, phase, run) {
|
|
58441
|
+
return results.map((row) => ({
|
|
58442
|
+
...row,
|
|
58443
|
+
boundKeyPrefix: finalBoundKey({ id: row.linkedWalletId, importedTo: phase.importedTo.get(row.address) ?? null }, run, phase.toKey.target.keyPrefix),
|
|
58444
|
+
rebind: walletRebindJson({ id: row.linkedWalletId, remoteAuthority: row.remoteAuthority }, run)
|
|
58445
|
+
}));
|
|
58446
|
+
}
|
|
55909
58447
|
async function vaultPromoteBatch(args, ctx) {
|
|
55910
58448
|
if (args.some((arg) => arg === "--from" || arg.startsWith("--from="))) {
|
|
55911
58449
|
return usage(ctx, "promote-batch promotes existing vault keys in place and takes no --from. For fresh keys: candle vault new-key --chain solana --labels-from <file>, then candle vault promote --from <label> for each.");
|
|
55912
58450
|
}
|
|
55913
58451
|
const parsed = parseArgs(args, {
|
|
55914
|
-
valueFlags: ["--pairs-from", "--rpc-url", "--keystore"],
|
|
58452
|
+
valueFlags: ["--pairs-from", "--rpc-url", "--keystore", "--to-key"],
|
|
55915
58453
|
booleanFlags: ["--token-holdings", "--accept-unknown-exposure", "--accept-older-copy"],
|
|
55916
58454
|
pathFlags: ["--keystore", "--pairs-from"]
|
|
55917
58455
|
});
|
|
@@ -55922,6 +58460,9 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55922
58460
|
const pairsFile = parsed.values["--pairs-from"];
|
|
55923
58461
|
if (pairsFile === undefined)
|
|
55924
58462
|
return usage(ctx, USAGE_LINE);
|
|
58463
|
+
const toKeyRaw = parsed.values["--to-key"];
|
|
58464
|
+
if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
|
|
58465
|
+
return usage(ctx, "--to-key needs a label or a prefix.");
|
|
55925
58466
|
if (!refuseEnvPassphrase(ctx))
|
|
55926
58467
|
return 1;
|
|
55927
58468
|
if (!requireTty(ctx, "vault promote-batch"))
|
|
@@ -55946,6 +58487,13 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55946
58487
|
if (!parsedFile.ok)
|
|
55947
58488
|
return usage(ctx, renderPhaseAFindings(pairsFile, parsedFile.findings));
|
|
55948
58489
|
const { rows, hasValueUsd } = parsedFile;
|
|
58490
|
+
let toKey;
|
|
58491
|
+
if (toKeyRaw !== undefined) {
|
|
58492
|
+
const preflight = await preflightToKey(ctx, toKeyRaw, { labels: rows.map((row) => row.label) });
|
|
58493
|
+
if (!preflight.ok)
|
|
58494
|
+
return preflight.exit;
|
|
58495
|
+
toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
|
|
58496
|
+
}
|
|
55949
58497
|
return runVaultCommand(ctx, async ({ hold }) => {
|
|
55950
58498
|
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
55951
58499
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
@@ -55981,7 +58529,17 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55981
58529
|
if (roomRefusal !== null)
|
|
55982
58530
|
throw roomRefusal;
|
|
55983
58531
|
const room = roomRead.room;
|
|
55984
|
-
const
|
|
58532
|
+
const live = await readControlledBy(ctx);
|
|
58533
|
+
const controlledBy = toKey === undefined ? live : withToKey(live, {
|
|
58534
|
+
keyPrefix: toKey.target.keyPrefix,
|
|
58535
|
+
label: toKey.target.label,
|
|
58536
|
+
warnings: targetWarnings(toKey.target)
|
|
58537
|
+
});
|
|
58538
|
+
const rebindPhase = toKey === undefined ? undefined : { toKey, callingKeyPrefix: live.keyPrefix, importedTo: new Map };
|
|
58539
|
+
for (const item of planned) {
|
|
58540
|
+
if (item.kind === "skip")
|
|
58541
|
+
rebindPhase?.importedTo.set(item.subject.address, item.subject.tee?.boundKeyPrefix ?? null);
|
|
58542
|
+
}
|
|
55985
58543
|
const addresses = planned.map((item) => item.subject.address);
|
|
55986
58544
|
const rpc2 = createSolanaRpc(rpcUrl2, deps.fetch);
|
|
55987
58545
|
const host = new URL(rpcUrl2).host;
|
|
@@ -56040,28 +58598,58 @@ ${footer}
|
|
|
56040
58598
|
});
|
|
56041
58599
|
}
|
|
56042
58600
|
if (acting.length === 0) {
|
|
56043
|
-
|
|
58601
|
+
const roles2 = {
|
|
58602
|
+
checked: [...ROLE_GROUP_IDS],
|
|
58603
|
+
notChecked: [],
|
|
58604
|
+
found: [],
|
|
58605
|
+
requests: 0,
|
|
58606
|
+
planned: 0,
|
|
58607
|
+
rateLimited: 0,
|
|
58608
|
+
elapsedMs: 0
|
|
58609
|
+
};
|
|
58610
|
+
const skipped = planned.map((item) => skippedResult(item));
|
|
58611
|
+
const rebindable = rebindPhase === undefined ? undefined : splitRebindable(skipped);
|
|
58612
|
+
if (rebindPhase === undefined || rebindable === undefined || rebindable.wallets.length === 0) {
|
|
58613
|
+
deps.stderr.write(`
|
|
56044
58614
|
${table}
|
|
56045
58615
|
|
|
56046
58616
|
${footer}
|
|
56047
58617
|
Nothing to do: every row already landed.
|
|
56048
58618
|
`);
|
|
58619
|
+
return finish(ctx, {
|
|
58620
|
+
file: pairsFile,
|
|
58621
|
+
rows: rows.length,
|
|
58622
|
+
keys: skipped,
|
|
58623
|
+
destinations,
|
|
58624
|
+
exit: 0,
|
|
58625
|
+
controlledBy,
|
|
58626
|
+
roles: roles2
|
|
58627
|
+
});
|
|
58628
|
+
}
|
|
58629
|
+
const n = rebindable.wallets.length;
|
|
58630
|
+
deps.stderr.write(`
|
|
58631
|
+
${table}
|
|
58632
|
+
|
|
58633
|
+
${footer}
|
|
58634
|
+
Every row already landed; ${n} wallet${n === 1 ? "" : "s"} to rebind.
|
|
58635
|
+
`);
|
|
58636
|
+
deps.stderr.write(`
|
|
58637
|
+
${renderControlledBy(controlledBy, n)}
|
|
58638
|
+
`);
|
|
58639
|
+
const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to move ${n === 1 ? "this wallet" : `these ${n} wallets`} to ${rebindPhase.toKey.target.keyPrefix}: `);
|
|
58640
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
58641
|
+
throw new VaultError("PROMOTE_NOT_ACKNOWLEDGED", `The acknowledgement is the word ${CONFIRM_WORD}; nothing was moved, and nothing was written.`, { suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.` });
|
|
58642
|
+
}
|
|
58643
|
+
const rebound2 = await runRebindPhase(ctx, rebindPhase, skipped);
|
|
56049
58644
|
return finish(ctx, {
|
|
56050
58645
|
file: pairsFile,
|
|
56051
58646
|
rows: rows.length,
|
|
56052
|
-
keys:
|
|
58647
|
+
keys: skipped,
|
|
56053
58648
|
destinations,
|
|
56054
58649
|
exit: 0,
|
|
56055
58650
|
controlledBy,
|
|
56056
|
-
roles:
|
|
56057
|
-
|
|
56058
|
-
notChecked: [],
|
|
56059
|
-
found: [],
|
|
56060
|
-
requests: 0,
|
|
56061
|
-
planned: 0,
|
|
56062
|
-
rateLimited: 0,
|
|
56063
|
-
elapsedMs: 0
|
|
56064
|
-
}
|
|
58651
|
+
roles: roles2,
|
|
58652
|
+
rebound: rebound2
|
|
56065
58653
|
});
|
|
56066
58654
|
}
|
|
56067
58655
|
const checked = roles;
|
|
@@ -56152,6 +58740,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56152
58740
|
worst = Math.max(worst, imported.exit);
|
|
56153
58741
|
done += 1;
|
|
56154
58742
|
last = { address: subject2.address, keyId: subject2.id };
|
|
58743
|
+
rebindPhase?.importedTo.set(subject2.address, imported.submitted.boundKeyPrefix ?? null);
|
|
56155
58744
|
results.push({
|
|
56156
58745
|
line: row2.line,
|
|
56157
58746
|
label: subject2.label,
|
|
@@ -56191,6 +58780,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56191
58780
|
await rowObserver?.({ line: row.line, stage: "resumed", vault: current });
|
|
56192
58781
|
done += 1;
|
|
56193
58782
|
last = { address: subject.address, keyId: subject.id };
|
|
58783
|
+
rebindPhase?.importedTo.set(subject.address, subject.tee?.boundKeyPrefix ?? null);
|
|
56194
58784
|
results.push({
|
|
56195
58785
|
line: row.line,
|
|
56196
58786
|
label: subject.label,
|
|
@@ -56216,6 +58806,20 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56216
58806
|
}
|
|
56217
58807
|
if (stopped !== undefined) {
|
|
56218
58808
|
reportPartial(ctx, { landed: done, acting: acting.length, failure: stopped });
|
|
58809
|
+
let notReached;
|
|
58810
|
+
if (rebindPhase !== undefined) {
|
|
58811
|
+
const { wallets: wallets2, notRebindable } = splitRebindable(results);
|
|
58812
|
+
notReached = {
|
|
58813
|
+
target: rebindPhase.toKey.target,
|
|
58814
|
+
callingKeyPrefix: rebindPhase.callingKeyPrefix,
|
|
58815
|
+
wallets: wallets2,
|
|
58816
|
+
notRebindable
|
|
58817
|
+
};
|
|
58818
|
+
const report = renderRebindReport(notReached);
|
|
58819
|
+
if (report.length > 0)
|
|
58820
|
+
deps.stderr.write(`${report}
|
|
58821
|
+
`);
|
|
58822
|
+
}
|
|
56219
58823
|
if (ctx.json) {
|
|
56220
58824
|
writeJson(deps, {
|
|
56221
58825
|
ok: false,
|
|
@@ -56226,13 +58830,14 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56226
58830
|
resumed: results.filter((r) => r.state === "resumed").length,
|
|
56227
58831
|
skipped: results.filter((r) => r.state === "skipped").length,
|
|
56228
58832
|
destinations: destinations.map(({ label, address, keys }) => ({ label, address, keys })),
|
|
56229
|
-
keys: results,
|
|
58833
|
+
keys: rebindPhase === undefined ? results : keysWithRebind(results, rebindPhase, undefined),
|
|
56230
58834
|
failedLine: stopped.line,
|
|
56231
58835
|
code: stopped.code,
|
|
56232
58836
|
message: stopped.message,
|
|
56233
58837
|
...stopped.suggestion !== undefined ? { suggestion: stopped.suggestion } : {},
|
|
56234
58838
|
controlledBy: controlledByJson(controlledBy),
|
|
56235
|
-
authorities: authoritiesJson(checked)
|
|
58839
|
+
authorities: authoritiesJson(checked),
|
|
58840
|
+
...notReached !== undefined ? rebindJson(notReached) : {}
|
|
56236
58841
|
});
|
|
56237
58842
|
} else {
|
|
56238
58843
|
deps.stderr.write(`${stopped.message}${stopped.suggestion ? ` ${stopped.suggestion}` : ""}
|
|
@@ -56242,6 +58847,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56242
58847
|
}
|
|
56243
58848
|
if (last !== undefined)
|
|
56244
58849
|
await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
|
|
58850
|
+
const rebound = rebindPhase === undefined ? undefined : await runRebindPhase(ctx, rebindPhase, results);
|
|
56245
58851
|
return finish(ctx, {
|
|
56246
58852
|
file: pairsFile,
|
|
56247
58853
|
rows: rows.length,
|
|
@@ -56249,10 +58855,27 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56249
58855
|
destinations,
|
|
56250
58856
|
exit: worst,
|
|
56251
58857
|
controlledBy,
|
|
56252
|
-
roles: checked
|
|
58858
|
+
roles: checked,
|
|
58859
|
+
...rebound !== undefined ? { rebound } : {}
|
|
56253
58860
|
});
|
|
56254
58861
|
});
|
|
56255
58862
|
}
|
|
58863
|
+
async function runRebindPhase(ctx, phase, results) {
|
|
58864
|
+
const { wallets: wallets2, notRebindable } = splitRebindable(results);
|
|
58865
|
+
const run = wallets2.length > 0 ? await rebindPromoted(ctx, phase.toKey.deviceToken, phase.toKey.target.keyPrefix, wallets2) : undefined;
|
|
58866
|
+
const input = {
|
|
58867
|
+
target: phase.toKey.target,
|
|
58868
|
+
callingKeyPrefix: phase.callingKeyPrefix,
|
|
58869
|
+
wallets: wallets2,
|
|
58870
|
+
run,
|
|
58871
|
+
notRebindable
|
|
58872
|
+
};
|
|
58873
|
+
const report = renderRebindReport(input);
|
|
58874
|
+
if (report.length > 0)
|
|
58875
|
+
ctx.deps.stderr.write(`${report}
|
|
58876
|
+
`);
|
|
58877
|
+
return { phase, run, input };
|
|
58878
|
+
}
|
|
56256
58879
|
async function verifySubjectSecret(vault, subject) {
|
|
56257
58880
|
const secret = await decryptKey(vault, subject.id);
|
|
56258
58881
|
try {
|
|
@@ -56375,7 +58998,7 @@ function renderBatchTable(planned, opts) {
|
|
|
56375
58998
|
const authority = item.kind === "skip" ? "" : opts.roles === undefined ? "?" : authorityCell(address, opts.roles);
|
|
56376
58999
|
const cells = [String(item.row.line), state, item.subject.label, address, destination, authority];
|
|
56377
59000
|
const lamports = opts.lamports?.get(address);
|
|
56378
|
-
cells.push(lamports === undefined ? "unread" :
|
|
59001
|
+
cells.push(lamports === undefined ? "unread" : formatSol4(lamports));
|
|
56379
59002
|
if (opts.tokenCounts !== undefined)
|
|
56380
59003
|
cells.push(String(opts.tokenCounts.get(address) ?? 0));
|
|
56381
59004
|
if (opts.hasValueUsd)
|
|
@@ -56414,7 +59037,7 @@ function renderFooter(opts) {
|
|
|
56414
59037
|
else
|
|
56415
59038
|
total += value;
|
|
56416
59039
|
}
|
|
56417
|
-
lines.push(`value_usd totals your file's own column; this CLI reads no price. Total for the ${opts.acting.length} rows this run will act on: ${
|
|
59040
|
+
lines.push(`value_usd totals your file's own column; this CLI reads no price. Total for the ${opts.acting.length} rows this run will act on: ${formatUsd2(total)}${excluded > 0 ? ` (${excluded} row${excluded === 1 ? "" : "s"} excluded: the cell did not parse)` : ""}`);
|
|
56418
59041
|
}
|
|
56419
59042
|
const accepted = opts.acting.filter((item) => item.kind === "promote" && item.acceptedUnknown);
|
|
56420
59043
|
if (accepted.length > 0) {
|
|
@@ -56439,10 +59062,12 @@ function finish(ctx, opts) {
|
|
|
56439
59062
|
const resumed = opts.keys.filter((r) => r.state === "resumed").length;
|
|
56440
59063
|
const skipped = opts.keys.filter((r) => r.state === "skipped").length;
|
|
56441
59064
|
const unverified = opts.keys.filter((r) => r.state !== "skipped" && r.remoteAuthority !== "verified-active");
|
|
56442
|
-
const
|
|
59065
|
+
const rebindFailed = opts.rebound?.run !== undefined && !opts.rebound.run.ok;
|
|
59066
|
+
const exit = rebindFailed ? Math.max(opts.exit, 1) : opts.exit;
|
|
59067
|
+
const complete = exit === 0;
|
|
56443
59068
|
if (ctx.json) {
|
|
56444
59069
|
writeJson(ctx.deps, {
|
|
56445
|
-
ok:
|
|
59070
|
+
ok: !rebindFailed,
|
|
56446
59071
|
complete,
|
|
56447
59072
|
file: opts.file,
|
|
56448
59073
|
rows: opts.rows,
|
|
@@ -56450,11 +59075,12 @@ function finish(ctx, opts) {
|
|
|
56450
59075
|
resumed,
|
|
56451
59076
|
skipped,
|
|
56452
59077
|
destinations: opts.destinations.map(({ label, address, keys }) => ({ label, address, keys })),
|
|
56453
|
-
keys: opts.keys,
|
|
59078
|
+
keys: opts.rebound === undefined ? opts.keys : keysWithRebind(opts.keys, opts.rebound.phase, opts.rebound.run),
|
|
56454
59079
|
controlledBy: controlledByJson(opts.controlledBy),
|
|
56455
|
-
authorities: authoritiesJson(opts.roles)
|
|
59080
|
+
authorities: authoritiesJson(opts.roles),
|
|
59081
|
+
...opts.rebound !== undefined ? rebindJson(opts.rebound.input) : {}
|
|
56456
59082
|
});
|
|
56457
|
-
return
|
|
59083
|
+
return exit;
|
|
56458
59084
|
}
|
|
56459
59085
|
const parts = [];
|
|
56460
59086
|
if (promoted > 0)
|
|
@@ -56465,11 +59091,19 @@ function finish(ctx, opts) {
|
|
|
56465
59091
|
parts.push(`${skipped} already promoted (skipped)`);
|
|
56466
59092
|
ctx.deps.stdout.write(`${parts.join("; ")}.
|
|
56467
59093
|
`);
|
|
56468
|
-
if (
|
|
59094
|
+
if (unverified.length > 0) {
|
|
56469
59095
|
ctx.deps.stdout.write(`${unverified.length} import${unverified.length === 1 ? "" : "s"} did not report verified-active: ${unverified.map((r) => r.address).join(", ")}
|
|
56470
59096
|
`);
|
|
56471
59097
|
}
|
|
56472
|
-
|
|
59098
|
+
if (opts.rebound !== undefined) {
|
|
59099
|
+
const { input, run } = opts.rebound;
|
|
59100
|
+
const moved = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "rebound").length;
|
|
59101
|
+
const already = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "unchanged").length;
|
|
59102
|
+
const pending = input.wallets.length - moved - already;
|
|
59103
|
+
ctx.deps.stdout.write(`${moved + already} of ${input.wallets.length + input.notRebindable.length} wallets bound to ${input.target.keyPrefix}${pending > 0 ? `; ${pending} still on ${input.callingKeyPrefix}` : ""}${input.notRebindable.length > 0 ? `; ${input.notRebindable.length} not yet rebindable` : ""}.
|
|
59104
|
+
`);
|
|
59105
|
+
}
|
|
59106
|
+
return exit;
|
|
56473
59107
|
}
|
|
56474
59108
|
|
|
56475
59109
|
// src/commands/vault-rename.ts
|
|
@@ -57555,8 +60189,78 @@ function describeEnvelope(envelope, facts) {
|
|
|
57555
60189
|
|
|
57556
60190
|
// src/commands/vault-transfer.ts
|
|
57557
60191
|
init_args();
|
|
60192
|
+
init_deps();
|
|
57558
60193
|
init_promote_support();
|
|
57559
60194
|
init_store();
|
|
60195
|
+
|
|
60196
|
+
// src/vault/tee-transfer.ts
|
|
60197
|
+
init_errors();
|
|
60198
|
+
function assertNoPendingSweep(entry) {
|
|
60199
|
+
const pending = entry.tee?.sweepPending?.length ?? 0;
|
|
60200
|
+
if (pending === 0)
|
|
60201
|
+
return;
|
|
60202
|
+
throw new VaultError("TRANSFER_SWEEP_PENDING", `${entry.label} (${entry.address}) has ${pending} pending sweep transaction(s); a transfer would interleave with that sweep.`, { suggestion: `Finish the sweep first: candle tee sweep ${entry.address} --rpc-url <url>` });
|
|
60203
|
+
}
|
|
60204
|
+
async function readTeeServerState(ctx, entry, apiKey) {
|
|
60205
|
+
if (entry.linkedWalletId === undefined) {
|
|
60206
|
+
return { read: false, reason: "this wallet has no linked wallet id recorded" };
|
|
60207
|
+
}
|
|
60208
|
+
if (apiKey === undefined)
|
|
60209
|
+
return { read: false, reason: "no API key for this profile" };
|
|
60210
|
+
try {
|
|
60211
|
+
const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/lifecycle`, {
|
|
60212
|
+
auth: "key",
|
|
60213
|
+
credentials: { apiKey },
|
|
60214
|
+
apiUrl: ctx.apiUrl,
|
|
60215
|
+
fetch: ctx.deps.fetch,
|
|
60216
|
+
env: ctx.deps.env
|
|
60217
|
+
});
|
|
60218
|
+
if (!result.ok)
|
|
60219
|
+
return { read: false, reason: result.message };
|
|
60220
|
+
const state = result.body.state;
|
|
60221
|
+
if (typeof state !== "string")
|
|
60222
|
+
return { read: false, reason: "the lifecycle response carried no state" };
|
|
60223
|
+
return { read: true, state };
|
|
60224
|
+
} catch (error) {
|
|
60225
|
+
return { read: false, reason: error instanceof Error ? error.message : String(error) };
|
|
60226
|
+
}
|
|
60227
|
+
}
|
|
60228
|
+
function serverStateNotice(state) {
|
|
60229
|
+
if (!state.read) {
|
|
60230
|
+
return `Could not read this wallet's Candle state (${state.reason}); proceeding. An agent may be trading it.`;
|
|
60231
|
+
}
|
|
60232
|
+
if (state.state === "enabled") {
|
|
60233
|
+
return "Warning: this wallet is enabled, so an agent may be trading it now; a trade may fail if this transfer leaves too little.";
|
|
60234
|
+
}
|
|
60235
|
+
return;
|
|
60236
|
+
}
|
|
60237
|
+
async function reportTransferActivity(ctx, apiKey, signature) {
|
|
60238
|
+
const unseen = "Candle's history will not show this transfer.";
|
|
60239
|
+
if (apiKey === undefined)
|
|
60240
|
+
return { outcome: "no-api-key", line: `No API key for this profile, so ${unseen}` };
|
|
60241
|
+
try {
|
|
60242
|
+
const result = await apiRequest("/api/v1/activity/report", {
|
|
60243
|
+
method: "POST",
|
|
60244
|
+
body: { chain: "solana", signature },
|
|
60245
|
+
auth: "key",
|
|
60246
|
+
credentials: { apiKey },
|
|
60247
|
+
apiUrl: ctx.apiUrl,
|
|
60248
|
+
fetch: ctx.deps.fetch,
|
|
60249
|
+
env: ctx.deps.env
|
|
60250
|
+
});
|
|
60251
|
+
if (result.ok)
|
|
60252
|
+
return { outcome: "reported", line: "Reported to Candle's history." };
|
|
60253
|
+
if (result.code === "SCOPE_MISSING") {
|
|
60254
|
+
return { outcome: "scope-missing", line: `This profile's API key lacks activity:write, so ${unseen}` };
|
|
60255
|
+
}
|
|
60256
|
+
return { outcome: "failed", line: `Could not report this transfer (${result.message}); ${unseen}` };
|
|
60257
|
+
} catch (error) {
|
|
60258
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
60259
|
+
return { outcome: "failed", line: `Could not report this transfer (${reason}); ${unseen}` };
|
|
60260
|
+
}
|
|
60261
|
+
}
|
|
60262
|
+
|
|
60263
|
+
// src/commands/vault-transfer.ts
|
|
57560
60264
|
init_vault_support();
|
|
57561
60265
|
async function vaultTransfer(args, ctx) {
|
|
57562
60266
|
const parsed = parseArgs(args, {
|
|
@@ -57597,11 +60301,17 @@ async function vaultTransfer(args, ctx) {
|
|
|
57597
60301
|
promptText: "Vault passphrase (input hidden): "
|
|
57598
60302
|
});
|
|
57599
60303
|
const vault = hold(opened.vault);
|
|
57600
|
-
const fromEntry =
|
|
60304
|
+
const fromEntry = findTransferSource(vault.index, fromLabel);
|
|
57601
60305
|
if (fromEntry === undefined) {
|
|
57602
|
-
return usage(ctx, `No vault key matches --from ${fromLabel}.`);
|
|
60306
|
+
return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
|
|
57603
60307
|
}
|
|
57604
|
-
|
|
60308
|
+
assertTransferSigner(fromEntry);
|
|
60309
|
+
const promoted = fromEntry.role === "tee-wallet";
|
|
60310
|
+
if (promoted)
|
|
60311
|
+
assertNoPendingSweep(fromEntry);
|
|
60312
|
+
const note = (line) => (ctx.json ? ctx.deps.stderr : ctx.deps.stdout).write(`${line}
|
|
60313
|
+
`);
|
|
60314
|
+
const apiKey = promoted ? await resolveApiKey(ctx.deps, ctx.profile) : undefined;
|
|
57605
60315
|
const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], rpcUrl2, ctx);
|
|
57606
60316
|
if (reconciled !== null)
|
|
57607
60317
|
return reconciled;
|
|
@@ -57615,11 +60325,25 @@ async function vaultTransfer(args, ctx) {
|
|
|
57615
60325
|
});
|
|
57616
60326
|
const feeQuote = await quoteTransferFee(rpcUrl2, ctx.deps.fetch, plan.from, plan.instructions);
|
|
57617
60327
|
displayTransferPlan(ctx, plan, feeQuote);
|
|
60328
|
+
if (promoted) {
|
|
60329
|
+
const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
|
|
60330
|
+
if (notice !== undefined)
|
|
60331
|
+
note(notice);
|
|
60332
|
+
}
|
|
57618
60333
|
await confirmLastSix(ctx, to, "the destination");
|
|
57619
60334
|
await opened.confirm(`sign transfer of ${plan.amount} ${plan.asset} to ${to}`);
|
|
57620
60335
|
const secret = await decryptKey(vault, fromEntry.id);
|
|
57621
60336
|
try {
|
|
57622
60337
|
const result = await signAndBroadcastTransfer({ ctx, rpcUrl: rpcUrl2, secret64: secret, plan });
|
|
60338
|
+
let activityReport;
|
|
60339
|
+
if (promoted && result.finalized) {
|
|
60340
|
+
const report = await reportTransferActivity(ctx, apiKey, result.signature);
|
|
60341
|
+
activityReport = report.outcome;
|
|
60342
|
+
note(report.line);
|
|
60343
|
+
} else if (promoted) {
|
|
60344
|
+
activityReport = "not-finalized";
|
|
60345
|
+
note("Not reported to Candle's history: finality is not yet confirmed.");
|
|
60346
|
+
}
|
|
57623
60347
|
if (ctx.json) {
|
|
57624
60348
|
writeJson(ctx.deps, {
|
|
57625
60349
|
ok: result.finalized,
|
|
@@ -57629,7 +60353,8 @@ async function vaultTransfer(args, ctx) {
|
|
|
57629
60353
|
amount: plan.amount,
|
|
57630
60354
|
asset: plan.asset,
|
|
57631
60355
|
amountRaw: plan.amountRaw.toString(),
|
|
57632
|
-
finalized: result.finalized
|
|
60356
|
+
finalized: result.finalized,
|
|
60357
|
+
...activityReport !== undefined ? { activityReport } : {}
|
|
57633
60358
|
});
|
|
57634
60359
|
} else {
|
|
57635
60360
|
ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
|
|
@@ -57648,7 +60373,7 @@ init_args();
|
|
|
57648
60373
|
init_release();
|
|
57649
60374
|
import { dirname as dirname10, join as join11 } from "node:path";
|
|
57650
60375
|
init_render();
|
|
57651
|
-
var
|
|
60376
|
+
var USAGE3 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
|
|
57652
60377
|
async function resolveIdentity(deps, bundlePath, flag) {
|
|
57653
60378
|
if (flag)
|
|
57654
60379
|
return { kind: "ok", uri: flag, provenance: "identity from --identity" };
|
|
@@ -57680,19 +60405,19 @@ async function verify(args, ctx) {
|
|
|
57680
60405
|
});
|
|
57681
60406
|
if ("error" in parsed) {
|
|
57682
60407
|
writeUsageFailure(deps, `${parsed.error}
|
|
57683
|
-
${
|
|
60408
|
+
${USAGE3}`, json);
|
|
57684
60409
|
return 2;
|
|
57685
60410
|
}
|
|
57686
60411
|
const file = parsed.positionals[0];
|
|
57687
60412
|
if (parsed.positionals.length !== 1 || file === undefined) {
|
|
57688
60413
|
writeUsageFailure(deps, `verify takes exactly one file.
|
|
57689
|
-
${
|
|
60414
|
+
${USAGE3}`, json);
|
|
57690
60415
|
return 2;
|
|
57691
60416
|
}
|
|
57692
60417
|
const bundlePath = parsed.values["--bundle"];
|
|
57693
60418
|
if (!bundlePath) {
|
|
57694
60419
|
writeUsageFailure(deps, `--bundle is required.
|
|
57695
|
-
${
|
|
60420
|
+
${USAGE3}`, json);
|
|
57696
60421
|
return 2;
|
|
57697
60422
|
}
|
|
57698
60423
|
const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
|
|
@@ -57706,7 +60431,7 @@ ${USAGE2}`, json);
|
|
|
57706
60431
|
}
|
|
57707
60432
|
if (resolved.kind === "absent") {
|
|
57708
60433
|
writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
|
|
57709
|
-
${
|
|
60434
|
+
${USAGE3}`, json);
|
|
57710
60435
|
return 2;
|
|
57711
60436
|
}
|
|
57712
60437
|
const identity = resolved.uri;
|
|
@@ -57747,6 +60472,177 @@ function messageOf2(error) {
|
|
|
57747
60472
|
return error instanceof Error ? error.message : String(error);
|
|
57748
60473
|
}
|
|
57749
60474
|
|
|
60475
|
+
// src/commands/wallets-trust.ts
|
|
60476
|
+
init_args();
|
|
60477
|
+
init_deps();
|
|
60478
|
+
init_profiles();
|
|
60479
|
+
init_render();
|
|
60480
|
+
init_promote_support();
|
|
60481
|
+
var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
|
|
60482
|
+
var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
|
|
60483
|
+
var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
|
|
60484
|
+
var DEVICE_TOKEN_REQUIRED2 = {
|
|
60485
|
+
code: "DEVICE_TOKEN_REQUIRED",
|
|
60486
|
+
message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
|
|
60487
|
+
suggestion: "Run: candle auth login"
|
|
60488
|
+
};
|
|
60489
|
+
function sessionLinkedLine(rows) {
|
|
60490
|
+
const n = rows.length;
|
|
60491
|
+
const it = n === 1 ? "it" : "them";
|
|
60492
|
+
const names = rows.map((row) => row.label ?? row.address).join(", ");
|
|
60493
|
+
return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
|
|
60494
|
+
}
|
|
60495
|
+
function nothingToChangeLine(shown, trusted) {
|
|
60496
|
+
const session = shown.sessionLinked ?? [];
|
|
60497
|
+
const unchanged = shown.unchanged;
|
|
60498
|
+
if (session.length > 0 && unchanged.length === 0) {
|
|
60499
|
+
const one = session.length === 1;
|
|
60500
|
+
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"}.
|
|
60501
|
+
`;
|
|
60502
|
+
}
|
|
60503
|
+
const bits = [];
|
|
60504
|
+
if (session.length > 0)
|
|
60505
|
+
bits.push(sessionLinkedLine(session).replace(/\.$/, ""));
|
|
60506
|
+
if (unchanged.length > 0) {
|
|
60507
|
+
bits.push(unchanged.length === 1 ? `that wallet is already ${trusted ? "trusted" : "untrusted"}` : `all ${unchanged.length} wallets are already ${trusted ? "trusted" : "untrusted"}`);
|
|
60508
|
+
}
|
|
60509
|
+
if (bits.length === 0)
|
|
60510
|
+
return `Nothing to change.
|
|
60511
|
+
`;
|
|
60512
|
+
return `Nothing to change: ${bits.join("; ")}.
|
|
60513
|
+
`;
|
|
60514
|
+
}
|
|
60515
|
+
function writeTrustFailure(ctx, result) {
|
|
60516
|
+
const { deps, apiUrl, json } = ctx;
|
|
60517
|
+
let code = result.code;
|
|
60518
|
+
let message = result.message;
|
|
60519
|
+
let suggestion;
|
|
60520
|
+
if (result.status === 404) {
|
|
60521
|
+
code = "TRUST_UNSUPPORTED";
|
|
60522
|
+
message = "This Candle API does not support trusting wallets yet; nothing changed.";
|
|
60523
|
+
} else {
|
|
60524
|
+
const error = result.raw?.error;
|
|
60525
|
+
if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
|
|
60526
|
+
const ids = error.matches.map((m) => String(m.id)).join(", ");
|
|
60527
|
+
suggestion = `Name one of them by id or address: ${ids}`;
|
|
60528
|
+
} else if (error?.reason === "not_found") {
|
|
60529
|
+
suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
|
|
60530
|
+
}
|
|
60531
|
+
}
|
|
60532
|
+
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
|
|
60533
|
+
if (json) {
|
|
60534
|
+
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
60535
|
+
`);
|
|
60536
|
+
} else {
|
|
60537
|
+
deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
60538
|
+
`);
|
|
60539
|
+
}
|
|
60540
|
+
return 1;
|
|
60541
|
+
}
|
|
60542
|
+
function trustTable(rows) {
|
|
60543
|
+
return renderTable(["line", "label", "wallet", "address"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address]));
|
|
60544
|
+
}
|
|
60545
|
+
async function setTrust(args, ctx, trusted) {
|
|
60546
|
+
const { deps, apiUrl, json } = ctx;
|
|
60547
|
+
const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
|
|
60548
|
+
const command = trusted ? "wallets trust" : "wallets untrust";
|
|
60549
|
+
const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
|
|
60550
|
+
if ("error" in parsed) {
|
|
60551
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
60552
|
+
return 2;
|
|
60553
|
+
}
|
|
60554
|
+
if (parsed.positionals.length === 0) {
|
|
60555
|
+
writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
|
|
60556
|
+
return 2;
|
|
60557
|
+
}
|
|
60558
|
+
const skipConfirm = !trusted && parsed.booleans.has("--yes");
|
|
60559
|
+
await printIdentity(ctx);
|
|
60560
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
60561
|
+
if (!deviceToken) {
|
|
60562
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
|
|
60563
|
+
return 1;
|
|
60564
|
+
}
|
|
60565
|
+
if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
|
|
60566
|
+
writeLocalFailure(deps, {
|
|
60567
|
+
code: "TRUST_REQUIRES_TTY",
|
|
60568
|
+
message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
|
|
60569
|
+
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."
|
|
60570
|
+
}, json);
|
|
60571
|
+
return 1;
|
|
60572
|
+
}
|
|
60573
|
+
const call = (body) => apiRequest(TRUST_PATH, {
|
|
60574
|
+
method: "POST",
|
|
60575
|
+
body,
|
|
60576
|
+
auth: "device",
|
|
60577
|
+
credentials: { deviceToken },
|
|
60578
|
+
apiUrl,
|
|
60579
|
+
fetch: deps.fetch,
|
|
60580
|
+
env: deps.env
|
|
60581
|
+
});
|
|
60582
|
+
const preview = await call({ dryRun: true, wallets: parsed.positionals, trusted });
|
|
60583
|
+
if (!preview.ok)
|
|
60584
|
+
return writeTrustFailure(ctx, preview);
|
|
60585
|
+
const shown = preview.body;
|
|
60586
|
+
const changing = shown.changed;
|
|
60587
|
+
if (changing.length === 0) {
|
|
60588
|
+
if (json) {
|
|
60589
|
+
deps.stdout.write(`${JSON.stringify({ ...shown, command })}
|
|
60590
|
+
`);
|
|
60591
|
+
} else {
|
|
60592
|
+
deps.stdout.write(nothingToChangeLine(shown, trusted));
|
|
60593
|
+
}
|
|
60594
|
+
return 0;
|
|
60595
|
+
}
|
|
60596
|
+
const n = changing.length;
|
|
60597
|
+
const screen = [
|
|
60598
|
+
trustTable(changing),
|
|
60599
|
+
...shown.unchanged.length > 0 ? [
|
|
60600
|
+
`${shown.unchanged.length} already ${trusted ? "trusted" : "untrusted"}: ${shown.unchanged.map((row) => row.label ?? row.address).join(", ")}`
|
|
60601
|
+
] : [],
|
|
60602
|
+
...(shown.sessionLinked?.length ?? 0) > 0 ? [sessionLinkedLine(shown.sessionLinked ?? [])] : [],
|
|
60603
|
+
"",
|
|
60604
|
+
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.`,
|
|
60605
|
+
""
|
|
60606
|
+
];
|
|
60607
|
+
deps.stderr.write(`${screen.join(`
|
|
60608
|
+
`)}
|
|
60609
|
+
`);
|
|
60610
|
+
if (!skipConfirm) {
|
|
60611
|
+
const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${trusted ? "trust" : "untrust"} ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
|
|
60612
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
60613
|
+
writeLocalFailure(deps, {
|
|
60614
|
+
code: "TRUST_NOT_ACKNOWLEDGED",
|
|
60615
|
+
message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
|
|
60616
|
+
suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
|
|
60617
|
+
}, json);
|
|
60618
|
+
return 1;
|
|
60619
|
+
}
|
|
60620
|
+
}
|
|
60621
|
+
const committed = await call({ walletIds: changing.map((row) => row.id), trusted });
|
|
60622
|
+
if (!committed.ok)
|
|
60623
|
+
return writeTrustFailure(ctx, committed);
|
|
60624
|
+
const result = committed.body;
|
|
60625
|
+
if (json) {
|
|
60626
|
+
deps.stdout.write(`${JSON.stringify({ ...result, command })}
|
|
60627
|
+
`);
|
|
60628
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
60629
|
+
}
|
|
60630
|
+
const done = result.changed.length;
|
|
60631
|
+
deps.stdout.write(`${trusted ? "Trusted" : "Untrusted"} ${done} wallet${done === 1 ? "" : "s"}.
|
|
60632
|
+
`);
|
|
60633
|
+
for (const skip of result.skipped) {
|
|
60634
|
+
deps.stderr.write(`Skipped ${skip.walletId}: ${skip.reason === "revoked" ? "revoked" : "not on this account"}.
|
|
60635
|
+
`);
|
|
60636
|
+
}
|
|
60637
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
60638
|
+
}
|
|
60639
|
+
function walletsTrust(args, ctx) {
|
|
60640
|
+
return setTrust(args, ctx, true);
|
|
60641
|
+
}
|
|
60642
|
+
function walletsUntrust(args, ctx) {
|
|
60643
|
+
return setTrust(args, ctx, false);
|
|
60644
|
+
}
|
|
60645
|
+
|
|
57750
60646
|
// src/config.ts
|
|
57751
60647
|
import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
57752
60648
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -58062,11 +60958,15 @@ function extractGlobalFlags(argv) {
|
|
|
58062
60958
|
}
|
|
58063
60959
|
var COMMANDS = {
|
|
58064
60960
|
swap: { bare: swap, subcommands: { status: swapStatus } },
|
|
60961
|
+
transfer: { bare: transfer },
|
|
58065
60962
|
launch: { bare: launch },
|
|
60963
|
+
pnl: { bare: pnl },
|
|
60964
|
+
portfolio: { bare: portfolio },
|
|
60965
|
+
lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
|
|
58066
60966
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
58067
60967
|
keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
|
|
58068
60968
|
wallets: {
|
|
58069
|
-
subcommands: { import: walletsImport, revoke: walletsRevoke },
|
|
60969
|
+
subcommands: { import: walletsImport, revoke: walletsRevoke, trust: walletsTrust, untrust: walletsUntrust },
|
|
58070
60970
|
bare: wallets
|
|
58071
60971
|
},
|
|
58072
60972
|
vault: {
|