@candledottv/cli 0.11.6 → 0.11.7
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 +12 -1
- package/dist/index.js +2428 -184
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -332,6 +332,9 @@ var init_agent_key_access = __esm(() => {
|
|
|
332
332
|
function formatScopesForSummary(scopes) {
|
|
333
333
|
return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
|
|
334
334
|
}
|
|
335
|
+
function terminalText(value) {
|
|
336
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
337
|
+
}
|
|
335
338
|
function renderTable(headers, rows) {
|
|
336
339
|
const widths = headers.map((header, col) => Math.max(header.length, ...rows.map((row) => (row[col] ?? "").length)));
|
|
337
340
|
const line = (cells) => cells.map((cell, col) => col === cells.length - 1 ? cell ?? "" : (cell ?? "").padEnd(widths[col] ?? 0)).join(" ");
|
|
@@ -3140,7 +3143,8 @@ var init_errors = __esm(() => {
|
|
|
3140
3143
|
"WALLET_LIMIT_REACHED",
|
|
3141
3144
|
"TIER_REQUIRED",
|
|
3142
3145
|
"LINKED_WALLET_ROOM_UNREADABLE",
|
|
3143
|
-
"PROMOTE_ACCOUNT_UNRESOLVED"
|
|
3146
|
+
"PROMOTE_ACCOUNT_UNRESOLVED",
|
|
3147
|
+
"TRANSFER_SWEEP_PENDING"
|
|
3144
3148
|
];
|
|
3145
3149
|
VaultError = class VaultError extends Error {
|
|
3146
3150
|
code;
|
|
@@ -8344,6 +8348,23 @@ function retryAfterMs(header, now) {
|
|
|
8344
8348
|
return;
|
|
8345
8349
|
return Math.max(0, at - now);
|
|
8346
8350
|
}
|
|
8351
|
+
function readProgramAccounts(method, entries, sliced) {
|
|
8352
|
+
const pubkeys = entries.map((entry) => {
|
|
8353
|
+
const pubkey = entry?.pubkey;
|
|
8354
|
+
if (typeof pubkey !== "string")
|
|
8355
|
+
throw new SolanaRpcError(`RPC ${method} answered an account without a pubkey`);
|
|
8356
|
+
return pubkey;
|
|
8357
|
+
});
|
|
8358
|
+
if (!sliced)
|
|
8359
|
+
return pubkeys;
|
|
8360
|
+
return entries.map((entry, i) => {
|
|
8361
|
+
const data = entry.account?.data;
|
|
8362
|
+
const encoded = Array.isArray(data) && data[1] === "base64" ? data[0] : undefined;
|
|
8363
|
+
if (typeof encoded !== "string")
|
|
8364
|
+
throw new SolanaRpcError(`RPC ${method} answered an account without base64 data`);
|
|
8365
|
+
return { pubkey: pubkeys[i], data: new Uint8Array(Buffer.from(encoded, "base64")) };
|
|
8366
|
+
});
|
|
8367
|
+
}
|
|
8347
8368
|
function rawAccountView(value, address) {
|
|
8348
8369
|
if (!value)
|
|
8349
8370
|
return null;
|
|
@@ -8488,28 +8509,31 @@ function createSolanaRpc(url, fetchFn) {
|
|
|
8488
8509
|
const r = await call("getSignaturesForAddress", [address, { limit: 1 }]);
|
|
8489
8510
|
return Array.isArray(r) && r.length > 0;
|
|
8490
8511
|
},
|
|
8491
|
-
async isBlockhashValid(blockhash) {
|
|
8492
|
-
const r = await call("isBlockhashValid", [blockhash, { commitment
|
|
8512
|
+
async isBlockhashValid(blockhash, commitment = "finalized") {
|
|
8513
|
+
const r = await call("isBlockhashValid", [blockhash, { commitment }]);
|
|
8493
8514
|
if (typeof r?.value !== "boolean")
|
|
8494
8515
|
throw new Error("isBlockhashValid answered with a non-boolean value");
|
|
8495
8516
|
return r.value;
|
|
8496
8517
|
},
|
|
8497
|
-
async
|
|
8498
|
-
const r = await call("getProgramAccounts", [
|
|
8518
|
+
getProgramAccounts: async (programId, filters, opts) => {
|
|
8519
|
+
const r = await call("getProgramAccounts", [
|
|
8520
|
+
programId,
|
|
8521
|
+
{
|
|
8522
|
+
encoding: "base64",
|
|
8523
|
+
commitment: "finalized",
|
|
8524
|
+
dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
|
|
8525
|
+
filters
|
|
8526
|
+
}
|
|
8527
|
+
], opts?.signal);
|
|
8499
8528
|
if (!Array.isArray(r))
|
|
8500
8529
|
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
|
-
});
|
|
8530
|
+
return readProgramAccounts("getProgramAccounts", r, opts?.dataSlice !== undefined);
|
|
8507
8531
|
},
|
|
8508
|
-
async
|
|
8532
|
+
getProgramAccountsV2: async (programId, filters, opts) => {
|
|
8509
8533
|
const config = {
|
|
8510
8534
|
encoding: "base64",
|
|
8511
8535
|
commitment: "finalized",
|
|
8512
|
-
dataSlice: { offset: 0, length: 0 },
|
|
8536
|
+
dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
|
|
8513
8537
|
filters,
|
|
8514
8538
|
limit: opts?.limit ?? PROGRAM_ACCOUNTS_V2_LIMIT
|
|
8515
8539
|
};
|
|
@@ -8520,19 +8544,17 @@ function createSolanaRpc(url, fetchFn) {
|
|
|
8520
8544
|
throw new SolanaRpcError("RPC getProgramAccountsV2 answered without an account list");
|
|
8521
8545
|
}
|
|
8522
8546
|
const page = r;
|
|
8523
|
-
const
|
|
8524
|
-
|
|
8525
|
-
throw new SolanaRpcError("RPC getProgramAccountsV2 answered an account without a pubkey");
|
|
8526
|
-
}
|
|
8527
|
-
return entry.pubkey;
|
|
8528
|
-
});
|
|
8547
|
+
const sliced = opts?.dataSlice !== undefined;
|
|
8548
|
+
const accounts = readProgramAccounts("getProgramAccountsV2", page.accounts, sliced);
|
|
8529
8549
|
const cursor = page.paginationKey;
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8550
|
+
let paginationKey = null;
|
|
8551
|
+
if (cursor !== null && cursor !== undefined) {
|
|
8552
|
+
if (typeof cursor !== "string" || cursor.length === 0) {
|
|
8553
|
+
throw new SolanaRpcError("RPC getProgramAccountsV2 answered a paginationKey that is not a string");
|
|
8554
|
+
}
|
|
8555
|
+
paginationKey = cursor;
|
|
8534
8556
|
}
|
|
8535
|
-
return {
|
|
8557
|
+
return sliced ? { accounts, paginationKey } : { pubkeys: accounts, paginationKey };
|
|
8536
8558
|
}
|
|
8537
8559
|
};
|
|
8538
8560
|
}
|
|
@@ -8837,13 +8859,13 @@ var init_keys = __esm(() => {
|
|
|
8837
8859
|
|
|
8838
8860
|
// src/vault/signer-roles.ts
|
|
8839
8861
|
function tokenMintFilters(key) {
|
|
8840
|
-
return [{ dataSize: 82 }, memcmp(
|
|
8862
|
+
return [{ dataSize: 82 }, memcmp(4, key)];
|
|
8841
8863
|
}
|
|
8842
8864
|
function tokenFreezeFilters(key) {
|
|
8843
8865
|
return [{ dataSize: 82 }, memcmp(46, concat2(COPTION_SOME_U32, key))];
|
|
8844
8866
|
}
|
|
8845
8867
|
function token2022MintFilters(key) {
|
|
8846
|
-
const authority = memcmp(
|
|
8868
|
+
const authority = memcmp(4, key);
|
|
8847
8869
|
return [
|
|
8848
8870
|
[{ dataSize: 82 }, authority],
|
|
8849
8871
|
[memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
|
|
@@ -8856,6 +8878,9 @@ function token2022FreezeFilters(key) {
|
|
|
8856
8878
|
[memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
|
|
8857
8879
|
];
|
|
8858
8880
|
}
|
|
8881
|
+
function keepSetAuthority(hits) {
|
|
8882
|
+
return hits.filter((hit) => hit.data.length === COPTION_SOME_U32.length && hit.data.every((byte, i) => byte === COPTION_SOME_U32[i])).map((hit) => hit.pubkey);
|
|
8883
|
+
}
|
|
8859
8884
|
function programUpgradeFilters(key) {
|
|
8860
8885
|
return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat2(COPTION_SOME_U8, key))];
|
|
8861
8886
|
}
|
|
@@ -8950,19 +8975,31 @@ async function readSignerRoles(rpc, addresses, opts) {
|
|
|
8950
8975
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(Object.assign(new Error("timed out"), { name: "TimeoutError" })), timeoutMs) : undefined;
|
|
8951
8976
|
try {
|
|
8952
8977
|
requests += 1;
|
|
8978
|
+
const tagSlice = task.group.tagSlice;
|
|
8953
8979
|
if (task.via === "v2") {
|
|
8954
8980
|
if (rpc.getProgramAccountsV2 === undefined) {
|
|
8955
8981
|
throw new SolanaRpcError("RPC getProgramAccountsV2 failed: -32601 Method not found", {
|
|
8956
8982
|
rpcCode: RPC_METHOD_NOT_FOUND
|
|
8957
8983
|
});
|
|
8958
8984
|
}
|
|
8959
|
-
|
|
8985
|
+
const pageOpts = {
|
|
8960
8986
|
signal: controller.signal,
|
|
8961
8987
|
...task.paginationKey !== undefined ? { paginationKey: task.paginationKey } : {}
|
|
8962
|
-
}
|
|
8988
|
+
};
|
|
8989
|
+
if (tagSlice === undefined)
|
|
8990
|
+
return await rpc.getProgramAccountsV2(task.group.programId, filters, pageOpts);
|
|
8991
|
+
const page = await rpc.getProgramAccountsV2(task.group.programId, filters, { ...pageOpts, dataSlice: tagSlice });
|
|
8992
|
+
return { pubkeys: keepSetAuthority(page.accounts), paginationKey: page.paginationKey };
|
|
8963
8993
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8994
|
+
if (tagSlice === undefined) {
|
|
8995
|
+
const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
|
|
8996
|
+
return { pubkeys, paginationKey: null };
|
|
8997
|
+
}
|
|
8998
|
+
const hits = await rpc.getProgramAccounts(task.group.programId, filters, {
|
|
8999
|
+
signal: controller.signal,
|
|
9000
|
+
dataSlice: tagSlice
|
|
9001
|
+
});
|
|
9002
|
+
return { pubkeys: keepSetAuthority(hits), paginationKey: null };
|
|
8966
9003
|
} finally {
|
|
8967
9004
|
if (timer !== undefined)
|
|
8968
9005
|
clearTimeout(timer);
|
|
@@ -9205,7 +9242,7 @@ var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", S
|
|
|
9205
9242
|
return out;
|
|
9206
9243
|
}, COPTION_SOME_U32, COPTION_SOME_U8, LOADER_PROGRAM_DATA_TAG, LOADER_PROGRAM_TAG, TOKEN_2022_MINT_ACCOUNT_TYPE, memcmp = (offset, bytes) => ({
|
|
9207
9244
|
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");
|
|
9245
|
+
}), 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
9246
|
var init_signer_roles = __esm(() => {
|
|
9210
9247
|
init_esm();
|
|
9211
9248
|
init_solana_lite();
|
|
@@ -9229,6 +9266,7 @@ var init_signer_roles = __esm(() => {
|
|
|
9229
9266
|
LOADER_PROGRAM_DATA_TAG = new Uint8Array([3, 0, 0, 0]);
|
|
9230
9267
|
LOADER_PROGRAM_TAG = new Uint8Array([2, 0, 0, 0]);
|
|
9231
9268
|
TOKEN_2022_MINT_ACCOUNT_TYPE = new Uint8Array([1]);
|
|
9269
|
+
MINT_AUTHORITY_TAG_SLICE = { offset: 0, length: 4 };
|
|
9232
9270
|
ROLE_GROUPS = [
|
|
9233
9271
|
{
|
|
9234
9272
|
id: "token-mint",
|
|
@@ -9236,7 +9274,8 @@ var init_signer_roles = __esm(() => {
|
|
|
9236
9274
|
program: "token",
|
|
9237
9275
|
programId: TOKEN_PROGRAM_ID,
|
|
9238
9276
|
label: "token mint",
|
|
9239
|
-
shapes: (key) => [tokenMintFilters(key)]
|
|
9277
|
+
shapes: (key) => [tokenMintFilters(key)],
|
|
9278
|
+
tagSlice: MINT_AUTHORITY_TAG_SLICE
|
|
9240
9279
|
},
|
|
9241
9280
|
{
|
|
9242
9281
|
id: "token-freeze",
|
|
@@ -9252,7 +9291,8 @@ var init_signer_roles = __esm(() => {
|
|
|
9252
9291
|
program: "token-2022",
|
|
9253
9292
|
programId: TOKEN_2022_PROGRAM_ID,
|
|
9254
9293
|
label: "Token-2022 mint",
|
|
9255
|
-
shapes: token2022MintFilters
|
|
9294
|
+
shapes: token2022MintFilters,
|
|
9295
|
+
tagSlice: MINT_AUTHORITY_TAG_SLICE
|
|
9256
9296
|
},
|
|
9257
9297
|
{
|
|
9258
9298
|
id: "token2022-freeze",
|
|
@@ -9298,6 +9338,7 @@ var init_signer_roles = __esm(() => {
|
|
|
9298
9338
|
// src/vault/promote-support.ts
|
|
9299
9339
|
var exports_promote_support = {};
|
|
9300
9340
|
__export(exports_promote_support, {
|
|
9341
|
+
withToKey: () => withToKey,
|
|
9301
9342
|
subjectPhrase: () => subjectPhrase,
|
|
9302
9343
|
shortAddress: () => shortAddress,
|
|
9303
9344
|
runRoleCheck: () => runRoleCheck,
|
|
@@ -9307,6 +9348,7 @@ __export(exports_promote_support, {
|
|
|
9307
9348
|
promoteSentence: () => promoteSentence,
|
|
9308
9349
|
printPromoteSentence: () => printPromoteSentence,
|
|
9309
9350
|
findVaultRoleEntry: () => findVaultRoleEntry,
|
|
9351
|
+
findTransferSource: () => findTransferSource,
|
|
9310
9352
|
findEntryByLabelOrAddress: () => findEntryByLabelOrAddress,
|
|
9311
9353
|
controlledByJson: () => controlledByJson,
|
|
9312
9354
|
confirmPrompt: () => confirmPrompt,
|
|
@@ -9352,6 +9394,10 @@ function findVaultRoleEntry(index, labelOrAddress) {
|
|
|
9352
9394
|
return byLabel;
|
|
9353
9395
|
return index.entries.find((entry) => entry.address === labelOrAddress);
|
|
9354
9396
|
}
|
|
9397
|
+
function findTransferSource(index, labelOrAddress) {
|
|
9398
|
+
const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
|
|
9399
|
+
return byLabel("vault") ?? byLabel("tee-wallet") ?? index.entries.find((entry) => entry.address === labelOrAddress);
|
|
9400
|
+
}
|
|
9355
9401
|
function findEntryByLabelOrAddress(index, labelOrAddress) {
|
|
9356
9402
|
const byLabel = index.entries.find((entry) => entry.label !== undefined && entry.label === labelOrAddress);
|
|
9357
9403
|
if (byLabel !== undefined)
|
|
@@ -9472,6 +9518,9 @@ async function confirmPromotion(ctx, n) {
|
|
|
9472
9518
|
function shortAddress(address) {
|
|
9473
9519
|
return `${address.slice(0, 6)}…${address.slice(-4)}`;
|
|
9474
9520
|
}
|
|
9521
|
+
function withToKey(controlledBy, toKey) {
|
|
9522
|
+
return { ...controlledBy, toKey };
|
|
9523
|
+
}
|
|
9475
9524
|
function accountUnresolved(reason) {
|
|
9476
9525
|
return new VaultError("PROMOTE_ACCOUNT_UNRESOLVED", `Could not confirm which Candle account this API key acts for (${reason}); nothing was written.`, {
|
|
9477
9526
|
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 +9594,51 @@ function renderControlledBy(controlledBy, n) {
|
|
|
9545
9594
|
const label = cleaned.length > 0 ? `(${cleaned}) ` : "";
|
|
9546
9595
|
const environment = controlledBy.environment ?? "not a Candle host";
|
|
9547
9596
|
const from = controlledBy.apiUrlFrom !== undefined ? `, from ${controlledBy.apiUrlFrom}` : "";
|
|
9597
|
+
const { toKey } = controlledBy;
|
|
9598
|
+
if (toKey === undefined) {
|
|
9599
|
+
return [
|
|
9600
|
+
`${subject} will be controlled by:`,
|
|
9601
|
+
` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
|
|
9602
|
+
` API key ${controlledBy.keyPrefix}… ${label}${source}`,
|
|
9603
|
+
` API ${controlledBy.apiUrl} (${environment}${from})`
|
|
9604
|
+
].join(`
|
|
9605
|
+
`);
|
|
9606
|
+
}
|
|
9607
|
+
const toLabel = toKey.label !== null && labelCell(toKey.label).length > 0 ? `(${labelCell(toKey.label)}) ` : "";
|
|
9548
9608
|
return [
|
|
9549
9609
|
`${subject} will be controlled by:`,
|
|
9550
9610
|
` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
|
|
9551
|
-
` API key ${
|
|
9552
|
-
`
|
|
9611
|
+
` API key ${toKey.keyPrefix}… ${toLabel}--to-key, bound by a rebind after the import`,
|
|
9612
|
+
` imported under ${controlledBy.keyPrefix}… ${label}${source}`,
|
|
9613
|
+
` API ${controlledBy.apiUrl} (${environment}${from})`,
|
|
9614
|
+
...toKey.warnings
|
|
9553
9615
|
].join(`
|
|
9554
9616
|
`);
|
|
9555
9617
|
}
|
|
9556
9618
|
function controlledByJson(controlledBy) {
|
|
9619
|
+
const importedUnder = {
|
|
9620
|
+
keyPrefix: controlledBy.keyPrefix,
|
|
9621
|
+
keyLabel: controlledBy.keyLabel,
|
|
9622
|
+
keySource: controlledBy.keySource
|
|
9623
|
+
};
|
|
9624
|
+
if (controlledBy.toKey === undefined) {
|
|
9625
|
+
return {
|
|
9626
|
+
account: controlledBy.account,
|
|
9627
|
+
username: controlledBy.username,
|
|
9628
|
+
...importedUnder,
|
|
9629
|
+
apiUrl: controlledBy.apiUrl,
|
|
9630
|
+
environment: controlledBy.environment
|
|
9631
|
+
};
|
|
9632
|
+
}
|
|
9557
9633
|
return {
|
|
9558
9634
|
account: controlledBy.account,
|
|
9559
9635
|
username: controlledBy.username,
|
|
9560
|
-
keyPrefix: controlledBy.keyPrefix,
|
|
9561
|
-
keyLabel: controlledBy.
|
|
9562
|
-
keySource:
|
|
9636
|
+
keyPrefix: controlledBy.toKey.keyPrefix,
|
|
9637
|
+
keyLabel: controlledBy.toKey.label,
|
|
9638
|
+
keySource: "--to-key",
|
|
9563
9639
|
apiUrl: controlledBy.apiUrl,
|
|
9564
|
-
environment: controlledBy.environment
|
|
9640
|
+
environment: controlledBy.environment,
|
|
9641
|
+
importedUnder
|
|
9565
9642
|
};
|
|
9566
9643
|
}
|
|
9567
9644
|
async function runRoleCheck(ctx, rpc, addresses) {
|
|
@@ -13625,14 +13702,14 @@ async function request(ctx, key, path, body) {
|
|
|
13625
13702
|
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
13626
13703
|
return result.body;
|
|
13627
13704
|
}
|
|
13628
|
-
async function
|
|
13705
|
+
async function listTradingWallets(ctx, key, scope) {
|
|
13629
13706
|
let cursor;
|
|
13630
13707
|
const rows = [];
|
|
13631
13708
|
const cursors = new Set;
|
|
13632
13709
|
let appId = "";
|
|
13633
13710
|
for (;; ) {
|
|
13634
13711
|
const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
|
|
13635
|
-
if (!response.scopes?.includes(scope))
|
|
13712
|
+
if (scope !== undefined && !response.scopes?.includes(scope))
|
|
13636
13713
|
throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
|
|
13637
13714
|
appId = response.privyAppId ?? "";
|
|
13638
13715
|
if (!Array.isArray(response.page))
|
|
@@ -13675,7 +13752,7 @@ async function completeTradingWallet(ctx, row, appId, scope) {
|
|
|
13675
13752
|
};
|
|
13676
13753
|
}
|
|
13677
13754
|
async function tradingWallet(ctx, key, name, scope) {
|
|
13678
|
-
const { rows, appId } = await
|
|
13755
|
+
const { rows, appId } = await listTradingWallets(ctx, key, scope);
|
|
13679
13756
|
const matches = rows.filter((row) => matchesName(row, name));
|
|
13680
13757
|
if (matches.length !== 1) {
|
|
13681
13758
|
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)}.`);
|
|
@@ -13684,7 +13761,7 @@ async function tradingWallet(ctx, key, name, scope) {
|
|
|
13684
13761
|
}
|
|
13685
13762
|
async function tradingPayer(ctx, key, name) {
|
|
13686
13763
|
const scope = "swap:write";
|
|
13687
|
-
const { rows, appId } = await
|
|
13764
|
+
const { rows, appId } = await listTradingWallets(ctx, key, scope);
|
|
13688
13765
|
const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
|
|
13689
13766
|
const asEmbedded = () => ({ kind: "embedded", address: embedded });
|
|
13690
13767
|
const options = [
|
|
@@ -13808,7 +13885,7 @@ Minimum received: ${safeText(quote.minimumReceived)}
|
|
|
13808
13885
|
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary trade prompt.");
|
|
13809
13886
|
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
13810
13887
|
}
|
|
13811
|
-
async function
|
|
13888
|
+
async function saveOperationSignature(ctx, key, id, kind, transaction) {
|
|
13812
13889
|
const bytes = Buffer.from(transaction, "base64");
|
|
13813
13890
|
let offset = 0;
|
|
13814
13891
|
let count = 0;
|
|
@@ -13825,7 +13902,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
|
|
|
13825
13902
|
const signature = base58.encode(bytes.subarray(offset, offset + 64));
|
|
13826
13903
|
const path = operationPath(ctx, key, id);
|
|
13827
13904
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
13828
|
-
await writeFile4(temporary, JSON.stringify({ id, kind
|
|
13905
|
+
await writeFile4(temporary, JSON.stringify({ id, kind, signature }), { mode: 384 });
|
|
13829
13906
|
const file = await open3(temporary, "r");
|
|
13830
13907
|
try {
|
|
13831
13908
|
await file.sync();
|
|
@@ -13835,7 +13912,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
|
|
|
13835
13912
|
await rename3(temporary, path);
|
|
13836
13913
|
return signature;
|
|
13837
13914
|
}
|
|
13838
|
-
var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, BASES;
|
|
13915
|
+
var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, lpAmountSchema, lpBuildSchema, lpPositionsSchema, lpPoolsSchema, BASES;
|
|
13839
13916
|
var init_trading = __esm(() => {
|
|
13840
13917
|
init_esm();
|
|
13841
13918
|
init_zod();
|
|
@@ -13897,6 +13974,51 @@ var init_trading = __esm(() => {
|
|
|
13897
13974
|
wallets: exports_external.object({ solana: exports_external.object({ address: exports_external.string() }).passthrough().nullable().optional() }).passthrough().optional()
|
|
13898
13975
|
}).passthrough();
|
|
13899
13976
|
operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
|
|
13977
|
+
lpAmountSchema = exports_external.object({ mint: exports_external.string(), raw: exports_external.string().regex(/^\d+$/), decimals: exports_external.number().int() });
|
|
13978
|
+
lpBuildSchema = exports_external.object({
|
|
13979
|
+
build: exports_external.object({
|
|
13980
|
+
action: exports_external.enum(["add", "remove", "claim"]),
|
|
13981
|
+
pool: exports_external.string(),
|
|
13982
|
+
position: exports_external.string(),
|
|
13983
|
+
transaction: exports_external.string().min(1),
|
|
13984
|
+
walletAddress: exports_external.string(),
|
|
13985
|
+
signature: exports_external.string().optional()
|
|
13986
|
+
}).passthrough(),
|
|
13987
|
+
preview: exports_external.object({
|
|
13988
|
+
amounts: exports_external.array(lpAmountSchema).default([]),
|
|
13989
|
+
tokenRisks: risksSchema.default([]),
|
|
13990
|
+
warnings: exports_external.array(exports_external.string()).default([]),
|
|
13991
|
+
candleFeeBps: exports_external.number().optional()
|
|
13992
|
+
}).passthrough().nullable(),
|
|
13993
|
+
replay: exports_external.boolean().optional()
|
|
13994
|
+
});
|
|
13995
|
+
lpPositionsSchema = exports_external.object({
|
|
13996
|
+
positions: exports_external.array(exports_external.object({
|
|
13997
|
+
position: exports_external.string(),
|
|
13998
|
+
pool: exports_external.string(),
|
|
13999
|
+
tokens: exports_external.array(exports_external.object({
|
|
14000
|
+
mint: exports_external.string(),
|
|
14001
|
+
decimals: exports_external.number().int(),
|
|
14002
|
+
amountRaw: exports_external.string(),
|
|
14003
|
+
unclaimedFeesRaw: exports_external.string(),
|
|
14004
|
+
valueUsd: exports_external.number().nullable().optional()
|
|
14005
|
+
}).passthrough()),
|
|
14006
|
+
poolShare: exports_external.number().optional(),
|
|
14007
|
+
valueUsd: exports_external.number().nullable().optional()
|
|
14008
|
+
}).passthrough())
|
|
14009
|
+
});
|
|
14010
|
+
lpPoolsSchema = exports_external.object({
|
|
14011
|
+
page: exports_external.number().int(),
|
|
14012
|
+
pages: exports_external.number().int(),
|
|
14013
|
+
pools: exports_external.array(exports_external.object({
|
|
14014
|
+
pool: exports_external.string(),
|
|
14015
|
+
tokens: exports_external.array(exports_external.string()),
|
|
14016
|
+
liquidityUsd: exports_external.number().nullable().optional(),
|
|
14017
|
+
baseFeePercent: exports_external.number().nullable().optional(),
|
|
14018
|
+
volume24hUsd: exports_external.number().nullable().optional(),
|
|
14019
|
+
estimatedAprPercent: exports_external.number().nullable().optional()
|
|
14020
|
+
}).passthrough())
|
|
14021
|
+
});
|
|
13900
14022
|
BASES = {
|
|
13901
14023
|
SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
|
|
13902
14024
|
USDC: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
|
|
@@ -37604,7 +37726,7 @@ var init_update_notice = __esm(() => {
|
|
|
37604
37726
|
});
|
|
37605
37727
|
|
|
37606
37728
|
// ../mcp/src/orchestrate.ts
|
|
37607
|
-
import { randomUUID as
|
|
37729
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
37608
37730
|
function requireApiKey(cfg) {
|
|
37609
37731
|
if (!cfg.apiKey) {
|
|
37610
37732
|
throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
|
|
@@ -37716,7 +37838,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
|
|
|
37716
37838
|
}
|
|
37717
37839
|
async function executeTrade(args, cfg, doFetch) {
|
|
37718
37840
|
const apiKey = requireApiKey(cfg);
|
|
37719
|
-
const clientTradeId = args.clientTradeId ??
|
|
37841
|
+
const clientTradeId = args.clientTradeId ?? randomUUID4();
|
|
37720
37842
|
if (args.amount !== undefined && args.percent !== undefined) {
|
|
37721
37843
|
return errText("pass exactly one of amount or percent, not both", { clientTradeId });
|
|
37722
37844
|
}
|
|
@@ -37854,7 +37976,7 @@ async function executeTrade(args, cfg, doFetch) {
|
|
|
37854
37976
|
}
|
|
37855
37977
|
async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
37856
37978
|
const apiKey = requireApiKey(cfg);
|
|
37857
|
-
const clientLaunchId = args.clientLaunchId ??
|
|
37979
|
+
const clientLaunchId = args.clientLaunchId ?? randomUUID4();
|
|
37858
37980
|
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...launchFields } = args;
|
|
37859
37981
|
let buyAmount;
|
|
37860
37982
|
if (devBuy !== undefined) {
|
|
@@ -38263,8 +38385,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
38263
38385
|
inputSchema: setProfileWalletsShape
|
|
38264
38386
|
}, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
|
|
38265
38387
|
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
|
|
38388
|
+
title: "Read an agent profile's P&L",
|
|
38389
|
+
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
38390
|
inputSchema: profilePnlShape
|
|
38269
38391
|
}, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
|
|
38270
38392
|
register("candle_get_profile_trades", {
|
|
@@ -38593,7 +38715,7 @@ init_render();
|
|
|
38593
38715
|
init_secret_store();
|
|
38594
38716
|
|
|
38595
38717
|
// src/version.ts
|
|
38596
|
-
var CLI_VERSION = "0.11.
|
|
38718
|
+
var CLI_VERSION = "0.11.7";
|
|
38597
38719
|
|
|
38598
38720
|
// src/commands/auth.ts
|
|
38599
38721
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -39092,6 +39214,45 @@ var HELP = {
|
|
|
39092
39214
|
],
|
|
39093
39215
|
env: ENV_API
|
|
39094
39216
|
},
|
|
39217
|
+
lp: {
|
|
39218
|
+
group: "Trade",
|
|
39219
|
+
summary: "Meteora DAMM v2 liquidity from a TEE wallet: pools, add, positions, remove, claim",
|
|
39220
|
+
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).",
|
|
39221
|
+
usage: ["candle lp <subcommand> [flags]"],
|
|
39222
|
+
rows: [
|
|
39223
|
+
{
|
|
39224
|
+
invocation: "pools <token> [--page <n>]",
|
|
39225
|
+
description: "Pools listing a token: liquidity, fee, 24h volume, estimated yield"
|
|
39226
|
+
},
|
|
39227
|
+
{
|
|
39228
|
+
invocation: "add <pool> --amount <n> <token> --wallet <tee> [--position <nft>]",
|
|
39229
|
+
description: "Open a position, or add to one; the other side follows the pool's ratio"
|
|
39230
|
+
},
|
|
39231
|
+
{ invocation: "positions [--wallet <tee>]", description: "Every position across your TEE wallets" },
|
|
39232
|
+
{
|
|
39233
|
+
invocation: "remove <position> --percent <n> [--wallet <tee>]",
|
|
39234
|
+
description: "Withdraw; at 100% also claims fees and closes the position"
|
|
39235
|
+
},
|
|
39236
|
+
{ invocation: "claim <position> [--wallet <tee>]", description: "Claim fees and any rewards" }
|
|
39237
|
+
],
|
|
39238
|
+
flags: [
|
|
39239
|
+
{ invocation: "--wallet <tee>", description: "The TEE wallet by id, address or unique label" },
|
|
39240
|
+
{ invocation: "--rpc-url <url>", description: "Your Solana RPC, for mint reads and the broadcast" },
|
|
39241
|
+
{
|
|
39242
|
+
invocation: "--client-trade-id <id>",
|
|
39243
|
+
description: "Idempotency: the same id never deposits or withdraws twice"
|
|
39244
|
+
},
|
|
39245
|
+
{ invocation: "--slippage-bps <n>", description: "add and remove: the tolerance, 0 to 1000 (default 100)" },
|
|
39246
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt (an ordinary prompt only)" }
|
|
39247
|
+
],
|
|
39248
|
+
examples: [
|
|
39249
|
+
"candle lp pools So11111111111111111111111111111111111111112",
|
|
39250
|
+
"candle lp add <pool> --amount 0.5 SOL --wallet AgentOne --rpc-url https://<rpc>",
|
|
39251
|
+
"candle lp positions",
|
|
39252
|
+
"candle lp remove <position> --percent 100 --wallet AgentOne --rpc-url https://<rpc>"
|
|
39253
|
+
],
|
|
39254
|
+
env: ENV_API
|
|
39255
|
+
},
|
|
39095
39256
|
launch: {
|
|
39096
39257
|
group: "Trade",
|
|
39097
39258
|
summary: "Create a Solana token (the first buy is a separate swap)",
|
|
@@ -39107,6 +39268,31 @@ var HELP = {
|
|
|
39107
39268
|
examples: ["candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne"],
|
|
39108
39269
|
env: ENV_API
|
|
39109
39270
|
},
|
|
39271
|
+
portfolio: {
|
|
39272
|
+
group: "Account",
|
|
39273
|
+
summary: "Every wallet's holdings, prices and value in one table (vault read over your own RPC)",
|
|
39274
|
+
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.",
|
|
39275
|
+
usage: ["candle portfolio [--rpc-url <url>] [--json]"],
|
|
39276
|
+
rows: [],
|
|
39277
|
+
flags: [
|
|
39278
|
+
{
|
|
39279
|
+
invocation: "--rpc-url <url>",
|
|
39280
|
+
description: "Your own Solana RPC, for the vault. Without it (or CANDLE_SOLANA_RPC_URL) the vault is not read"
|
|
39281
|
+
},
|
|
39282
|
+
KEYSTORE_FLAG
|
|
39283
|
+
],
|
|
39284
|
+
examples: ["candle portfolio", "candle portfolio --rpc-url https://your-rpc.example --json"],
|
|
39285
|
+
env: [...ENV_API, ...ENV_LOCAL_SIGNING]
|
|
39286
|
+
},
|
|
39287
|
+
pnl: {
|
|
39288
|
+
group: "Account",
|
|
39289
|
+
summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
|
|
39290
|
+
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.",
|
|
39291
|
+
usage: ["candle pnl [--profile <name>] [--json]"],
|
|
39292
|
+
rows: [],
|
|
39293
|
+
examples: ["candle pnl", "candle pnl --profile scalper --json"],
|
|
39294
|
+
env: ENV_API
|
|
39295
|
+
},
|
|
39110
39296
|
keys: {
|
|
39111
39297
|
group: "Account",
|
|
39112
39298
|
summary: "API keys, and the wallets each key may use",
|
|
@@ -39228,14 +39414,14 @@ var HELP = {
|
|
|
39228
39414
|
},
|
|
39229
39415
|
{
|
|
39230
39416
|
invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
|
|
39231
|
-
description: "Sign a vault
|
|
39417
|
+
description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
|
|
39232
39418
|
},
|
|
39233
39419
|
{
|
|
39234
|
-
invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>]",
|
|
39420
|
+
invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
|
|
39235
39421
|
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
39422
|
},
|
|
39237
39423
|
{
|
|
39238
|
-
invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings]",
|
|
39424
|
+
invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--to-key <prefix|label>] [--token-holdings]",
|
|
39239
39425
|
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
39426
|
},
|
|
39241
39427
|
{
|
|
@@ -39269,6 +39455,10 @@ var HELP = {
|
|
|
39269
39455
|
{
|
|
39270
39456
|
invocation: "--pairs-from <file>",
|
|
39271
39457
|
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."
|
|
39458
|
+
},
|
|
39459
|
+
{
|
|
39460
|
+
invocation: "--to-key <prefix|label>",
|
|
39461
|
+
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
39462
|
}
|
|
39273
39463
|
],
|
|
39274
39464
|
examples: [
|
|
@@ -39278,6 +39468,7 @@ var HELP = {
|
|
|
39278
39468
|
"candle vault rename key-7 treasury-cold",
|
|
39279
39469
|
"candle vault new-key --chain solana --labels-from ./replacement-names.txt",
|
|
39280
39470
|
"candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc>",
|
|
39471
|
+
"candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc> --to-key tr-01",
|
|
39281
39472
|
"candle vault enroll security-key --label yubikey-a",
|
|
39282
39473
|
"candle vault backup --to /Volumes/BACKUP/vault.enc",
|
|
39283
39474
|
"candle vault backup --to icloud",
|
|
@@ -39310,7 +39501,7 @@ var HELP = {
|
|
|
39310
39501
|
},
|
|
39311
39502
|
{
|
|
39312
39503
|
invocation: "sweep <address> --rpc-url <url> [--emergency]",
|
|
39313
|
-
description: "Sign locally and move everything to the pinned vault"
|
|
39504
|
+
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
39505
|
},
|
|
39315
39506
|
{
|
|
39316
39507
|
invocation: "rebind <wallet...> --to-key <prefix|label>",
|
|
@@ -43820,7 +44011,7 @@ function validClientId(id) {
|
|
|
43820
44011
|
}
|
|
43821
44012
|
async function lookupOperation(ctx, key, id, kind) {
|
|
43822
44013
|
const local = await savedOperation(ctx, key, id);
|
|
43823
|
-
const kinds = kind ? [kind] : local ? [local.kind] : ["trade", "swap", "launch"];
|
|
44014
|
+
const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
|
|
43824
44015
|
const found = [];
|
|
43825
44016
|
for (const candidate of kinds) {
|
|
43826
44017
|
try {
|
|
@@ -44095,7 +44286,7 @@ async function launch(args, ctx) {
|
|
|
44095
44286
|
if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
|
|
44096
44287
|
throw new TradingError("QUOTE_EXPIRED", "The launch build expired before signing.");
|
|
44097
44288
|
const signed = await relaySign(ctx, key, wallet, built.transaction);
|
|
44098
|
-
const signature = await
|
|
44289
|
+
const signature = await saveOperationSignature(ctx, key, id, "launch", signed);
|
|
44099
44290
|
const broadcastSignature = await createSolanaRpc(url, ctx.deps.fetch).sendTransaction(signed);
|
|
44100
44291
|
if (broadcastSignature !== signature)
|
|
44101
44292
|
throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
|
|
@@ -44108,6 +44299,368 @@ async function launch(args, ctx) {
|
|
|
44108
44299
|
}
|
|
44109
44300
|
}
|
|
44110
44301
|
|
|
44302
|
+
// src/commands/lp.ts
|
|
44303
|
+
init_args();
|
|
44304
|
+
init_render();
|
|
44305
|
+
init_solana_lite();
|
|
44306
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
44307
|
+
init_trading();
|
|
44308
|
+
var LP_SCOPE = "lp:write";
|
|
44309
|
+
var CONFIRM_POLL_MS2 = 2000;
|
|
44310
|
+
var CONFIRM_MAX_POLLS2 = 45;
|
|
44311
|
+
async function lpRequest(ctx, key, path, body) {
|
|
44312
|
+
const result = await apiRequest(path, {
|
|
44313
|
+
apiUrl: ctx.apiUrl,
|
|
44314
|
+
credentials: { apiKey: key },
|
|
44315
|
+
auth: "key",
|
|
44316
|
+
method: body ? "POST" : "GET",
|
|
44317
|
+
body,
|
|
44318
|
+
fetch: ctx.deps.fetch,
|
|
44319
|
+
env: ctx.deps.env
|
|
44320
|
+
});
|
|
44321
|
+
if (!result.ok) {
|
|
44322
|
+
if (result.status === 404 && !result.code)
|
|
44323
|
+
throw new TradingError("LP_NOT_ENABLED", "This Candle deployment does not serve LP routes (LP_ENABLED is off there, or the API predates them).");
|
|
44324
|
+
throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
|
|
44325
|
+
}
|
|
44326
|
+
if (!result.body || typeof result.body !== "object")
|
|
44327
|
+
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
44328
|
+
return result.body;
|
|
44329
|
+
}
|
|
44330
|
+
function solanaAddress(value, what) {
|
|
44331
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
|
|
44332
|
+
throw new TradingError("INVALID_ADDRESS", `${what} must be a Solana address (base58).`);
|
|
44333
|
+
return value;
|
|
44334
|
+
}
|
|
44335
|
+
function poolToken(value) {
|
|
44336
|
+
const base = baseAsset(value);
|
|
44337
|
+
if (base)
|
|
44338
|
+
return BASES[base]?.mint;
|
|
44339
|
+
return solanaAddress(value, "The token");
|
|
44340
|
+
}
|
|
44341
|
+
function matchesWallet(row, name) {
|
|
44342
|
+
return row.id === name || row.address === name || row.label === name;
|
|
44343
|
+
}
|
|
44344
|
+
function usage2(ctx, line) {
|
|
44345
|
+
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
44346
|
+
return 2;
|
|
44347
|
+
}
|
|
44348
|
+
function amountLine(amount) {
|
|
44349
|
+
const base = Object.entries(BASES).find(([, asset]) => asset.mint === amount.mint)?.[0];
|
|
44350
|
+
return `${decimalAmount(amount.raw, amount.decimals)} ${base ?? amount.mint}`;
|
|
44351
|
+
}
|
|
44352
|
+
async function lpPools(args, ctx) {
|
|
44353
|
+
const parsed = parseArgs(args, { valueFlags: ["--page"] });
|
|
44354
|
+
const page = Number(parsed && !("error" in parsed) ? parsed.values["--page"] ?? "1" : "1");
|
|
44355
|
+
if ("error" in parsed)
|
|
44356
|
+
return usage2(ctx, parsed.error);
|
|
44357
|
+
if (parsed.positionals.length !== 1 || !Number.isSafeInteger(page) || page < 1)
|
|
44358
|
+
return usage2(ctx, "Usage: candle lp pools <token> [--page <n>]");
|
|
44359
|
+
try {
|
|
44360
|
+
const token = poolToken(parsed.positionals[0]);
|
|
44361
|
+
const key = await tradingKey(ctx);
|
|
44362
|
+
const pools = lpPoolsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/pools/${encodeURIComponent(token)}?page=${page}`));
|
|
44363
|
+
if (ctx.json)
|
|
44364
|
+
return printTradingResult(ctx, { success: true, token, ...pools });
|
|
44365
|
+
const out = ctx.deps.stdout;
|
|
44366
|
+
if (pools.pools.length === 0) {
|
|
44367
|
+
out.write(`No DAMM v2 pool lists ${token} (page ${pools.page} of ${pools.pages}).
|
|
44368
|
+
`);
|
|
44369
|
+
return 0;
|
|
44370
|
+
}
|
|
44371
|
+
const usd = (value) => value === null || value === undefined ? "unavailable" : `$${value.toFixed(2)}`;
|
|
44372
|
+
const pct = (value) => value === null || value === undefined ? "unavailable" : `${value.toFixed(2)}%`;
|
|
44373
|
+
out.write(`DAMM v2 pools for ${token} (page ${pools.page} of ${pools.pages}):
|
|
44374
|
+
`);
|
|
44375
|
+
for (const pool of pools.pools) {
|
|
44376
|
+
out.write(` ${safeText(pool.pool)}
|
|
44377
|
+
pair ${pool.tokens.map(safeText).join(" / ")}
|
|
44378
|
+
liquidity ${usd(pool.liquidityUsd)}, fee ${pct(pool.baseFeePercent)}, 24h volume ${usd(pool.volume24hUsd)}, est. yield ${pct(pool.estimatedAprPercent)} APR (indexed, not a quote)
|
|
44379
|
+
`);
|
|
44380
|
+
}
|
|
44381
|
+
return 0;
|
|
44382
|
+
} catch (error) {
|
|
44383
|
+
return tradingFailure(ctx, error);
|
|
44384
|
+
}
|
|
44385
|
+
}
|
|
44386
|
+
async function lpPositions(args, ctx) {
|
|
44387
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
|
|
44388
|
+
if ("error" in parsed)
|
|
44389
|
+
return usage2(ctx, parsed.error);
|
|
44390
|
+
if (parsed.positionals.length !== 0)
|
|
44391
|
+
return usage2(ctx, "Usage: candle lp positions [--wallet <tee>]");
|
|
44392
|
+
try {
|
|
44393
|
+
const key = await tradingKey(ctx);
|
|
44394
|
+
const { rows } = await listTradingWallets(ctx, key);
|
|
44395
|
+
const name = parsed.values["--wallet"];
|
|
44396
|
+
const wallets = rows.filter((row) => row.chain === "solana" && (name === undefined || matchesWallet(row, name)));
|
|
44397
|
+
if (name !== undefined && wallets.length === 0)
|
|
44398
|
+
throw new TradingError("TEE_WALLET_REQUIRED", `No TEE wallet on this key is called "${name}".`);
|
|
44399
|
+
const report = [];
|
|
44400
|
+
for (const row of wallets) {
|
|
44401
|
+
const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
|
|
44402
|
+
report.push({
|
|
44403
|
+
id: row.id,
|
|
44404
|
+
address: row.address,
|
|
44405
|
+
...row.label ? { label: row.label } : {},
|
|
44406
|
+
positions: positions.positions
|
|
44407
|
+
});
|
|
44408
|
+
}
|
|
44409
|
+
if (ctx.json)
|
|
44410
|
+
return printTradingResult(ctx, { success: true, wallets: report });
|
|
44411
|
+
const out = ctx.deps.stdout;
|
|
44412
|
+
const total = report.reduce((n, wallet) => n + wallet.positions.length, 0);
|
|
44413
|
+
if (total === 0) {
|
|
44414
|
+
out.write(`No DAMM v2 positions across ${report.length} TEE wallet(s).
|
|
44415
|
+
`);
|
|
44416
|
+
return 0;
|
|
44417
|
+
}
|
|
44418
|
+
for (const wallet of report) {
|
|
44419
|
+
if (wallet.positions.length === 0)
|
|
44420
|
+
continue;
|
|
44421
|
+
out.write(`${wallet.label ? `${safeText(wallet.label)} ` : ""}(${wallet.id}, ${safeText(wallet.address)})
|
|
44422
|
+
`);
|
|
44423
|
+
for (const position of wallet.positions) {
|
|
44424
|
+
out.write(` position ${safeText(position.position)} in pool ${safeText(position.pool)}
|
|
44425
|
+
`);
|
|
44426
|
+
for (const token of position.tokens) {
|
|
44427
|
+
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)})`}
|
|
44428
|
+
`);
|
|
44429
|
+
}
|
|
44430
|
+
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`}
|
|
44431
|
+
`);
|
|
44432
|
+
}
|
|
44433
|
+
}
|
|
44434
|
+
return 0;
|
|
44435
|
+
} catch (error) {
|
|
44436
|
+
return tradingFailure(ctx, error);
|
|
44437
|
+
}
|
|
44438
|
+
}
|
|
44439
|
+
async function walletHolding(ctx, key, position, name) {
|
|
44440
|
+
if (name !== undefined)
|
|
44441
|
+
return tradingWallet(ctx, key, name, LP_SCOPE);
|
|
44442
|
+
const { rows, appId } = await listTradingWallets(ctx, key, LP_SCOPE);
|
|
44443
|
+
for (const row of rows) {
|
|
44444
|
+
if (row.chain !== "solana")
|
|
44445
|
+
continue;
|
|
44446
|
+
const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
|
|
44447
|
+
if (positions.positions.some((held) => held.position === position))
|
|
44448
|
+
return completeTradingWallet(ctx, row, appId, LP_SCOPE);
|
|
44449
|
+
}
|
|
44450
|
+
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.`);
|
|
44451
|
+
}
|
|
44452
|
+
async function waitConfirmed(ctx, rpc2, signature, id) {
|
|
44453
|
+
for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
|
|
44454
|
+
const observed = classifyStatus(await rpc2.getSignatureStatus(signature));
|
|
44455
|
+
if (observed.kind === "finalized")
|
|
44456
|
+
return;
|
|
44457
|
+
if (observed.kind === "failed")
|
|
44458
|
+
throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}.`);
|
|
44459
|
+
if (observed.kind === "nonfinal") {
|
|
44460
|
+
if (observed.err !== null && observed.err !== undefined)
|
|
44461
|
+
throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed: ${JSON.stringify(observed.err)}.`);
|
|
44462
|
+
if (observed.confirmationStatus === "confirmed")
|
|
44463
|
+
return;
|
|
44464
|
+
}
|
|
44465
|
+
await ctx.deps.sleep(CONFIRM_POLL_MS2);
|
|
44466
|
+
}
|
|
44467
|
+
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.`);
|
|
44468
|
+
}
|
|
44469
|
+
async function confirmPreview(ctx, plan, built) {
|
|
44470
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
44471
|
+
output.write(`${safeText(plan.intent)}
|
|
44472
|
+
Payer: ${safeText(plan.wallet.address)}
|
|
44473
|
+
`);
|
|
44474
|
+
output.write(`Pool: ${safeText(built.build.pool)}
|
|
44475
|
+
Position: ${safeText(built.build.position)}
|
|
44476
|
+
`);
|
|
44477
|
+
const preview = built.preview;
|
|
44478
|
+
const amounts = preview?.amounts ?? [];
|
|
44479
|
+
if (amounts.length > 0) {
|
|
44480
|
+
const verb = plan.action === "add" ? "Deposit (maximum, at the pool's ratio)" : plan.action === "remove" ? "Withdraw (minimum)" : "Claim";
|
|
44481
|
+
output.write(`${verb}: ${amounts.map(amountLine).join(" + ")}
|
|
44482
|
+
`);
|
|
44483
|
+
}
|
|
44484
|
+
output.write(`Candle LP fee: ${safeText(preview?.candleFeeBps ?? 0)} bps
|
|
44485
|
+
`);
|
|
44486
|
+
for (const warning of preview?.warnings ?? [])
|
|
44487
|
+
output.write(`Warning: ${safeText(warning)}
|
|
44488
|
+
`);
|
|
44489
|
+
for (const risk of preview?.tokenRisks ?? [])
|
|
44490
|
+
output.write(`Warning (${safeText(risk.mint)}): ${safeText(risk.message)}
|
|
44491
|
+
`);
|
|
44492
|
+
if (built.replay)
|
|
44493
|
+
output.write(`This client id was already built; the same artifact is shown again.
|
|
44494
|
+
`);
|
|
44495
|
+
if (plan.yes)
|
|
44496
|
+
return true;
|
|
44497
|
+
if (!ctx.deps.isTTY.stdin)
|
|
44498
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary LP prompt.");
|
|
44499
|
+
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
44500
|
+
}
|
|
44501
|
+
async function runLpOperation(ctx, plan) {
|
|
44502
|
+
const { id, key, wallet, action } = plan;
|
|
44503
|
+
const confirm = async (signature2, extra) => {
|
|
44504
|
+
const result = await lpRequest(ctx, key, "/api/v1/agent/lp/confirm", { clientTradeId: id, signature: signature2 });
|
|
44505
|
+
return printTradingResult(ctx, {
|
|
44506
|
+
...result,
|
|
44507
|
+
clientTradeId: id,
|
|
44508
|
+
kind: "lp",
|
|
44509
|
+
action,
|
|
44510
|
+
signature: signature2,
|
|
44511
|
+
wallet: safeText(wallet.address),
|
|
44512
|
+
...extra
|
|
44513
|
+
});
|
|
44514
|
+
};
|
|
44515
|
+
const local = await savedOperation(ctx, key, id);
|
|
44516
|
+
if (local?.signature)
|
|
44517
|
+
return confirm(local.signature, { resumed: true });
|
|
44518
|
+
if (!local && !await claimOperation(ctx, key, id, "lp"))
|
|
44519
|
+
throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
|
|
44520
|
+
ctx.deps.stderr.write(`Operation: ${id}
|
|
44521
|
+
`);
|
|
44522
|
+
const built = lpBuildSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/${action}/build`, {
|
|
44523
|
+
linkedWalletId: wallet.id,
|
|
44524
|
+
clientTradeId: id,
|
|
44525
|
+
...plan.body
|
|
44526
|
+
}));
|
|
44527
|
+
if (built.build.walletAddress !== wallet.address || built.build.action !== action)
|
|
44528
|
+
throw new TradingError("INVALID_RESPONSE", "The LP build does not name the requested wallet and action; nothing was signed.");
|
|
44529
|
+
if (built.build.signature)
|
|
44530
|
+
return confirm(built.build.signature, { resumed: true });
|
|
44531
|
+
const preview = {
|
|
44532
|
+
pool: built.build.pool,
|
|
44533
|
+
position: built.build.position,
|
|
44534
|
+
amounts: built.preview?.amounts ?? [],
|
|
44535
|
+
warnings: built.preview?.warnings ?? [],
|
|
44536
|
+
tokenRisks: built.preview?.tokenRisks ?? [],
|
|
44537
|
+
candleFeeBps: built.preview?.candleFeeBps ?? 0
|
|
44538
|
+
};
|
|
44539
|
+
if (!await confirmPreview(ctx, plan, built))
|
|
44540
|
+
return printTradingResult(ctx, {
|
|
44541
|
+
success: true,
|
|
44542
|
+
status: "cancelled",
|
|
44543
|
+
clientTradeId: id,
|
|
44544
|
+
kind: "lp",
|
|
44545
|
+
action,
|
|
44546
|
+
preview
|
|
44547
|
+
});
|
|
44548
|
+
const signed = await relaySign(ctx, key, wallet, built.build.transaction);
|
|
44549
|
+
const signature = await saveOperationSignature(ctx, key, id, "lp", signed);
|
|
44550
|
+
const rpc2 = createSolanaRpc(plan.url, ctx.deps.fetch);
|
|
44551
|
+
const echoed = await rpc2.sendTransaction(signed);
|
|
44552
|
+
if (echoed !== signature)
|
|
44553
|
+
throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
|
|
44554
|
+
ctx.deps.stderr.write(`LP ${action} signature: ${signature}
|
|
44555
|
+
`);
|
|
44556
|
+
await waitConfirmed(ctx, rpc2, signature, id);
|
|
44557
|
+
return confirm(signature, { preview });
|
|
44558
|
+
}
|
|
44559
|
+
function slippageOf(flag) {
|
|
44560
|
+
const slippage = Number(flag ?? "100");
|
|
44561
|
+
if (!Number.isInteger(slippage) || slippage < 0 || slippage > 1000)
|
|
44562
|
+
throw new TradingError("INVALID_AMOUNT", "--slippage-bps must be an integer from 0 to 1000.");
|
|
44563
|
+
return slippage;
|
|
44564
|
+
}
|
|
44565
|
+
async function lpAdd(args, ctx) {
|
|
44566
|
+
const parsed = parseArgs(args, {
|
|
44567
|
+
valueFlags: ["--amount", "--wallet", "--position", "--client-trade-id", "--slippage-bps", "--rpc-url"],
|
|
44568
|
+
booleanFlags: ["--yes"]
|
|
44569
|
+
});
|
|
44570
|
+
if ("error" in parsed)
|
|
44571
|
+
return usage2(ctx, parsed.error);
|
|
44572
|
+
const flags = parsed.values;
|
|
44573
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44574
|
+
if (parsed.positionals.length !== 2 || !flags["--amount"] || !flags["--wallet"] || !validClientId(id))
|
|
44575
|
+
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]");
|
|
44576
|
+
try {
|
|
44577
|
+
const pool = solanaAddress(parsed.positionals[0], "The pool");
|
|
44578
|
+
const token = poolToken(parsed.positionals[1]);
|
|
44579
|
+
const position = flags["--position"] ? solanaAddress(flags["--position"], "--position") : undefined;
|
|
44580
|
+
rawAmount(flags["--amount"], 18);
|
|
44581
|
+
const slippageBps = slippageOf(flags["--slippage-bps"]);
|
|
44582
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44583
|
+
const key = await tradingKey(ctx);
|
|
44584
|
+
const wallet = await tradingWallet(ctx, key, flags["--wallet"], LP_SCOPE);
|
|
44585
|
+
const decimals = await decimalsFor(ctx, baseAsset(token) ?? token, flags["--rpc-url"]);
|
|
44586
|
+
const amountRaw = rawAmount(flags["--amount"], decimals);
|
|
44587
|
+
return await runLpOperation(ctx, {
|
|
44588
|
+
action: "add",
|
|
44589
|
+
id,
|
|
44590
|
+
key,
|
|
44591
|
+
wallet,
|
|
44592
|
+
url,
|
|
44593
|
+
yes: parsed.booleans.has("--yes"),
|
|
44594
|
+
intent: `Add ${decimalAmount(amountRaw, decimals)} ${baseAsset(token) ?? token} of liquidity to DAMM v2 pool ${pool}${position ? ` (position ${position})` : " (new position)"}`,
|
|
44595
|
+
body: { pool, token, amountRaw, slippageBps, ...position ? { position } : {} }
|
|
44596
|
+
});
|
|
44597
|
+
} catch (error) {
|
|
44598
|
+
return tradingFailure(ctx, error, id);
|
|
44599
|
+
}
|
|
44600
|
+
}
|
|
44601
|
+
async function lpRemove(args, ctx) {
|
|
44602
|
+
const parsed = parseArgs(args, {
|
|
44603
|
+
valueFlags: ["--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
|
|
44604
|
+
booleanFlags: ["--yes"]
|
|
44605
|
+
});
|
|
44606
|
+
if ("error" in parsed)
|
|
44607
|
+
return usage2(ctx, parsed.error);
|
|
44608
|
+
const flags = parsed.values;
|
|
44609
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44610
|
+
const percent = Number(flags["--percent"]);
|
|
44611
|
+
if (parsed.positionals.length !== 1 || !flags["--percent"] || !/^\d+(\.\d{1,2})?$/.test(flags["--percent"]) || !(percent > 0 && percent <= 100) || !validClientId(id))
|
|
44612
|
+
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]");
|
|
44613
|
+
try {
|
|
44614
|
+
const position = solanaAddress(parsed.positionals[0], "The position");
|
|
44615
|
+
const slippageBps = slippageOf(flags["--slippage-bps"]);
|
|
44616
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44617
|
+
const key = await tradingKey(ctx);
|
|
44618
|
+
const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
|
|
44619
|
+
return await runLpOperation(ctx, {
|
|
44620
|
+
action: "remove",
|
|
44621
|
+
id,
|
|
44622
|
+
key,
|
|
44623
|
+
wallet,
|
|
44624
|
+
url,
|
|
44625
|
+
yes: parsed.booleans.has("--yes"),
|
|
44626
|
+
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}`,
|
|
44627
|
+
body: { position, percent, slippageBps }
|
|
44628
|
+
});
|
|
44629
|
+
} catch (error) {
|
|
44630
|
+
return tradingFailure(ctx, error, id);
|
|
44631
|
+
}
|
|
44632
|
+
}
|
|
44633
|
+
async function lpClaim(args, ctx) {
|
|
44634
|
+
const parsed = parseArgs(args, {
|
|
44635
|
+
valueFlags: ["--wallet", "--client-trade-id", "--rpc-url"],
|
|
44636
|
+
booleanFlags: ["--yes"]
|
|
44637
|
+
});
|
|
44638
|
+
if ("error" in parsed)
|
|
44639
|
+
return usage2(ctx, parsed.error);
|
|
44640
|
+
const flags = parsed.values;
|
|
44641
|
+
const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
|
|
44642
|
+
if (parsed.positionals.length !== 1 || !validClientId(id))
|
|
44643
|
+
return usage2(ctx, "Usage: candle lp claim <position> [--wallet <tee>] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
|
|
44644
|
+
try {
|
|
44645
|
+
const position = solanaAddress(parsed.positionals[0], "The position");
|
|
44646
|
+
const url = rpcUrl(ctx, flags["--rpc-url"]);
|
|
44647
|
+
const key = await tradingKey(ctx);
|
|
44648
|
+
const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
|
|
44649
|
+
return await runLpOperation(ctx, {
|
|
44650
|
+
action: "claim",
|
|
44651
|
+
id,
|
|
44652
|
+
key,
|
|
44653
|
+
wallet,
|
|
44654
|
+
url,
|
|
44655
|
+
yes: parsed.booleans.has("--yes"),
|
|
44656
|
+
intent: `Claim the fees and any rewards of position ${position}`,
|
|
44657
|
+
body: { position }
|
|
44658
|
+
});
|
|
44659
|
+
} catch (error) {
|
|
44660
|
+
return tradingFailure(ctx, error, id);
|
|
44661
|
+
}
|
|
44662
|
+
}
|
|
44663
|
+
|
|
44111
44664
|
// src/commands/mcp.ts
|
|
44112
44665
|
init_args();
|
|
44113
44666
|
init_deps();
|
|
@@ -44434,20 +44987,20 @@ async function writeSecretNames(deps, profile, names) {
|
|
|
44434
44987
|
else
|
|
44435
44988
|
await deps.updateProfile(profile, { secretNames: sorted });
|
|
44436
44989
|
}
|
|
44437
|
-
function
|
|
44990
|
+
function usage3(ctx, line) {
|
|
44438
44991
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
44439
44992
|
return 2;
|
|
44440
44993
|
}
|
|
44441
44994
|
async function secretsSet(args, ctx) {
|
|
44442
44995
|
const parsed = parseArgs(args, {});
|
|
44443
44996
|
if ("error" in parsed)
|
|
44444
|
-
return
|
|
44997
|
+
return usage3(ctx, parsed.error);
|
|
44445
44998
|
const [raw, extra] = parsed.positionals;
|
|
44446
44999
|
if (!raw || extra !== undefined)
|
|
44447
|
-
return
|
|
45000
|
+
return usage3(ctx, "Usage: candle secrets set <name>");
|
|
44448
45001
|
const name = canonicalSecretName(raw);
|
|
44449
45002
|
if (name === undefined) {
|
|
44450
|
-
return
|
|
45003
|
+
return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
44451
45004
|
}
|
|
44452
45005
|
if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
|
|
44453
45006
|
writeLocalFailure(ctx.deps, {
|
|
@@ -44459,7 +45012,7 @@ async function secretsSet(args, ctx) {
|
|
|
44459
45012
|
}
|
|
44460
45013
|
const value = await ctx.deps.promptSecret(`Value for ${name} (input hidden): `);
|
|
44461
45014
|
if (value.length === 0)
|
|
44462
|
-
return
|
|
45015
|
+
return usage3(ctx, "An empty value was typed; nothing was stored.");
|
|
44463
45016
|
try {
|
|
44464
45017
|
await ctx.deps.secretsStore.set(secretRef(ctx.profile, name), value);
|
|
44465
45018
|
} catch (error) {
|
|
@@ -44479,9 +45032,9 @@ async function secretsSet(args, ctx) {
|
|
|
44479
45032
|
async function secretsList(args, ctx) {
|
|
44480
45033
|
const parsed = parseArgs(args, {});
|
|
44481
45034
|
if ("error" in parsed)
|
|
44482
|
-
return
|
|
45035
|
+
return usage3(ctx, parsed.error);
|
|
44483
45036
|
if (parsed.positionals.length > 0)
|
|
44484
|
-
return
|
|
45037
|
+
return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
44485
45038
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
44486
45039
|
if (ctx.json) {
|
|
44487
45040
|
ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
|
|
@@ -44500,13 +45053,13 @@ async function secretsList(args, ctx) {
|
|
|
44500
45053
|
async function secretsRemove(args, ctx) {
|
|
44501
45054
|
const parsed = parseArgs(args, {});
|
|
44502
45055
|
if ("error" in parsed)
|
|
44503
|
-
return
|
|
45056
|
+
return usage3(ctx, parsed.error);
|
|
44504
45057
|
const [raw, extra] = parsed.positionals;
|
|
44505
45058
|
if (!raw || extra !== undefined)
|
|
44506
|
-
return
|
|
45059
|
+
return usage3(ctx, "Usage: candle secrets remove <name>");
|
|
44507
45060
|
const name = canonicalSecretName(raw);
|
|
44508
45061
|
if (name === undefined) {
|
|
44509
|
-
return
|
|
45062
|
+
return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
44510
45063
|
}
|
|
44511
45064
|
await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name));
|
|
44512
45065
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
@@ -44618,6 +45171,645 @@ async function runPlugin(name, rawArgs, ctx) {
|
|
|
44618
45171
|
return ctx.deps.runPlugin(path, split2.passthrough, env);
|
|
44619
45172
|
}
|
|
44620
45173
|
|
|
45174
|
+
// src/commands/pnl.ts
|
|
45175
|
+
init_args();
|
|
45176
|
+
init_deps();
|
|
45177
|
+
init_profiles();
|
|
45178
|
+
init_render();
|
|
45179
|
+
|
|
45180
|
+
// src/usd.ts
|
|
45181
|
+
function formatUsd(value) {
|
|
45182
|
+
if (!Number.isFinite(value))
|
|
45183
|
+
return "?";
|
|
45184
|
+
const sign2 = value < 0 ? "-" : "";
|
|
45185
|
+
const abs = Math.abs(value);
|
|
45186
|
+
if (abs > 0 && abs < 0.005)
|
|
45187
|
+
return `${sign2}<$0.01`;
|
|
45188
|
+
return `${sign2}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
45189
|
+
}
|
|
45190
|
+
function formatPrice(value) {
|
|
45191
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
45192
|
+
return "?";
|
|
45193
|
+
if (value >= 1)
|
|
45194
|
+
return formatUsd(value);
|
|
45195
|
+
return `$${Number(value.toPrecision(4)).toLocaleString("en-US", { maximumFractionDigits: 12 })}`;
|
|
45196
|
+
}
|
|
45197
|
+
function formatAmount(raw, decimals) {
|
|
45198
|
+
const digits = BigInt(raw).toString().padStart(decimals + 1, "0");
|
|
45199
|
+
if (decimals === 0)
|
|
45200
|
+
return digits;
|
|
45201
|
+
const whole = digits.slice(0, -decimals);
|
|
45202
|
+
const fraction = digits.slice(-decimals).replace(/0+$/, "");
|
|
45203
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
45204
|
+
}
|
|
45205
|
+
function formatQuantity(value) {
|
|
45206
|
+
if (!Number.isFinite(value))
|
|
45207
|
+
return "?";
|
|
45208
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
45209
|
+
}
|
|
45210
|
+
function shortAddress2(address) {
|
|
45211
|
+
return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address;
|
|
45212
|
+
}
|
|
45213
|
+
|
|
45214
|
+
// src/commands/pnl.ts
|
|
45215
|
+
var NO_API_KEY2 = {
|
|
45216
|
+
code: "NO_API_KEY",
|
|
45217
|
+
message: "No API key for this profile.",
|
|
45218
|
+
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
45219
|
+
};
|
|
45220
|
+
async function pnl(args, ctx) {
|
|
45221
|
+
const { deps, apiUrl, json } = ctx;
|
|
45222
|
+
const parsed = parseArgs(args, {});
|
|
45223
|
+
if ("error" in parsed) {
|
|
45224
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
45225
|
+
return 2;
|
|
45226
|
+
}
|
|
45227
|
+
if (parsed.positionals.length > 0) {
|
|
45228
|
+
writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}. Usage: candle pnl [--profile <name>]`, json);
|
|
45229
|
+
return 2;
|
|
45230
|
+
}
|
|
45231
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
45232
|
+
if (!apiKey) {
|
|
45233
|
+
writeLocalFailure(deps, NO_API_KEY2, json);
|
|
45234
|
+
return 1;
|
|
45235
|
+
}
|
|
45236
|
+
const perProfile = ctx.profileFlag !== undefined;
|
|
45237
|
+
let keyPrefix;
|
|
45238
|
+
if (perProfile) {
|
|
45239
|
+
keyPrefix = apiKeyPrefix(apiKey);
|
|
45240
|
+
if (keyPrefix === undefined) {
|
|
45241
|
+
writeLocalFailure(deps, {
|
|
45242
|
+
code: "BAD_REQUEST",
|
|
45243
|
+
message: `The API key for profile ${ctx.profileFlag} is not a Candle agent key, so it names no profile to read.`,
|
|
45244
|
+
suggestion: "Run `candle auth login --profile <name>` to store a key for that profile."
|
|
45245
|
+
}, json);
|
|
45246
|
+
return 1;
|
|
45247
|
+
}
|
|
45248
|
+
}
|
|
45249
|
+
await printIdentity(ctx);
|
|
45250
|
+
const path = perProfile ? `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl` : "/api/v1/agent/books";
|
|
45251
|
+
const result = await apiRequest(path, {
|
|
45252
|
+
auth: "key",
|
|
45253
|
+
credentials: { apiKey },
|
|
45254
|
+
apiUrl,
|
|
45255
|
+
fetch: deps.fetch,
|
|
45256
|
+
env: deps.env
|
|
45257
|
+
});
|
|
45258
|
+
if (!result.ok) {
|
|
45259
|
+
if (!perProfile && result.code === "SCOPE_MISSING") {
|
|
45260
|
+
writeLocalFailure(deps, {
|
|
45261
|
+
code: "SCOPE_MISSING",
|
|
45262
|
+
message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
|
|
45263
|
+
suggestion: "Log in with a Read or Read:Write key, or read this key's own P&L: candle pnl --profile <name>"
|
|
45264
|
+
}, json);
|
|
45265
|
+
return 1;
|
|
45266
|
+
}
|
|
45267
|
+
writeFailure(deps, result, { apiUrl, authType: "key" }, json);
|
|
45268
|
+
return 1;
|
|
45269
|
+
}
|
|
45270
|
+
const body = result.body;
|
|
45271
|
+
if (json) {
|
|
45272
|
+
const { success: _success, ...rest } = body;
|
|
45273
|
+
deps.stdout.write(`${JSON.stringify({ ok: true, scope: perProfile ? "profile" : "account", ...rest })}
|
|
45274
|
+
`);
|
|
45275
|
+
return 0;
|
|
45276
|
+
}
|
|
45277
|
+
if (perProfile) {
|
|
45278
|
+
const { pnl: p, lp } = body;
|
|
45279
|
+
deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
|
|
45280
|
+
|
|
45281
|
+
`);
|
|
45282
|
+
writeSummary(ctx, {
|
|
45283
|
+
realizedNetUsd: p.realizedNetUsd,
|
|
45284
|
+
realizedGrossUsd: p.realizedGrossUsd,
|
|
45285
|
+
feesUsd: p.feesUsd,
|
|
45286
|
+
unrealizedUsd: p.unrealizedUsd,
|
|
45287
|
+
unmarked: p.unmarkedPositions,
|
|
45288
|
+
unvalued: p.unvalued,
|
|
45289
|
+
counted: p.counted,
|
|
45290
|
+
positions: p.openPositions.length,
|
|
45291
|
+
truncated: p.truncated,
|
|
45292
|
+
lookback: p.lookback,
|
|
45293
|
+
lookbackUnit: "trades",
|
|
45294
|
+
oldestMarkAt: p.oldestMarkAt,
|
|
45295
|
+
lp
|
|
45296
|
+
});
|
|
45297
|
+
writePositions(ctx, p.openPositions, false);
|
|
45298
|
+
writeLpPositions(ctx, lp, false);
|
|
45299
|
+
return 0;
|
|
45300
|
+
}
|
|
45301
|
+
const books = body;
|
|
45302
|
+
deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
|
|
45303
|
+
|
|
45304
|
+
`);
|
|
45305
|
+
writeSummary(ctx, {
|
|
45306
|
+
...books.all,
|
|
45307
|
+
positions: books.positions.length,
|
|
45308
|
+
truncated: books.truncated,
|
|
45309
|
+
lookback: books.lookback,
|
|
45310
|
+
lookbackUnit: "ledger rows",
|
|
45311
|
+
oldestMarkAt: books.oldestMarkAt,
|
|
45312
|
+
lp: books.lp
|
|
45313
|
+
});
|
|
45314
|
+
writePositions(ctx, books.positions, true);
|
|
45315
|
+
writeLpPositions(ctx, books.lp, true);
|
|
45316
|
+
return 0;
|
|
45317
|
+
}
|
|
45318
|
+
function writeSummary(ctx, s) {
|
|
45319
|
+
const marked = s.positions - s.unmarked;
|
|
45320
|
+
const lines = [
|
|
45321
|
+
[
|
|
45322
|
+
"Realized net",
|
|
45323
|
+
formatUsd(s.realizedNetUsd),
|
|
45324
|
+
`gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
|
|
45325
|
+
],
|
|
45326
|
+
[
|
|
45327
|
+
"Unrealized",
|
|
45328
|
+
formatUsd(s.unrealizedUsd),
|
|
45329
|
+
`${marked} of ${s.positions} open ${s.positions === 1 ? "position" : "positions"} marked${s.unmarked > 0 ? `; ${s.unmarked} unpriced, not counted` : ""}`
|
|
45330
|
+
]
|
|
45331
|
+
];
|
|
45332
|
+
const lp = s.lp;
|
|
45333
|
+
if (lp?.read) {
|
|
45334
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
45335
|
+
lines.push([
|
|
45336
|
+
"LP realized",
|
|
45337
|
+
formatUsd(lp.realizedUsd),
|
|
45338
|
+
`withdrawn ${formatUsd(lp.withdrawnUsd)} against ${formatUsd(lp.realizedBasisUsd)} of cost basis, plus ${formatUsd(lp.claimedFeesUsd)} claimed fees`
|
|
45339
|
+
], [
|
|
45340
|
+
"LP unrealized",
|
|
45341
|
+
formatUsd(lp.unrealizedUsd),
|
|
45342
|
+
`${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` : ""}`
|
|
45343
|
+
], [
|
|
45344
|
+
"LP vs holding",
|
|
45345
|
+
lp.vsHoldingPositions > 0 ? formatUsd(lp.vsHoldingUsd) : "-",
|
|
45346
|
+
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"
|
|
45347
|
+
], [
|
|
45348
|
+
"Total",
|
|
45349
|
+
formatUsd(s.realizedNetUsd + s.unrealizedUsd + lp.realizedUsd + lp.unrealizedUsd),
|
|
45350
|
+
"realized net plus unrealized, tokens and LP"
|
|
45351
|
+
]);
|
|
45352
|
+
} else if (lp) {
|
|
45353
|
+
lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized, tokens only"]);
|
|
45354
|
+
} else {
|
|
45355
|
+
lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
|
|
45356
|
+
}
|
|
45357
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
45358
|
+
const valueWidth = Math.max(...lines.map(([, value]) => value.length));
|
|
45359
|
+
for (const [label, value, note] of lines) {
|
|
45360
|
+
ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
|
|
45361
|
+
`);
|
|
45362
|
+
}
|
|
45363
|
+
if (s.oldestMarkAt !== undefined) {
|
|
45364
|
+
ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
|
|
45365
|
+
`);
|
|
45366
|
+
}
|
|
45367
|
+
if (s.unvalued > 0 || (s.unresolved ?? 0) > 0) {
|
|
45368
|
+
ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
|
|
45369
|
+
`);
|
|
45370
|
+
}
|
|
45371
|
+
if (s.truncated) {
|
|
45372
|
+
ctx.deps.stdout.write(`History is truncated: this covers the most recent ${s.lookback} ${s.lookbackUnit}, not the account's lifetime.
|
|
45373
|
+
`);
|
|
45374
|
+
}
|
|
45375
|
+
if (lp?.read) {
|
|
45376
|
+
if (lp.unvalued > 0) {
|
|
45377
|
+
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.
|
|
45378
|
+
`);
|
|
45379
|
+
}
|
|
45380
|
+
if (lp.closedOutsideLedger > 0) {
|
|
45381
|
+
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.
|
|
45382
|
+
`);
|
|
45383
|
+
}
|
|
45384
|
+
if (lp.truncated) {
|
|
45385
|
+
ctx.deps.stdout.write(`LP history is truncated: this covers the most recent ${lp.lookback} LP operations, not the account's lifetime.
|
|
45386
|
+
`);
|
|
45387
|
+
}
|
|
45388
|
+
}
|
|
45389
|
+
}
|
|
45390
|
+
function writeLpPositions(ctx, lp, withBook) {
|
|
45391
|
+
if (!lp?.read || lp.positions.length === 0)
|
|
45392
|
+
return;
|
|
45393
|
+
const headers = ["POSITION", "POOL", "WALLET", "VALUE", "COST BASIS", "UNREALIZED", "VS HOLDING"];
|
|
45394
|
+
if (withBook)
|
|
45395
|
+
headers.push("BOOK");
|
|
45396
|
+
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";
|
|
45397
|
+
const rows = lp.positions.map((p) => {
|
|
45398
|
+
const row = [
|
|
45399
|
+
shortAddress2(p.position),
|
|
45400
|
+
shortAddress2(p.pool),
|
|
45401
|
+
shortAddress2(p.wallet),
|
|
45402
|
+
value(p),
|
|
45403
|
+
formatUsd(p.costBasisUsd),
|
|
45404
|
+
p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-",
|
|
45405
|
+
p.vsHoldingUsd !== undefined ? formatUsd(p.vsHoldingUsd) : "-"
|
|
45406
|
+
];
|
|
45407
|
+
if (withBook)
|
|
45408
|
+
row.push(p.book ?? "-");
|
|
45409
|
+
return row;
|
|
45410
|
+
});
|
|
45411
|
+
ctx.deps.stdout.write(`
|
|
45412
|
+
Open LP positions
|
|
45413
|
+
${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
45414
|
+
`);
|
|
45415
|
+
}
|
|
45416
|
+
function writePositions(ctx, positions, withBook) {
|
|
45417
|
+
if (positions.length === 0) {
|
|
45418
|
+
ctx.deps.stdout.write(`
|
|
45419
|
+
No open positions.
|
|
45420
|
+
`);
|
|
45421
|
+
return;
|
|
45422
|
+
}
|
|
45423
|
+
const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
|
|
45424
|
+
if (withBook)
|
|
45425
|
+
headers.push("BOOK");
|
|
45426
|
+
const rows = positions.map((p) => {
|
|
45427
|
+
const row = [
|
|
45428
|
+
p.symbol?.trim() || shortAddress2(p.mint),
|
|
45429
|
+
formatQuantity(p.quantity),
|
|
45430
|
+
formatPrice(p.avgEntryUsd),
|
|
45431
|
+
p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
|
|
45432
|
+
p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-"
|
|
45433
|
+
];
|
|
45434
|
+
if (withBook)
|
|
45435
|
+
row.push(p.book ?? "-");
|
|
45436
|
+
return row;
|
|
45437
|
+
});
|
|
45438
|
+
ctx.deps.stdout.write(`
|
|
45439
|
+
Open positions
|
|
45440
|
+
${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
45441
|
+
`);
|
|
45442
|
+
}
|
|
45443
|
+
|
|
45444
|
+
// src/commands/portfolio.ts
|
|
45445
|
+
init_args();
|
|
45446
|
+
init_deps();
|
|
45447
|
+
init_profiles();
|
|
45448
|
+
init_render();
|
|
45449
|
+
init_solana_lite();
|
|
45450
|
+
init_store();
|
|
45451
|
+
init_vault_support();
|
|
45452
|
+
var SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
45453
|
+
var KNOWN_SYMBOLS = {
|
|
45454
|
+
[SOL_MINT]: "SOL",
|
|
45455
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
|
|
45456
|
+
"9dXSV8VWuYvGfTzqvkBeoFwH9ihVTybDuWo5VaJPCNDL": "CNDL"
|
|
45457
|
+
};
|
|
45458
|
+
var CHUNK = 100;
|
|
45459
|
+
var TOKEN_READS_IN_FLIGHT = 8;
|
|
45460
|
+
var NO_API_KEY3 = {
|
|
45461
|
+
code: "NO_API_KEY",
|
|
45462
|
+
message: "No API key for this profile.",
|
|
45463
|
+
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
45464
|
+
};
|
|
45465
|
+
async function portfolio(args, ctx) {
|
|
45466
|
+
const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
|
|
45467
|
+
if ("error" in parsed)
|
|
45468
|
+
return usage(ctx, parsed.error);
|
|
45469
|
+
if (parsed.positionals.length > 0)
|
|
45470
|
+
return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
45471
|
+
const { deps } = ctx;
|
|
45472
|
+
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
45473
|
+
if (!apiKey) {
|
|
45474
|
+
writeLocalFailure(deps, NO_API_KEY3, ctx.json);
|
|
45475
|
+
return 1;
|
|
45476
|
+
}
|
|
45477
|
+
const rpcGiven = parsed.values["--rpc-url"] !== undefined || Boolean(deps.env[RPC_URL_ENV]?.trim());
|
|
45478
|
+
let rpcUrl2;
|
|
45479
|
+
if (rpcGiven) {
|
|
45480
|
+
const resolved = rpcUrlFrom(ctx, parsed);
|
|
45481
|
+
if (typeof resolved !== "string")
|
|
45482
|
+
return usage(ctx, resolved.error);
|
|
45483
|
+
rpcUrl2 = resolved;
|
|
45484
|
+
}
|
|
45485
|
+
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
45486
|
+
if ("error" in resolvedVault)
|
|
45487
|
+
return usage(ctx, resolvedVault.error);
|
|
45488
|
+
return runVaultCommand(ctx, async ({ hold }) => {
|
|
45489
|
+
let vaultEntries;
|
|
45490
|
+
let vaultReason;
|
|
45491
|
+
const raw = rpcUrl2 === undefined ? null : await readVaultRaw(resolvedVault.path);
|
|
45492
|
+
if (rpcUrl2 === undefined) {
|
|
45493
|
+
vaultReason = `not read: vault balances are read only over your own RPC; pass --rpc-url or set ${RPC_URL_ENV}`;
|
|
45494
|
+
} else if (raw === null) {
|
|
45495
|
+
vaultReason = `no vault at ${resolvedVault.path}`;
|
|
45496
|
+
} else {
|
|
45497
|
+
if (!refuseEnvPassphrase(ctx))
|
|
45498
|
+
return 1;
|
|
45499
|
+
if (!requirePromptStreams(ctx, "portfolio"))
|
|
45500
|
+
return 1;
|
|
45501
|
+
const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
|
|
45502
|
+
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 }));
|
|
45503
|
+
}
|
|
45504
|
+
await printIdentity(ctx);
|
|
45505
|
+
const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
|
|
45506
|
+
if (vaultEntries && vaultEntries.length > 0 && rpcHost !== undefined) {
|
|
45507
|
+
const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
|
|
45508
|
+
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.
|
|
45509
|
+
`);
|
|
45510
|
+
}
|
|
45511
|
+
const [candle, vaultRead] = await Promise.all([
|
|
45512
|
+
apiRequest("/api/v1/agent/portfolio", {
|
|
45513
|
+
auth: "key",
|
|
45514
|
+
credentials: { apiKey },
|
|
45515
|
+
apiUrl: ctx.apiUrl,
|
|
45516
|
+
fetch: deps.fetch,
|
|
45517
|
+
env: deps.env
|
|
45518
|
+
}),
|
|
45519
|
+
vaultEntries && rpcUrl2 ? readOwnRpc(vaultEntries.map((entry) => entry.address), rpcUrl2, deps.fetch) : Promise.resolve(undefined)
|
|
45520
|
+
]);
|
|
45521
|
+
if (!candle.ok) {
|
|
45522
|
+
writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
|
|
45523
|
+
return 1;
|
|
45524
|
+
}
|
|
45525
|
+
const fromCandle = candle.body;
|
|
45526
|
+
const prices = { ...fromCandle.prices ?? {} };
|
|
45527
|
+
let priceFailure;
|
|
45528
|
+
if (vaultRead) {
|
|
45529
|
+
const wanted = new Set;
|
|
45530
|
+
for (const wallet2 of vaultRead.byAddress.values()) {
|
|
45531
|
+
if (wallet2.lamports !== null)
|
|
45532
|
+
wanted.add(SOL_MINT);
|
|
45533
|
+
for (const token of wallet2.tokens ?? [])
|
|
45534
|
+
wanted.add(token.mint);
|
|
45535
|
+
}
|
|
45536
|
+
const missing = [...wanted].filter((mint) => prices[mint] === undefined);
|
|
45537
|
+
for (let at = 0;at < missing.length; at += CHUNK) {
|
|
45538
|
+
const priced = await apiRequest("/api/v1/agent/prices", {
|
|
45539
|
+
method: "POST",
|
|
45540
|
+
body: { mints: missing.slice(at, at + CHUNK) },
|
|
45541
|
+
auth: "key",
|
|
45542
|
+
credentials: { apiKey },
|
|
45543
|
+
apiUrl: ctx.apiUrl,
|
|
45544
|
+
fetch: deps.fetch,
|
|
45545
|
+
env: deps.env
|
|
45546
|
+
});
|
|
45547
|
+
if (priced.ok)
|
|
45548
|
+
Object.assign(prices, priced.body.prices ?? {});
|
|
45549
|
+
else
|
|
45550
|
+
priceFailure ??= priced.message;
|
|
45551
|
+
}
|
|
45552
|
+
}
|
|
45553
|
+
if (priceFailure !== undefined) {
|
|
45554
|
+
deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
|
|
45555
|
+
`);
|
|
45556
|
+
}
|
|
45557
|
+
const lpByWallet = new Map;
|
|
45558
|
+
const lpOf = (address) => {
|
|
45559
|
+
const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
|
|
45560
|
+
lpByWallet.set(address, entry);
|
|
45561
|
+
return entry;
|
|
45562
|
+
};
|
|
45563
|
+
for (const position of fromCandle.lp?.positions ?? [])
|
|
45564
|
+
lpOf(position.wallet).positions.push(lpRow(position, prices));
|
|
45565
|
+
for (const unread of fromCandle.lp?.unreadable ?? [])
|
|
45566
|
+
lpOf(unread.wallet).unread.push(unread.position);
|
|
45567
|
+
const wallet = (address, read, extra) => {
|
|
45568
|
+
const holdings = read === undefined ? null : valueHoldings(read, prices);
|
|
45569
|
+
const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
|
|
45570
|
+
const lp2 = fromCandle.lp ? lpOf(address) : undefined;
|
|
45571
|
+
return {
|
|
45572
|
+
address,
|
|
45573
|
+
...extra,
|
|
45574
|
+
holdings,
|
|
45575
|
+
...unread.length > 0 ? { unread } : {},
|
|
45576
|
+
...lp2 ? { lpPositions: lp2.positions } : {},
|
|
45577
|
+
...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
|
|
45578
|
+
valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
45579
|
+
unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
45580
|
+
};
|
|
45581
|
+
};
|
|
45582
|
+
const group = (name, wallets, read, reason) => ({
|
|
45583
|
+
group: name,
|
|
45584
|
+
read,
|
|
45585
|
+
...reason !== undefined ? { reason } : {},
|
|
45586
|
+
wallets,
|
|
45587
|
+
valueUsd: wallets.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
45588
|
+
unpriced: wallets.reduce((sum, w) => sum + w.unpriced, 0)
|
|
45589
|
+
});
|
|
45590
|
+
const orNull = (read) => read.lamports === null && read.tokens === null ? undefined : read;
|
|
45591
|
+
const groups = [
|
|
45592
|
+
group("vault", (vaultEntries ?? []).map((entry) => {
|
|
45593
|
+
const read = vaultRead?.byAddress.get(entry.address);
|
|
45594
|
+
return wallet(entry.address, read ? orNull(read) : undefined, { label: entry.label, role: entry.role });
|
|
45595
|
+
}), vaultEntries !== undefined, vaultReason),
|
|
45596
|
+
group("tee", (fromCandle.tee ?? []).map((row) => wallet(row.address, orNull(row), {
|
|
45597
|
+
id: row.id,
|
|
45598
|
+
...row.label ? { label: row.label } : {},
|
|
45599
|
+
active: row.active
|
|
45600
|
+
})), true),
|
|
45601
|
+
group("embedded", (fromCandle.embedded ?? []).map((row) => wallet(row.address, orNull(row), {})), true)
|
|
45602
|
+
];
|
|
45603
|
+
const unavailable = [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []];
|
|
45604
|
+
const lpUnread = (fromCandle.lp?.unreadable ?? []).length;
|
|
45605
|
+
const complete = fromCandle.complete !== false && unavailable.length === 0 && lpUnread === 0;
|
|
45606
|
+
const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
|
|
45607
|
+
const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
|
|
45608
|
+
const lp = fromCandle.lp ? {
|
|
45609
|
+
positions: fromCandle.lp.positions.length,
|
|
45610
|
+
unpriced: fromCandle.lp.positions.filter((p) => p.valueUsd === null).length,
|
|
45611
|
+
unreadable: lpUnread,
|
|
45612
|
+
valueUsd: fromCandle.lp.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
|
|
45613
|
+
} : undefined;
|
|
45614
|
+
if (ctx.json) {
|
|
45615
|
+
writeJson(deps, {
|
|
45616
|
+
ok: true,
|
|
45617
|
+
totalUsd,
|
|
45618
|
+
unpriced,
|
|
45619
|
+
complete,
|
|
45620
|
+
unavailable,
|
|
45621
|
+
...rpcHost !== undefined ? { rpcHost } : {},
|
|
45622
|
+
...lp ? { lp } : {},
|
|
45623
|
+
groups
|
|
45624
|
+
});
|
|
45625
|
+
return complete ? 0 : 3;
|
|
45626
|
+
}
|
|
45627
|
+
writeTable(ctx, groups, { totalUsd, unpriced, unavailable: unavailable.length, lp });
|
|
45628
|
+
return complete ? 0 : 3;
|
|
45629
|
+
});
|
|
45630
|
+
}
|
|
45631
|
+
async function readOwnRpc(addresses, rpcUrl2, fetchFn) {
|
|
45632
|
+
const rpc2 = createSolanaRpc(rpcUrl2, fetchFn);
|
|
45633
|
+
const unique = [...new Set(addresses)];
|
|
45634
|
+
const lamports = new Map;
|
|
45635
|
+
for (let at = 0;at < unique.length; at += CHUNK) {
|
|
45636
|
+
const chunk = unique.slice(at, at + CHUNK);
|
|
45637
|
+
try {
|
|
45638
|
+
const accounts = await rpc2.getMultipleAccounts(chunk);
|
|
45639
|
+
for (const [i, address] of chunk.entries())
|
|
45640
|
+
lamports.set(address, (accounts[i]?.lamports ?? 0n).toString());
|
|
45641
|
+
} catch {
|
|
45642
|
+
for (const address of chunk)
|
|
45643
|
+
lamports.set(address, null);
|
|
45644
|
+
}
|
|
45645
|
+
}
|
|
45646
|
+
const tokens = new Map;
|
|
45647
|
+
const failed = new Set;
|
|
45648
|
+
const reads = unique.flatMap((owner) => [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) => ({ owner, programId })));
|
|
45649
|
+
let next = 0;
|
|
45650
|
+
await Promise.all(Array.from({ length: Math.min(TOKEN_READS_IN_FLIGHT, reads.length) }, async () => {
|
|
45651
|
+
while (next < reads.length) {
|
|
45652
|
+
const { owner, programId } = reads[next++];
|
|
45653
|
+
try {
|
|
45654
|
+
const accounts = await rpc2.getTokenAccountsByOwner(owner, programId);
|
|
45655
|
+
const held = tokens.get(owner) ?? new Map;
|
|
45656
|
+
tokens.set(owner, held);
|
|
45657
|
+
const program = programId === TOKEN_PROGRAM_ID ? "token" : "token-2022";
|
|
45658
|
+
for (const account of accounts) {
|
|
45659
|
+
if (!/^\d+$/.test(account.amountRaw) || BigInt(account.amountRaw) === 0n)
|
|
45660
|
+
continue;
|
|
45661
|
+
const key = `${program}:${account.mint}`;
|
|
45662
|
+
const prior = held.get(key);
|
|
45663
|
+
held.set(key, {
|
|
45664
|
+
mint: account.mint,
|
|
45665
|
+
amountRaw: (BigInt(prior?.amountRaw ?? "0") + BigInt(account.amountRaw)).toString(),
|
|
45666
|
+
decimals: account.decimals,
|
|
45667
|
+
program
|
|
45668
|
+
});
|
|
45669
|
+
}
|
|
45670
|
+
} catch {
|
|
45671
|
+
failed.add(owner);
|
|
45672
|
+
}
|
|
45673
|
+
}
|
|
45674
|
+
}));
|
|
45675
|
+
const byAddress = new Map;
|
|
45676
|
+
const unavailable = [];
|
|
45677
|
+
for (const address of unique) {
|
|
45678
|
+
const sol = lamports.get(address) ?? null;
|
|
45679
|
+
const held = failed.has(address) ? null : [...tokens.get(address)?.values() ?? []];
|
|
45680
|
+
byAddress.set(address, { lamports: sol, tokens: held });
|
|
45681
|
+
if (sol === null || held === null)
|
|
45682
|
+
unavailable.push(address);
|
|
45683
|
+
}
|
|
45684
|
+
return { byAddress, unavailable };
|
|
45685
|
+
}
|
|
45686
|
+
function valueHoldings(read, prices) {
|
|
45687
|
+
const raw = [];
|
|
45688
|
+
if (read.lamports !== null && BigInt(read.lamports) > 0n) {
|
|
45689
|
+
raw.push({ mint: SOL_MINT, amountRaw: read.lamports, decimals: 9, program: "native" });
|
|
45690
|
+
}
|
|
45691
|
+
for (const token of read.tokens ?? [])
|
|
45692
|
+
raw.push(token);
|
|
45693
|
+
return raw.map((h) => {
|
|
45694
|
+
const price = prices[h.mint];
|
|
45695
|
+
const priceUsd = price?.priceUsd ?? null;
|
|
45696
|
+
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
45697
|
+
return {
|
|
45698
|
+
...h,
|
|
45699
|
+
symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
|
|
45700
|
+
amount,
|
|
45701
|
+
priceUsd,
|
|
45702
|
+
valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
|
|
45703
|
+
priceSource: price?.source ?? null
|
|
45704
|
+
};
|
|
45705
|
+
});
|
|
45706
|
+
}
|
|
45707
|
+
function lpRow(position, prices) {
|
|
45708
|
+
return {
|
|
45709
|
+
position: position.position,
|
|
45710
|
+
pool: position.pool,
|
|
45711
|
+
tokens: position.tokens.map((token) => ({
|
|
45712
|
+
...token,
|
|
45713
|
+
symbol: KNOWN_SYMBOLS[token.mint] ?? token.symbol ?? prices[token.mint]?.symbol ?? null,
|
|
45714
|
+
amount: formatAmount(token.amountRaw, token.decimals),
|
|
45715
|
+
unclaimedFees: formatAmount(token.unclaimedFeesRaw, token.decimals)
|
|
45716
|
+
})),
|
|
45717
|
+
...position.poolShare !== undefined ? { poolShare: position.poolShare } : {},
|
|
45718
|
+
valueUsd: position.valueUsd,
|
|
45719
|
+
unpriced: position.unpriced
|
|
45720
|
+
};
|
|
45721
|
+
}
|
|
45722
|
+
function writeTable(ctx, groups, totals) {
|
|
45723
|
+
const { deps } = ctx;
|
|
45724
|
+
const rows = [];
|
|
45725
|
+
const lpRows = [];
|
|
45726
|
+
const notes = [];
|
|
45727
|
+
for (const g of groups) {
|
|
45728
|
+
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);
|
|
45729
|
+
let lpCount = 0;
|
|
45730
|
+
let lpUnread = 0;
|
|
45731
|
+
for (const w of shown) {
|
|
45732
|
+
const name = `${w.label ? `${w.label} ` : ""}(${shortAddress2(w.address)})${w.role === "external" ? " external" : ""}`;
|
|
45733
|
+
const side = (t, amount) => `${amount} ${t.symbol ?? shortAddress2(t.mint)}`;
|
|
45734
|
+
for (const p of [...w.lpPositions ?? []].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1))) {
|
|
45735
|
+
lpCount += 1;
|
|
45736
|
+
lpRows.push([
|
|
45737
|
+
g.group,
|
|
45738
|
+
name,
|
|
45739
|
+
shortAddress2(p.position),
|
|
45740
|
+
shortAddress2(p.pool),
|
|
45741
|
+
p.tokens.map((t) => side(t, t.amount)).join(" + "),
|
|
45742
|
+
p.tokens.map((t) => side(t, t.unclaimedFees)).join(" + "),
|
|
45743
|
+
p.valueUsd === null ? "unpriced" : formatUsd(p.valueUsd)
|
|
45744
|
+
]);
|
|
45745
|
+
}
|
|
45746
|
+
for (const position of w.lpUnread ?? []) {
|
|
45747
|
+
lpUnread += 1;
|
|
45748
|
+
lpRows.push([g.group, name, shortAddress2(position), "-", "not read", "-", "-"]);
|
|
45749
|
+
}
|
|
45750
|
+
if (w.holdings === null) {
|
|
45751
|
+
rows.push([g.group, name, "-", "not read", "-", "-"]);
|
|
45752
|
+
continue;
|
|
45753
|
+
}
|
|
45754
|
+
const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
|
|
45755
|
+
for (const h of sorted) {
|
|
45756
|
+
rows.push([
|
|
45757
|
+
g.group,
|
|
45758
|
+
name,
|
|
45759
|
+
h.symbol ?? shortAddress2(h.mint),
|
|
45760
|
+
h.amount,
|
|
45761
|
+
h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
|
|
45762
|
+
h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
|
|
45763
|
+
]);
|
|
45764
|
+
}
|
|
45765
|
+
for (const part of w.unread ?? [])
|
|
45766
|
+
rows.push([g.group, name, part === "sol" ? "SOL" : "tokens", "not read", "-", "-"]);
|
|
45767
|
+
}
|
|
45768
|
+
const empty = g.wallets.length - shown.length;
|
|
45769
|
+
const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
|
|
45770
|
+
notes.push([
|
|
45771
|
+
g.group,
|
|
45772
|
+
g.read ? formatUsd(g.valueUsd) : "-",
|
|
45773
|
+
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"
|
|
45774
|
+
]);
|
|
45775
|
+
}
|
|
45776
|
+
if (rows.length > 0)
|
|
45777
|
+
deps.stdout.write(`
|
|
45778
|
+
${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
45779
|
+
`);
|
|
45780
|
+
else
|
|
45781
|
+
deps.stdout.write(`
|
|
45782
|
+
Nothing held in any wallet read.
|
|
45783
|
+
`);
|
|
45784
|
+
if (lpRows.length > 0)
|
|
45785
|
+
deps.stdout.write(`
|
|
45786
|
+
LP positions
|
|
45787
|
+
${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
|
|
45788
|
+
`);
|
|
45789
|
+
notes.push([
|
|
45790
|
+
"total",
|
|
45791
|
+
formatUsd(totals.totalUsd),
|
|
45792
|
+
totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : "every holding priced"
|
|
45793
|
+
]);
|
|
45794
|
+
const width = Math.max(...notes.map(([label]) => label.length));
|
|
45795
|
+
const valueWidth = Math.max(...notes.map(([, value]) => value.length));
|
|
45796
|
+
deps.stdout.write(`
|
|
45797
|
+
`);
|
|
45798
|
+
for (const [label, value, note] of notes) {
|
|
45799
|
+
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
|
|
45800
|
+
`);
|
|
45801
|
+
}
|
|
45802
|
+
if (totals.unavailable > 0) {
|
|
45803
|
+
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.
|
|
45804
|
+
`);
|
|
45805
|
+
}
|
|
45806
|
+
if ((totals.lp?.unreadable ?? 0) > 0) {
|
|
45807
|
+
const n = totals.lp?.unreadable ?? 0;
|
|
45808
|
+
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.
|
|
45809
|
+
`);
|
|
45810
|
+
}
|
|
45811
|
+
}
|
|
45812
|
+
|
|
44621
45813
|
// src/commands/profile.ts
|
|
44622
45814
|
init_args();
|
|
44623
45815
|
init_profiles();
|
|
@@ -45353,7 +46545,7 @@ async function readMints(rpc2, mints) {
|
|
|
45353
46545
|
}
|
|
45354
46546
|
return out;
|
|
45355
46547
|
}
|
|
45356
|
-
function
|
|
46548
|
+
function formatAmount2(raw, decimals) {
|
|
45357
46549
|
if (decimals === undefined)
|
|
45358
46550
|
return `${raw} raw`;
|
|
45359
46551
|
const negative = raw < 0n;
|
|
@@ -45363,7 +46555,7 @@ function formatAmount(raw, decimals) {
|
|
|
45363
46555
|
return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
|
|
45364
46556
|
}
|
|
45365
46557
|
function formatSol(lamports) {
|
|
45366
|
-
return `${
|
|
46558
|
+
return `${formatAmount2(lamports, 9)} SOL`;
|
|
45367
46559
|
}
|
|
45368
46560
|
function displayLines(input) {
|
|
45369
46561
|
const { tx, compiled, signers, simulation, mints } = input;
|
|
@@ -45386,7 +46578,7 @@ function displayLines(input) {
|
|
|
45386
46578
|
}
|
|
45387
46579
|
for (const token of deltas.tokens.filter((delta) => delta.owner === entry.address)) {
|
|
45388
46580
|
const info = mints.get(token.mint);
|
|
45389
|
-
lines.push(` ${token.mint}: ${
|
|
46581
|
+
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
46582
|
}
|
|
45391
46583
|
}
|
|
45392
46584
|
const others = [];
|
|
@@ -45401,7 +46593,7 @@ function displayLines(input) {
|
|
|
45401
46593
|
if (signerAddresses.has(token.owner) || token.after <= token.before)
|
|
45402
46594
|
continue;
|
|
45403
46595
|
const info = mints.get(token.mint);
|
|
45404
|
-
others.push(`${token.owner} receives +${
|
|
46596
|
+
others.push(`${token.owner} receives +${formatAmount2(token.after - token.before, info?.decimals)} of ${token.mint} (account ${token.account})`);
|
|
45405
46597
|
}
|
|
45406
46598
|
if (others.length === 0)
|
|
45407
46599
|
lines.push("others no other account gains a balance in the simulation");
|
|
@@ -45689,6 +46881,285 @@ async function signMessage2(args, ctx) {
|
|
|
45689
46881
|
init_esm();
|
|
45690
46882
|
init_args();
|
|
45691
46883
|
init_deps();
|
|
46884
|
+
|
|
46885
|
+
// src/lp-close.ts
|
|
46886
|
+
init_solana_lite();
|
|
46887
|
+
var DAMM_V2_PROGRAM_ID = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
|
|
46888
|
+
var CLOSE_PROGRAM_ALLOWLIST = new Set([
|
|
46889
|
+
DAMM_V2_PROGRAM_ID,
|
|
46890
|
+
TOKEN_PROGRAM_ID,
|
|
46891
|
+
TOKEN_2022_PROGRAM_ID,
|
|
46892
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
46893
|
+
COMPUTE_BUDGET_PROGRAM_ID,
|
|
46894
|
+
SYSTEM_PROGRAM_ID
|
|
46895
|
+
]);
|
|
46896
|
+
var DAMM_POSITION_CLOSE_UNAVAILABLE = "DAMM_POSITION_CLOSE_UNAVAILABLE";
|
|
46897
|
+
var DAMM_CLOSE_TRANSACTION_REFUSED = "DAMM_CLOSE_TRANSACTION_REFUSED";
|
|
46898
|
+
function isPositionCandidate(account) {
|
|
46899
|
+
return account.programId === TOKEN_2022_PROGRAM_ID && account.amountRaw === "1" && account.decimals === 0;
|
|
46900
|
+
}
|
|
46901
|
+
function parseCloseArtifact(body) {
|
|
46902
|
+
if (!body || typeof body !== "object")
|
|
46903
|
+
return;
|
|
46904
|
+
const { transaction, accountKeys } = body;
|
|
46905
|
+
if (typeof transaction !== "string" || transaction.length === 0)
|
|
46906
|
+
return;
|
|
46907
|
+
if (!Array.isArray(accountKeys) || !accountKeys.every((key) => typeof key === "string" && key.length > 0))
|
|
46908
|
+
return;
|
|
46909
|
+
return { transaction, accountKeys };
|
|
46910
|
+
}
|
|
46911
|
+
var MINT_BASE_SIZE2 = 82;
|
|
46912
|
+
var ACCOUNT_TYPE_OFFSET2 = 165;
|
|
46913
|
+
var ACCOUNT_TYPE_MINT = 1;
|
|
46914
|
+
var MULTISIG_SIZE2 = 355;
|
|
46915
|
+
var TOKEN_ACCOUNT_SIZE2 = 165;
|
|
46916
|
+
var TOKEN_ACCOUNT_STATE_INITIALIZED = 1;
|
|
46917
|
+
var CONTROL_RANGES = [
|
|
46918
|
+
[0, 32, "mint"],
|
|
46919
|
+
[32, 64, "authority"],
|
|
46920
|
+
[72, 108, "delegate"],
|
|
46921
|
+
[108, 109, "state"],
|
|
46922
|
+
[121, 129, "delegated amount"],
|
|
46923
|
+
[129, 165, "close authority"]
|
|
46924
|
+
];
|
|
46925
|
+
var TOKEN_IX_CLOSE_ACCOUNT = 9;
|
|
46926
|
+
var ATA_IX_CREATE = 0;
|
|
46927
|
+
var ATA_IX_CREATE_IDEMPOTENT = 1;
|
|
46928
|
+
function isTokenProgram(owner) {
|
|
46929
|
+
return owner === TOKEN_PROGRAM_ID || owner === TOKEN_2022_PROGRAM_ID;
|
|
46930
|
+
}
|
|
46931
|
+
function isMintAccount(view) {
|
|
46932
|
+
if (view === null || !isTokenProgram(view.owner))
|
|
46933
|
+
return false;
|
|
46934
|
+
if (view.data.length === MINT_BASE_SIZE2)
|
|
46935
|
+
return true;
|
|
46936
|
+
if (view.data.length === MULTISIG_SIZE2)
|
|
46937
|
+
return false;
|
|
46938
|
+
return view.data.length > ACCOUNT_TYPE_OFFSET2 && view.data[ACCOUNT_TYPE_OFFSET2] === ACCOUNT_TYPE_MINT;
|
|
46939
|
+
}
|
|
46940
|
+
function isUnsigned(tx) {
|
|
46941
|
+
return tx.signatures.every((slot) => slot.every((byte) => byte === 0));
|
|
46942
|
+
}
|
|
46943
|
+
function bytesEqual2(a, b) {
|
|
46944
|
+
return a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
46945
|
+
}
|
|
46946
|
+
function allZero(bytes) {
|
|
46947
|
+
return bytes.every((byte) => byte === 0);
|
|
46948
|
+
}
|
|
46949
|
+
async function verifyCloseArtifact(input) {
|
|
46950
|
+
const { artifact, rpc: rpc2, tee, vault, nftMint, nftAccount } = input;
|
|
46951
|
+
const refuse2 = (reason) => ({ ok: false, reason });
|
|
46952
|
+
const allowedOwner = (address) => address === tee || address === vault;
|
|
46953
|
+
let tx;
|
|
46954
|
+
try {
|
|
46955
|
+
tx = decodeTransaction(decodeStrictBase64(artifact.transaction));
|
|
46956
|
+
} catch (error) {
|
|
46957
|
+
if (error instanceof TransactionDecodeError)
|
|
46958
|
+
return refuse2(`the close transaction is undecodable: ${error.message}`);
|
|
46959
|
+
return refuse2(`the close transaction could not be read: ${error instanceof Error ? error.message : error}`);
|
|
46960
|
+
}
|
|
46961
|
+
if (tx.message.version !== 0)
|
|
46962
|
+
return refuse2("the close transaction is not a v0 message");
|
|
46963
|
+
if (!isUnsigned(tx))
|
|
46964
|
+
return refuse2("the close transaction already carries a signature; close-build must return it unsigned");
|
|
46965
|
+
if (tx.message.numRequiredSignatures !== 1)
|
|
46966
|
+
return refuse2(`the close transaction requires ${tx.message.numRequiredSignatures} signers; a position close needs only the TEE wallet`);
|
|
46967
|
+
if (tx.message.staticKeys[0] !== tee)
|
|
46968
|
+
return refuse2(`the fee payer is ${tx.message.staticKeys[0]}, not the TEE wallet`);
|
|
46969
|
+
let compiled;
|
|
46970
|
+
try {
|
|
46971
|
+
compiled = await resolveCompiledKeys(tx.message, rpc2);
|
|
46972
|
+
} catch (error) {
|
|
46973
|
+
if (error instanceof LookupTableError)
|
|
46974
|
+
return refuse2(`a lookup table could not be resolved: ${error.message}`);
|
|
46975
|
+
return refuse2(`the lookup tables could not be read: ${error instanceof Error ? error.message : error}`);
|
|
46976
|
+
}
|
|
46977
|
+
if (compiled.keys.length !== artifact.accountKeys.length)
|
|
46978
|
+
return refuse2(`the server listed ${artifact.accountKeys.length} account keys and the transaction resolves to ${compiled.keys.length}`);
|
|
46979
|
+
for (const [i, key] of compiled.keys.entries()) {
|
|
46980
|
+
if (artifact.accountKeys[i] !== key)
|
|
46981
|
+
return refuse2(`account key ${i} is ${key} in the transaction and ${artifact.accountKeys[i]} in the server's ordered array`);
|
|
46982
|
+
}
|
|
46983
|
+
let dammCalls = 0;
|
|
46984
|
+
for (const [i, ix] of tx.message.instructions.entries()) {
|
|
46985
|
+
const program = compiled.keys[ix.programIdIndex];
|
|
46986
|
+
const account = (at) => compiled.keys[ix.accountIndexes[at] ?? -1];
|
|
46987
|
+
if (program === undefined || !CLOSE_PROGRAM_ALLOWLIST.has(program))
|
|
46988
|
+
return refuse2(`instruction ${i} calls ${program ?? "an unknown program"}, which a position close never does`);
|
|
46989
|
+
switch (program) {
|
|
46990
|
+
case DAMM_V2_PROGRAM_ID:
|
|
46991
|
+
dammCalls += 1;
|
|
46992
|
+
break;
|
|
46993
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
46994
|
+
break;
|
|
46995
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID: {
|
|
46996
|
+
const kind = ix.data.length === 0 ? ATA_IX_CREATE : ix.data.length === 1 ? ix.data[0] : -1;
|
|
46997
|
+
if (kind !== ATA_IX_CREATE && kind !== ATA_IX_CREATE_IDEMPOTENT)
|
|
46998
|
+
return refuse2(`instruction ${i} is an Associated Token instruction other than Create, which a close never needs`);
|
|
46999
|
+
if (account(0) !== tee || !allowedOwner(account(2) ?? ""))
|
|
47000
|
+
return refuse2(`instruction ${i} creates a token account for ${account(2) ?? "?"}, not the TEE wallet or its vault`);
|
|
47001
|
+
break;
|
|
47002
|
+
}
|
|
47003
|
+
case TOKEN_PROGRAM_ID:
|
|
47004
|
+
case TOKEN_2022_PROGRAM_ID: {
|
|
47005
|
+
if (ix.data[0] !== TOKEN_IX_CLOSE_ACCOUNT || ix.data.length !== 1)
|
|
47006
|
+
return refuse2(`instruction ${i} is a token instruction other than CloseAccount, which a close never issues at the top level`);
|
|
47007
|
+
if (account(1) !== tee || account(2) !== tee)
|
|
47008
|
+
return refuse2(`instruction ${i} closes a token account to ${account(1) ?? "?"}, not the TEE wallet`);
|
|
47009
|
+
break;
|
|
47010
|
+
}
|
|
47011
|
+
default:
|
|
47012
|
+
return refuse2(`instruction ${i} is a top-level System instruction, which a close never issues`);
|
|
47013
|
+
}
|
|
47014
|
+
}
|
|
47015
|
+
if (dammCalls === 0)
|
|
47016
|
+
return refuse2("the transaction calls no DAMM v2 instruction, so it cannot close a position");
|
|
47017
|
+
let simulation;
|
|
47018
|
+
try {
|
|
47019
|
+
simulation = await simulateWithSnapshots(rpc2, artifact.transaction, compiled);
|
|
47020
|
+
} catch (error) {
|
|
47021
|
+
return refuse2(`the simulation could not be run: ${error instanceof Error ? error.message : error}`);
|
|
47022
|
+
}
|
|
47023
|
+
if (simulation.result.err !== null && simulation.result.err !== undefined)
|
|
47024
|
+
return refuse2(`the simulation failed: ${JSON.stringify(simulation.result.err)}`);
|
|
47025
|
+
const deltas = computeDeltas(simulation.snapshots);
|
|
47026
|
+
const postState = new Map(simulation.snapshots.map((snapshot) => [snapshot.address, snapshot.after]));
|
|
47027
|
+
for (const token of deltas.tokens) {
|
|
47028
|
+
if (token.after > token.before && !allowedOwner(token.owner))
|
|
47029
|
+
return refuse2(`${token.owner} would receive ${token.after - token.before} raw of ${token.mint} in account ${token.account}`);
|
|
47030
|
+
}
|
|
47031
|
+
for (const sol of deltas.sol) {
|
|
47032
|
+
if (sol.after <= sol.before)
|
|
47033
|
+
continue;
|
|
47034
|
+
if (allowedOwner(sol.address))
|
|
47035
|
+
continue;
|
|
47036
|
+
const balance = tokenBalanceOf(postState.get(sol.address) ?? null);
|
|
47037
|
+
if (balance !== undefined && allowedOwner(balance.owner))
|
|
47038
|
+
continue;
|
|
47039
|
+
return refuse2(`${sol.address} would receive ${sol.after - sol.before} lamports`);
|
|
47040
|
+
}
|
|
47041
|
+
const nftSnapshot = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
|
|
47042
|
+
if (nftSnapshot === undefined)
|
|
47043
|
+
return refuse2(`the position NFT account ${nftAccount} is not written by this transaction`);
|
|
47044
|
+
const nftAfter = tokenBalanceOf(nftSnapshot.after);
|
|
47045
|
+
if (nftAfter !== undefined && nftAfter.amount !== 0n)
|
|
47046
|
+
return refuse2(`the position NFT account ${nftAccount} still holds ${nftAfter.amount} after the simulation`);
|
|
47047
|
+
for (const { address, before, after } of simulation.snapshots) {
|
|
47048
|
+
if (allowedOwner(address)) {
|
|
47049
|
+
if (after === null || after.owner !== SYSTEM_PROGRAM_ID || after.data.length !== 0)
|
|
47050
|
+
return refuse2(`${address === tee ? "the TEE wallet" : "the vault"} would no longer be a plain System account after this transaction`);
|
|
47051
|
+
}
|
|
47052
|
+
if (after === null)
|
|
47053
|
+
continue;
|
|
47054
|
+
if (before !== null && after.owner !== before.owner)
|
|
47055
|
+
return refuse2(`writable account ${address} would change owner from ${before.owner} to ${after.owner}`);
|
|
47056
|
+
if (before !== null) {
|
|
47057
|
+
const controlled = tokenBalanceOf(before);
|
|
47058
|
+
if (controlled !== undefined && allowedOwner(controlled.owner)) {
|
|
47059
|
+
if (after.data.length < TOKEN_ACCOUNT_SIZE2)
|
|
47060
|
+
return refuse2(`token account ${address} would no longer decode as a token account`);
|
|
47061
|
+
for (const [start, end, what] of CONTROL_RANGES) {
|
|
47062
|
+
if (!bytesEqual2(before.data.subarray(start, end), after.data.subarray(start, end)))
|
|
47063
|
+
return refuse2(`token account ${address} would change its ${what}, which a close never does`);
|
|
47064
|
+
}
|
|
47065
|
+
continue;
|
|
47066
|
+
}
|
|
47067
|
+
}
|
|
47068
|
+
const created = before === null ? tokenBalanceOf(after) : undefined;
|
|
47069
|
+
if (created !== undefined) {
|
|
47070
|
+
if (!allowedOwner(created.owner))
|
|
47071
|
+
return refuse2(`created token account ${address} would be controlled by ${created.owner}`);
|
|
47072
|
+
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)))
|
|
47073
|
+
return refuse2(`created token account ${address} would start with a delegate, a close authority, or a frozen state`);
|
|
47074
|
+
}
|
|
47075
|
+
}
|
|
47076
|
+
const infos = new Map;
|
|
47077
|
+
const infoOf = async (address) => {
|
|
47078
|
+
if (!infos.has(address))
|
|
47079
|
+
infos.set(address, await rpc2.getAccountInfo(address));
|
|
47080
|
+
return infos.get(address) ?? null;
|
|
47081
|
+
};
|
|
47082
|
+
const canonical = new Set;
|
|
47083
|
+
for (const key of compiled.keys) {
|
|
47084
|
+
const view = await infoOf(key);
|
|
47085
|
+
if (!isMintAccount(view) || view === null)
|
|
47086
|
+
continue;
|
|
47087
|
+
const mint = decodePubkey(key);
|
|
47088
|
+
const program = decodePubkey(view.owner);
|
|
47089
|
+
canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(tee), mint, program)));
|
|
47090
|
+
canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(vault), mint, program)));
|
|
47091
|
+
}
|
|
47092
|
+
for (const [i, key] of compiled.keys.entries()) {
|
|
47093
|
+
if (!compiled.isWritable[i])
|
|
47094
|
+
continue;
|
|
47095
|
+
const view = await infoOf(key);
|
|
47096
|
+
if (view === null) {
|
|
47097
|
+
if (canonical.has(key))
|
|
47098
|
+
continue;
|
|
47099
|
+
return refuse2(`writable account ${key} does not exist and is not a TEE or vault associated token account for a mint this transaction names`);
|
|
47100
|
+
}
|
|
47101
|
+
if (isTokenProgram(view.owner)) {
|
|
47102
|
+
if (key === nftMint)
|
|
47103
|
+
continue;
|
|
47104
|
+
const balance = tokenBalanceOf(view);
|
|
47105
|
+
if (balance === undefined)
|
|
47106
|
+
return refuse2(`writable account ${key} is owned by a token program but is not a token account`);
|
|
47107
|
+
if (allowedOwner(balance.owner) || key === nftAccount)
|
|
47108
|
+
continue;
|
|
47109
|
+
const authority = await infoOf(balance.owner);
|
|
47110
|
+
if (authority !== null && authority.owner === DAMM_V2_PROGRAM_ID)
|
|
47111
|
+
continue;
|
|
47112
|
+
return refuse2(`writable token account ${key} is controlled by ${balance.owner}, which is neither this wallet, its vault, nor a DAMM v2 pool`);
|
|
47113
|
+
}
|
|
47114
|
+
if (view.owner === SYSTEM_PROGRAM_ID) {
|
|
47115
|
+
if (allowedOwner(key))
|
|
47116
|
+
continue;
|
|
47117
|
+
return refuse2(`writable system account ${key} is neither the TEE wallet nor its vault`);
|
|
47118
|
+
}
|
|
47119
|
+
if (view.owner !== DAMM_V2_PROGRAM_ID)
|
|
47120
|
+
return refuse2(`writable account ${key} is owned by ${view.owner}, not DAMM v2`);
|
|
47121
|
+
}
|
|
47122
|
+
return { ok: true, tx, compiled, simulation };
|
|
47123
|
+
}
|
|
47124
|
+
function formatSol2(lamports) {
|
|
47125
|
+
const negative = lamports < 0n;
|
|
47126
|
+
const abs = negative ? -lamports : lamports;
|
|
47127
|
+
const whole = abs / 1000000000n;
|
|
47128
|
+
const frac = (abs % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
|
|
47129
|
+
return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""} SOL`;
|
|
47130
|
+
}
|
|
47131
|
+
function describeClose(input) {
|
|
47132
|
+
const { verdict, tee, vault, nftMint, nftAccount } = input;
|
|
47133
|
+
const { tx, compiled, simulation } = verdict;
|
|
47134
|
+
const lines = [];
|
|
47135
|
+
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}`);
|
|
47136
|
+
lines.push(`fee payer ${compiled.keys[0]} (this TEE wallet)`);
|
|
47137
|
+
lines.push(`position NFT mint ${nftMint}, account ${nftAccount}`);
|
|
47138
|
+
const programs = [...new Set(tx.message.instructions.map((ix) => compiled.keys[ix.programIdIndex] ?? "?"))];
|
|
47139
|
+
for (const program of programs)
|
|
47140
|
+
lines.push(`program ${program === DAMM_V2_PROGRAM_ID ? `Meteora DAMM v2 (${program})` : programNameOf(program)}`);
|
|
47141
|
+
const deltas = computeDeltas(simulation.snapshots);
|
|
47142
|
+
const who = (address) => address === tee ? "wallet" : address === vault ? "vault" : address;
|
|
47143
|
+
for (const address of [tee, vault]) {
|
|
47144
|
+
const sol = deltas.sol.find((delta) => delta.address === address);
|
|
47145
|
+
if (sol && sol.after !== sol.before)
|
|
47146
|
+
lines.push(`${who(address).padEnd(11)} ${formatSol2(sol.before)} -> ${formatSol2(sol.after)} (${sol.after >= sol.before ? "+" : ""}${formatSol2(sol.after - sol.before)})`);
|
|
47147
|
+
for (const token of deltas.tokens.filter((delta) => delta.owner === address && delta.after !== delta.before)) {
|
|
47148
|
+
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)" : ""}`);
|
|
47149
|
+
}
|
|
47150
|
+
}
|
|
47151
|
+
const nft = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
|
|
47152
|
+
if (nft && nft.after === null)
|
|
47153
|
+
lines.push(`position NFT account ${nftAccount} is closed by this transaction`);
|
|
47154
|
+
const others = deltas.tokens.filter((token) => token.owner !== tee && token.owner !== vault && token.after !== token.before);
|
|
47155
|
+
for (const token of others)
|
|
47156
|
+
lines.push(`${token.owner === undefined ? "?" : "pool"} ${token.mint}: ${token.before} -> ${token.after} raw in ${token.account} (authority ${token.owner})`);
|
|
47157
|
+
lines.push("checked ordered keys, programs, simulation, positive deltas, control, writable accounts: all passed");
|
|
47158
|
+
lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
|
|
47159
|
+
return lines;
|
|
47160
|
+
}
|
|
47161
|
+
|
|
47162
|
+
// src/commands/tee.ts
|
|
45692
47163
|
init_profiles();
|
|
45693
47164
|
init_render();
|
|
45694
47165
|
init_solana_lite();
|
|
@@ -49429,8 +50900,8 @@ function readDisableOutcome(body) {
|
|
|
49429
50900
|
var MIN_PASSPHRASE_LENGTH = 12;
|
|
49430
50901
|
var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
49431
50902
|
var RPC_URL_ENV2 = "CANDLE_SOLANA_RPC_URL";
|
|
49432
|
-
var
|
|
49433
|
-
var
|
|
50903
|
+
var CONFIRM_POLL_MS3 = 2000;
|
|
50904
|
+
var CONFIRM_MAX_POLLS3 = 45;
|
|
49434
50905
|
function refuseEnvPassphrase2(ctx) {
|
|
49435
50906
|
if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
|
|
49436
50907
|
return true;
|
|
@@ -49441,7 +50912,7 @@ function refuseEnvPassphrase2(ctx) {
|
|
|
49441
50912
|
}, ctx.json);
|
|
49442
50913
|
return false;
|
|
49443
50914
|
}
|
|
49444
|
-
function
|
|
50915
|
+
function usage4(ctx, line) {
|
|
49445
50916
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
49446
50917
|
return 2;
|
|
49447
50918
|
}
|
|
@@ -49661,7 +51132,7 @@ async function openExistingTeeStore(ctx, parsed) {
|
|
|
49661
51132
|
const { deps, json } = ctx;
|
|
49662
51133
|
const found = await readTeeStore(ctx, parsed);
|
|
49663
51134
|
if ("usage" in found)
|
|
49664
|
-
return { ok: false, code:
|
|
51135
|
+
return { ok: false, code: usage4(ctx, found.usage) };
|
|
49665
51136
|
const { path } = found;
|
|
49666
51137
|
if ("error" in found) {
|
|
49667
51138
|
writeLocalFailure(deps, {
|
|
@@ -49758,12 +51229,12 @@ async function teeNew(args, ctx) {
|
|
|
49758
51229
|
return 1;
|
|
49759
51230
|
const parsed = parseArgs(args, { valueFlags: ["--label", "--keystore"], pathFlags: ["--keystore"] });
|
|
49760
51231
|
if ("error" in parsed)
|
|
49761
|
-
return
|
|
51232
|
+
return usage4(ctx, parsed.error);
|
|
49762
51233
|
if (parsed.positionals.length > 0)
|
|
49763
|
-
return
|
|
51234
|
+
return usage4(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
49764
51235
|
const found = await readTeeStore(ctx, parsed);
|
|
49765
51236
|
if ("usage" in found)
|
|
49766
|
-
return
|
|
51237
|
+
return usage4(ctx, found.usage);
|
|
49767
51238
|
const { path } = found;
|
|
49768
51239
|
if ("error" in found) {
|
|
49769
51240
|
writeLocalFailure(deps, {
|
|
@@ -49840,15 +51311,15 @@ async function teeEnable(args, ctx) {
|
|
|
49840
51311
|
pathFlags: ["--keystore"]
|
|
49841
51312
|
});
|
|
49842
51313
|
if ("error" in parsed)
|
|
49843
|
-
return
|
|
51314
|
+
return usage4(ctx, parsed.error);
|
|
49844
51315
|
const [address, extra] = parsed.positionals;
|
|
49845
51316
|
if (!address || extra !== undefined) {
|
|
49846
|
-
return
|
|
51317
|
+
return usage4(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
|
|
49847
51318
|
}
|
|
49848
51319
|
const vaultFlag = parsed.values["--vault"];
|
|
49849
51320
|
const vaultKey = parsed.values["--vault-key"];
|
|
49850
51321
|
if (vaultFlag !== undefined && vaultKey !== undefined) {
|
|
49851
|
-
return
|
|
51322
|
+
return usage4(ctx, "Use either --vault or --vault-key, not both.");
|
|
49852
51323
|
}
|
|
49853
51324
|
let vault = vaultFlag;
|
|
49854
51325
|
if (vaultKey !== undefined) {
|
|
@@ -49858,7 +51329,7 @@ async function teeEnable(args, ctx) {
|
|
|
49858
51329
|
const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
|
|
49859
51330
|
const resolvedVault = vaultPathFor2(ctx, parsed);
|
|
49860
51331
|
if ("error" in resolvedVault)
|
|
49861
|
-
return
|
|
51332
|
+
return usage4(ctx, resolvedVault.error);
|
|
49862
51333
|
const vaultPath = resolvedVault.path;
|
|
49863
51334
|
const raw = await readVaultRaw2(vaultPath);
|
|
49864
51335
|
if (raw === null) {
|
|
@@ -49896,11 +51367,11 @@ async function teeEnable(args, ctx) {
|
|
|
49896
51367
|
}
|
|
49897
51368
|
}
|
|
49898
51369
|
if (!vault)
|
|
49899
|
-
return
|
|
51370
|
+
return usage4(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
|
|
49900
51371
|
if (!isSolanaAddress(vault))
|
|
49901
|
-
return
|
|
51372
|
+
return usage4(ctx, "--vault is not a valid Solana address.");
|
|
49902
51373
|
if (vault === address)
|
|
49903
|
-
return
|
|
51374
|
+
return usage4(ctx, "--vault must be a different address from the TEE wallet.");
|
|
49904
51375
|
const vaultOwned = await addressOwnedByVault(ctx, address);
|
|
49905
51376
|
if (vaultOwned === "usage")
|
|
49906
51377
|
return 2;
|
|
@@ -50037,20 +51508,20 @@ async function teeFund(args, ctx) {
|
|
|
50037
51508
|
return 1;
|
|
50038
51509
|
const parsed = parseArgs(args, { valueFlags: ["--amount", "--asset", "--keystore"], pathFlags: ["--keystore"] });
|
|
50039
51510
|
if ("error" in parsed)
|
|
50040
|
-
return
|
|
51511
|
+
return usage4(ctx, parsed.error);
|
|
50041
51512
|
const [address, extra] = parsed.positionals;
|
|
50042
51513
|
if (!address || extra !== undefined)
|
|
50043
|
-
return
|
|
51514
|
+
return usage4(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
|
|
50044
51515
|
const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
|
|
50045
51516
|
if (asset !== "SOL" && asset !== "USDC")
|
|
50046
|
-
return
|
|
51517
|
+
return usage4(ctx, "--asset must be SOL or USDC.");
|
|
50047
51518
|
const amount = parsed.values["--amount"];
|
|
50048
51519
|
if (!amount)
|
|
50049
|
-
return
|
|
51520
|
+
return usage4(ctx, "--amount <n> is required.");
|
|
50050
51521
|
const decimals = asset === "SOL" ? 9 : 6;
|
|
50051
51522
|
const raw = decimalToRaw(amount, decimals);
|
|
50052
51523
|
if (raw === null || raw === 0n)
|
|
50053
|
-
return
|
|
51524
|
+
return usage4(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
|
|
50054
51525
|
const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
|
|
50055
51526
|
if (!resolved.ok)
|
|
50056
51527
|
return resolved.code;
|
|
@@ -50117,10 +51588,10 @@ async function teeStatus(args, ctx) {
|
|
|
50117
51588
|
return 1;
|
|
50118
51589
|
const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
|
|
50119
51590
|
if ("error" in parsed)
|
|
50120
|
-
return
|
|
51591
|
+
return usage4(ctx, parsed.error);
|
|
50121
51592
|
const [address, extra] = parsed.positionals;
|
|
50122
51593
|
if (!address || extra !== undefined)
|
|
50123
|
-
return
|
|
51594
|
+
return usage4(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
|
|
50124
51595
|
const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
|
|
50125
51596
|
if (!resolved.ok)
|
|
50126
51597
|
return resolved.code;
|
|
@@ -50162,7 +51633,7 @@ async function teeStatus(args, ctx) {
|
|
|
50162
51633
|
if (rpcUrl2) {
|
|
50163
51634
|
const checked = rpcUrlFrom2(ctx, parsed);
|
|
50164
51635
|
if (typeof checked !== "string")
|
|
50165
|
-
return
|
|
51636
|
+
return usage4(ctx, checked.error);
|
|
50166
51637
|
const rpc2 = createSolanaRpc(checked, deps.fetch);
|
|
50167
51638
|
try {
|
|
50168
51639
|
const lamports = await rpc2.getBalance(address);
|
|
@@ -50243,10 +51714,10 @@ async function teeDisable(args, ctx) {
|
|
|
50243
51714
|
return 1;
|
|
50244
51715
|
const parsed = parseArgs(args, { valueFlags: ["--keystore"], pathFlags: ["--keystore"] });
|
|
50245
51716
|
if ("error" in parsed)
|
|
50246
|
-
return
|
|
51717
|
+
return usage4(ctx, parsed.error);
|
|
50247
51718
|
const [address, extra] = parsed.positionals;
|
|
50248
51719
|
if (!address || extra !== undefined)
|
|
50249
|
-
return
|
|
51720
|
+
return usage4(ctx, "Usage: candle tee disable <address>");
|
|
50250
51721
|
await printIdentity(ctx);
|
|
50251
51722
|
const openedActive = await openActiveTee(ctx, parsed, address, "read");
|
|
50252
51723
|
if (!openedActive.ok)
|
|
@@ -50341,6 +51812,9 @@ function refusalState(raw) {
|
|
|
50341
51812
|
async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions, pending, recordPending, clearPending) {
|
|
50342
51813
|
const blockhash = await rpc2.getLatestBlockhash();
|
|
50343
51814
|
const message = compileLegacyMessage({ feePayer, recentBlockhash: blockhash, instructions });
|
|
51815
|
+
return broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending);
|
|
51816
|
+
}
|
|
51817
|
+
async function broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending) {
|
|
50344
51818
|
const signatureBytes = signMessage(message, secret);
|
|
50345
51819
|
const signature = base58.encode(signatureBytes);
|
|
50346
51820
|
const wire = serializeSignedTransaction(message, signatureBytes);
|
|
@@ -50366,7 +51840,7 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
|
|
|
50366
51840
|
error: `send did not answer cleanly (${error instanceof Error ? error.message : error}); it may still land`
|
|
50367
51841
|
};
|
|
50368
51842
|
}
|
|
50369
|
-
for (let i = 0;i <
|
|
51843
|
+
for (let i = 0;i < CONFIRM_MAX_POLLS3; i++) {
|
|
50370
51844
|
let status;
|
|
50371
51845
|
try {
|
|
50372
51846
|
status = await rpc2.getSignatureStatus(signature);
|
|
@@ -50388,12 +51862,12 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
|
|
|
50388
51862
|
error: `transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}`
|
|
50389
51863
|
};
|
|
50390
51864
|
}
|
|
50391
|
-
await deps.sleep(
|
|
51865
|
+
await deps.sleep(CONFIRM_POLL_MS3);
|
|
50392
51866
|
}
|
|
50393
51867
|
return {
|
|
50394
51868
|
status: "uncertain",
|
|
50395
51869
|
signature,
|
|
50396
|
-
error: `transaction ${signature} was not finalized within ${
|
|
51870
|
+
error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS3 * CONFIRM_POLL_MS3 / 1000}s; it may still land${echoNote}`
|
|
50397
51871
|
};
|
|
50398
51872
|
}
|
|
50399
51873
|
async function teeSweep(args, ctx) {
|
|
@@ -50406,13 +51880,13 @@ async function teeSweep(args, ctx) {
|
|
|
50406
51880
|
pathFlags: ["--keystore"]
|
|
50407
51881
|
});
|
|
50408
51882
|
if ("error" in parsed)
|
|
50409
|
-
return
|
|
51883
|
+
return usage4(ctx, parsed.error);
|
|
50410
51884
|
const [address, extra] = parsed.positionals;
|
|
50411
51885
|
if (!address || extra !== undefined)
|
|
50412
|
-
return
|
|
51886
|
+
return usage4(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
|
|
50413
51887
|
const rpcUrl2 = rpcUrlFrom2(ctx, parsed);
|
|
50414
51888
|
if (typeof rpcUrl2 !== "string")
|
|
50415
|
-
return
|
|
51889
|
+
return usage4(ctx, rpcUrl2.error);
|
|
50416
51890
|
const emergency = parsed.booleans.has("--emergency");
|
|
50417
51891
|
const openedActive = await openActiveTee(ctx, parsed, address, "sign");
|
|
50418
51892
|
if (!openedActive.ok)
|
|
@@ -50615,6 +52089,116 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50615
52089
|
pendingStill.push(p);
|
|
50616
52090
|
signingBlocked = true;
|
|
50617
52091
|
}
|
|
52092
|
+
const candidateAccounts = new Set;
|
|
52093
|
+
const lpCloses = [];
|
|
52094
|
+
{
|
|
52095
|
+
let token2022Accounts = [];
|
|
52096
|
+
try {
|
|
52097
|
+
token2022Accounts = await rpc2.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID);
|
|
52098
|
+
} catch (error) {
|
|
52099
|
+
residuals.push({
|
|
52100
|
+
kind: "inventory",
|
|
52101
|
+
detail: `could not list Token-2022 accounts for position discovery: ${error instanceof Error ? error.message : error}`
|
|
52102
|
+
});
|
|
52103
|
+
}
|
|
52104
|
+
const candidates = token2022Accounts.filter(isPositionCandidate);
|
|
52105
|
+
for (const acct of candidates)
|
|
52106
|
+
candidateAccounts.add(acct.pubkey);
|
|
52107
|
+
const leftover = (kind, acct, detail) => residuals.push({ kind, detail, mint: acct.mint, account: acct.pubkey, amountRaw: acct.amountRaw });
|
|
52108
|
+
if (!emergency && candidates.length > 0) {
|
|
52109
|
+
if (!json)
|
|
52110
|
+
deps.stdout.write(` ${candidates.length} position NFT candidate(s) (Token-2022, amount 1, decimals 0)
|
|
52111
|
+
`);
|
|
52112
|
+
const verified = [];
|
|
52113
|
+
for (const acct of candidates) {
|
|
52114
|
+
if (signingBlocked)
|
|
52115
|
+
break;
|
|
52116
|
+
if (!apiKey || !entry.linkedWalletId) {
|
|
52117
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "no bound API key is available to build the close; the NFT was left in place");
|
|
52118
|
+
continue;
|
|
52119
|
+
}
|
|
52120
|
+
const result = await apiRequest(`/api/v1/agent/lp/positions/${encodeURIComponent(acct.mint)}/close-build`, {
|
|
52121
|
+
method: "POST",
|
|
52122
|
+
auth: "key",
|
|
52123
|
+
credentials: { apiKey },
|
|
52124
|
+
apiUrl,
|
|
52125
|
+
fetch: deps.fetch,
|
|
52126
|
+
env: deps.env,
|
|
52127
|
+
body: {}
|
|
52128
|
+
});
|
|
52129
|
+
if (!result.ok) {
|
|
52130
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `close-build ${result.code ?? `HTTP ${result.status}`}: ${result.message}; the NFT was left in place`);
|
|
52131
|
+
continue;
|
|
52132
|
+
}
|
|
52133
|
+
const artifact = parseCloseArtifact(result.body);
|
|
52134
|
+
if (artifact === undefined) {
|
|
52135
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "close-build answered without a transaction and an ordered account key array; the NFT was left in place");
|
|
52136
|
+
continue;
|
|
52137
|
+
}
|
|
52138
|
+
const verdict = await verifyCloseArtifact({
|
|
52139
|
+
artifact,
|
|
52140
|
+
rpc: rpc2,
|
|
52141
|
+
tee: address,
|
|
52142
|
+
vault,
|
|
52143
|
+
nftMint: acct.mint,
|
|
52144
|
+
nftAccount: acct.pubkey
|
|
52145
|
+
});
|
|
52146
|
+
if (!verdict.ok) {
|
|
52147
|
+
leftover(DAMM_CLOSE_TRANSACTION_REFUSED, acct, `${verdict.reason}; refused before signing`);
|
|
52148
|
+
continue;
|
|
52149
|
+
}
|
|
52150
|
+
verified.push({ acct, verdict });
|
|
52151
|
+
}
|
|
52152
|
+
for (const { acct, verdict } of verified) {
|
|
52153
|
+
if (signingBlocked) {
|
|
52154
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "not signed: an earlier transaction is still in flight");
|
|
52155
|
+
continue;
|
|
52156
|
+
}
|
|
52157
|
+
let blockhashValid;
|
|
52158
|
+
try {
|
|
52159
|
+
blockhashValid = await rpc2.isBlockhashValid(verdict.tx.message.recentBlockhash, "confirmed");
|
|
52160
|
+
} catch (error) {
|
|
52161
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `could not check the close build's blockhash: ${error instanceof Error ? error.message : error}; not signed`);
|
|
52162
|
+
continue;
|
|
52163
|
+
}
|
|
52164
|
+
if (!blockhashValid) {
|
|
52165
|
+
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "the close build's blockhash has expired; not signed, re-run the sweep for a fresh close");
|
|
52166
|
+
continue;
|
|
52167
|
+
}
|
|
52168
|
+
const display = describeClose({ verdict, tee: address, vault, nftMint: acct.mint, nftAccount: acct.pubkey });
|
|
52169
|
+
lpCloses.push({ mint: acct.mint, account: acct.pubkey, display });
|
|
52170
|
+
if (!json) {
|
|
52171
|
+
deps.stdout.write(` Close for position ${acct.mint} (verified, unsigned; about to sign):
|
|
52172
|
+
`);
|
|
52173
|
+
for (const line of display)
|
|
52174
|
+
deps.stdout.write(` ${line}
|
|
52175
|
+
`);
|
|
52176
|
+
}
|
|
52177
|
+
const pending = {
|
|
52178
|
+
kind: "lp-close",
|
|
52179
|
+
mint: acct.mint,
|
|
52180
|
+
account: acct.pubkey,
|
|
52181
|
+
amountRaw: acct.amountRaw
|
|
52182
|
+
};
|
|
52183
|
+
const outcome = await broadcastMessage(rpc2, deps, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending);
|
|
52184
|
+
if (outcome.status !== "finalized") {
|
|
52185
|
+
if (!settle2(outcome, pending, "lp-close"))
|
|
52186
|
+
signingBlocked = true;
|
|
52187
|
+
continue;
|
|
52188
|
+
}
|
|
52189
|
+
await retainReceipt({
|
|
52190
|
+
kind: "lp-close",
|
|
52191
|
+
mint: acct.mint,
|
|
52192
|
+
amountRaw: acct.amountRaw,
|
|
52193
|
+
signature: outcome.signature,
|
|
52194
|
+
finalizedAt: new Date(deps.now()).toISOString()
|
|
52195
|
+
});
|
|
52196
|
+
if (!json)
|
|
52197
|
+
deps.stdout.write(` closed LP position ${acct.mint} and withdrew its liquidity: ${outcome.signature}
|
|
52198
|
+
`);
|
|
52199
|
+
}
|
|
52200
|
+
}
|
|
52201
|
+
}
|
|
50618
52202
|
const tokenAccounts = [];
|
|
50619
52203
|
for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
|
|
50620
52204
|
try {
|
|
@@ -50631,6 +52215,13 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50631
52215
|
if (signingBlocked)
|
|
50632
52216
|
break;
|
|
50633
52217
|
const token2022 = acct.programId === TOKEN_2022_PROGRAM_ID;
|
|
52218
|
+
if (candidateAccounts.has(acct.pubkey)) {
|
|
52219
|
+
if (!emergency)
|
|
52220
|
+
continue;
|
|
52221
|
+
if (!json)
|
|
52222
|
+
deps.stdout.write(` ${acct.mint}: position NFT moved as-is to the vault (emergency; unwind it from a fresh TEE wallet)
|
|
52223
|
+
`);
|
|
52224
|
+
}
|
|
50634
52225
|
if (acct.state !== "initialized") {
|
|
50635
52226
|
residuals.push({
|
|
50636
52227
|
kind: token2022 && acct.state === "frozen" ? "TOKEN_2022_FROZEN" : "frozen-or-uninitialized",
|
|
@@ -50885,7 +52476,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
50885
52476
|
signature: p.signature
|
|
50886
52477
|
})),
|
|
50887
52478
|
inventory,
|
|
50888
|
-
recordedOnServer
|
|
52479
|
+
recordedOnServer,
|
|
52480
|
+
...lpCloses.length > 0 ? { lpCloses } : {}
|
|
50889
52481
|
})}
|
|
50890
52482
|
`);
|
|
50891
52483
|
return finalState === "swept" ? 0 : 3;
|
|
@@ -50968,8 +52560,7 @@ function launchWarning(keyPrefix, toKey, rows) {
|
|
|
50968
52560
|
return `Warning: key ${keyPrefix} lacks launch:write; ${launchers.join(", ")} ${launchers.length === 1 ? "has" : "have"} allowLaunch but cannot launch under it.`;
|
|
50969
52561
|
}
|
|
50970
52562
|
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;
|
|
52563
|
+
function rebindFailureDetails(result, context) {
|
|
50973
52564
|
let code = result.code;
|
|
50974
52565
|
let message = result.message;
|
|
50975
52566
|
let suggestion;
|
|
@@ -50989,31 +52580,61 @@ function writeRebindFailure(ctx, result, context) {
|
|
|
50989
52580
|
} else if (result.code === "REBIND_STALE") {
|
|
50990
52581
|
suggestion = "Run the command again; the preview will show the current binding.";
|
|
50991
52582
|
}
|
|
52583
|
+
return {
|
|
52584
|
+
code: code ?? `HTTP ${result.status}`,
|
|
52585
|
+
message,
|
|
52586
|
+
...suggestion !== undefined ? { suggestion } : {},
|
|
52587
|
+
status: result.status
|
|
52588
|
+
};
|
|
52589
|
+
}
|
|
52590
|
+
function writeRebindFailure(ctx, result, context) {
|
|
52591
|
+
const { deps, apiUrl, json } = ctx;
|
|
52592
|
+
const { code, message, suggestion } = rebindFailureDetails(result, context);
|
|
50992
52593
|
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
|
|
50993
52594
|
if (json) {
|
|
50994
52595
|
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
50995
52596
|
`);
|
|
50996
52597
|
} else {
|
|
50997
|
-
deps.stderr.write(`${code
|
|
52598
|
+
deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
50998
52599
|
`);
|
|
50999
52600
|
}
|
|
51000
52601
|
return 1;
|
|
51001
52602
|
}
|
|
51002
|
-
async function
|
|
51003
|
-
const { deps, apiUrl
|
|
51004
|
-
|
|
51005
|
-
return { ok: true, keyPrefix: raw };
|
|
51006
|
-
const result = await apiRequest(KEYS_PATH2, {
|
|
52603
|
+
async function listAccountKeys(ctx, deviceToken) {
|
|
52604
|
+
const { deps, apiUrl } = ctx;
|
|
52605
|
+
return apiRequest(KEYS_PATH2, {
|
|
51007
52606
|
auth: "device",
|
|
51008
52607
|
credentials: { deviceToken },
|
|
51009
52608
|
apiUrl,
|
|
51010
52609
|
fetch: deps.fetch,
|
|
51011
52610
|
env: deps.env
|
|
51012
52611
|
});
|
|
51013
|
-
|
|
51014
|
-
|
|
52612
|
+
}
|
|
52613
|
+
async function postRebind(ctx, deviceToken, body) {
|
|
52614
|
+
const { deps, apiUrl } = ctx;
|
|
52615
|
+
return apiRequest(REBIND_PATH, {
|
|
52616
|
+
method: "POST",
|
|
52617
|
+
body,
|
|
52618
|
+
auth: "device",
|
|
52619
|
+
credentials: { deviceToken },
|
|
52620
|
+
apiUrl,
|
|
52621
|
+
fetch: deps.fetch,
|
|
52622
|
+
env: deps.env
|
|
52623
|
+
});
|
|
52624
|
+
}
|
|
52625
|
+
async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
|
|
52626
|
+
const { deps, json } = ctx;
|
|
52627
|
+
if (KEY_PREFIX_RE.test(raw))
|
|
52628
|
+
return { ok: true, keyPrefix: raw };
|
|
52629
|
+
let listed = opts.keys;
|
|
52630
|
+
if (listed === undefined) {
|
|
52631
|
+
const result = await listAccountKeys(ctx, deviceToken);
|
|
52632
|
+
if (!result.ok) {
|
|
52633
|
+
return { ok: false, code: writeRebindFailure(ctx, result, {}) };
|
|
52634
|
+
}
|
|
52635
|
+
listed = result.body?.keys ?? [];
|
|
51015
52636
|
}
|
|
51016
|
-
const keys =
|
|
52637
|
+
const keys = listed.filter((key) => !key.revokedAt);
|
|
51017
52638
|
const matches = keys.filter((key) => key.label === raw);
|
|
51018
52639
|
if (matches.length === 1)
|
|
51019
52640
|
return { ok: true, keyPrefix: matches[0].keyPrefix };
|
|
@@ -51071,15 +52692,7 @@ async function teeRebind(args, ctx) {
|
|
|
51071
52692
|
const target = await resolveTargetKey(ctx, deviceToken, toKey);
|
|
51072
52693
|
if (!target.ok)
|
|
51073
52694
|
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
|
-
});
|
|
52695
|
+
const call = (body) => postRebind(ctx, deviceToken, body);
|
|
51083
52696
|
const preview = await call({
|
|
51084
52697
|
dryRun: true,
|
|
51085
52698
|
toKeyPrefix: target.keyPrefix,
|
|
@@ -53960,9 +55573,18 @@ function decimalToRaw2(decimal, decimals) {
|
|
|
53960
55573
|
}
|
|
53961
55574
|
function assertVaultSigner(entry) {
|
|
53962
55575
|
if (entry.role !== "vault") {
|
|
53963
|
-
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is a
|
|
55576
|
+
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).`, {
|
|
55577
|
+
suggestion: `To move funds out of a promoted wallet, use: candle vault transfer <to> --from ${entry.label ?? entry.address}`
|
|
55578
|
+
});
|
|
53964
55579
|
}
|
|
53965
55580
|
}
|
|
55581
|
+
function assertTransferSigner(entry) {
|
|
55582
|
+
if (entry.role === "vault" || entry.role === "tee-wallet")
|
|
55583
|
+
return;
|
|
55584
|
+
throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is an external wallet and cannot sign vault transfer shapes (ED-10).`, {
|
|
55585
|
+
suggestion: `Move funds out of an external wallet with: candle external sweep ${entry.label} --to <vault> --rpc-url <url>`
|
|
55586
|
+
});
|
|
55587
|
+
}
|
|
53966
55588
|
function namedSendFailure(plan) {
|
|
53967
55589
|
if (!plan.token)
|
|
53968
55590
|
return;
|
|
@@ -54707,9 +56329,9 @@ init_render();
|
|
|
54707
56329
|
init_solana_lite();
|
|
54708
56330
|
init_store();
|
|
54709
56331
|
init_vault_support();
|
|
54710
|
-
var
|
|
56332
|
+
var CHUNK2 = 100;
|
|
54711
56333
|
var LAMPORTS_PER_SOL = 1000000000n;
|
|
54712
|
-
function
|
|
56334
|
+
function formatSol3(lamports) {
|
|
54713
56335
|
const whole = lamports / LAMPORTS_PER_SOL;
|
|
54714
56336
|
const fraction = (lamports % LAMPORTS_PER_SOL).toString().padStart(9, "0").replace(/0+$/, "");
|
|
54715
56337
|
return fraction === "" ? whole.toString() : `${whole}.${fraction}`;
|
|
@@ -54719,8 +56341,8 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
|
|
|
54719
56341
|
const lamports = new Map;
|
|
54720
56342
|
const unavailable = [];
|
|
54721
56343
|
let failure;
|
|
54722
|
-
for (let at = 0;at < addresses.length; at +=
|
|
54723
|
-
const chunk = addresses.slice(at, at +
|
|
56344
|
+
for (let at = 0;at < addresses.length; at += CHUNK2) {
|
|
56345
|
+
const chunk = addresses.slice(at, at + CHUNK2);
|
|
54724
56346
|
try {
|
|
54725
56347
|
const accounts = await rpc2.getMultipleAccounts(chunk);
|
|
54726
56348
|
for (const [i, address] of chunk.entries())
|
|
@@ -54771,7 +56393,7 @@ async function vaultList(args, ctx) {
|
|
|
54771
56393
|
const all = vault.index.entries;
|
|
54772
56394
|
const matched = filter === undefined ? all : all.filter((entry) => matches(entry, filter));
|
|
54773
56395
|
const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
|
|
54774
|
-
const requests = Math.ceil(solana.length /
|
|
56396
|
+
const requests = Math.ceil(solana.length / CHUNK2);
|
|
54775
56397
|
const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
|
|
54776
56398
|
if (balances && solana.length > 0 && rpcHost !== undefined) {
|
|
54777
56399
|
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 +56455,7 @@ async function vaultList(args, ctx) {
|
|
|
54833
56455
|
const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
|
|
54834
56456
|
if (balances) {
|
|
54835
56457
|
const held = lamports.get(entry.address);
|
|
54836
|
-
row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" :
|
|
56458
|
+
row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
|
|
54837
56459
|
}
|
|
54838
56460
|
return row;
|
|
54839
56461
|
});
|
|
@@ -54842,9 +56464,9 @@ ${renderTable(headers, rows)}
|
|
|
54842
56464
|
`);
|
|
54843
56465
|
if (balances) {
|
|
54844
56466
|
deps.stdout.write(complete ? `
|
|
54845
|
-
total ${
|
|
56467
|
+
total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
|
|
54846
56468
|
` : `
|
|
54847
|
-
total ${
|
|
56469
|
+
total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
|
|
54848
56470
|
`);
|
|
54849
56471
|
}
|
|
54850
56472
|
return complete ? 0 : 3;
|
|
@@ -54870,6 +56492,7 @@ async function vaultPhrase(args, ctx) {
|
|
|
54870
56492
|
init_esm();
|
|
54871
56493
|
init_args();
|
|
54872
56494
|
init_deps();
|
|
56495
|
+
init_profiles();
|
|
54873
56496
|
init_render();
|
|
54874
56497
|
init_solana_lite();
|
|
54875
56498
|
|
|
@@ -54976,12 +56599,299 @@ function restoreRefusedFailure(init, error) {
|
|
|
54976
56599
|
// src/commands/vault-promote.ts
|
|
54977
56600
|
init_errors();
|
|
54978
56601
|
init_promote_support();
|
|
56602
|
+
// src/vault/promote-to-key.ts
|
|
56603
|
+
init_deps();
|
|
56604
|
+
init_render();
|
|
56605
|
+
var REBIND_CHUNK = 200;
|
|
56606
|
+
var SELECTED_SCOPE_LIMIT = 50;
|
|
56607
|
+
function targetKeyRefusal(row, keyPrefix, now) {
|
|
56608
|
+
const written = "Nothing was written.";
|
|
56609
|
+
if (row === undefined) {
|
|
56610
|
+
return {
|
|
56611
|
+
code: "REBIND_KEY_NOT_FOUND",
|
|
56612
|
+
message: `No key ${keyPrefix} on this account. ${written}`,
|
|
56613
|
+
suggestion: "The target must be a key on the same account: candle keys list"
|
|
56614
|
+
};
|
|
56615
|
+
}
|
|
56616
|
+
if (row.revokedAt !== undefined) {
|
|
56617
|
+
return {
|
|
56618
|
+
code: "REBIND_TARGET_INVALID",
|
|
56619
|
+
message: `The target key ${keyPrefix} is revoked. ${written}`,
|
|
56620
|
+
suggestion: "Pick an active key: candle keys list"
|
|
56621
|
+
};
|
|
56622
|
+
}
|
|
56623
|
+
if (row.expiresAt !== undefined && row.expiresAt !== null && row.expiresAt <= now) {
|
|
56624
|
+
return {
|
|
56625
|
+
code: "REBIND_TARGET_INVALID",
|
|
56626
|
+
message: `The target key ${keyPrefix} has expired. ${written}`,
|
|
56627
|
+
suggestion: "Pick an active key: candle keys list"
|
|
56628
|
+
};
|
|
56629
|
+
}
|
|
56630
|
+
if (row.environment !== "production") {
|
|
56631
|
+
return {
|
|
56632
|
+
code: "REBIND_TARGET_INVALID",
|
|
56633
|
+
message: `The target key ${keyPrefix} is a test key; the trade rail refuses test keys. ${written}`,
|
|
56634
|
+
suggestion: "Pick a production key: candle keys list"
|
|
56635
|
+
};
|
|
56636
|
+
}
|
|
56637
|
+
if (!row.scopes.includes("swap:write")) {
|
|
56638
|
+
return {
|
|
56639
|
+
code: "REBIND_TARGET_INVALID",
|
|
56640
|
+
message: `The target key ${keyPrefix} lacks swap:write, which a TEE wallet needs to trade. ${written}`,
|
|
56641
|
+
suggestion: "Pick a key with swap:write, or mint one: candle keys create"
|
|
56642
|
+
};
|
|
56643
|
+
}
|
|
56644
|
+
return null;
|
|
56645
|
+
}
|
|
56646
|
+
function tradeReadinessOf(row) {
|
|
56647
|
+
const hasTxLimit = row.txLimit !== null && row.txLimit !== undefined;
|
|
56648
|
+
const limits = row.spendLimits ?? [];
|
|
56649
|
+
const cap = (asset) => limits.some((limit) => limit.asset === asset.toLowerCase() || limit.asset === asset.toUpperCase());
|
|
56650
|
+
const sol = cap("sol");
|
|
56651
|
+
const usdc = cap("usdc");
|
|
56652
|
+
const missingCaps = [];
|
|
56653
|
+
if (!hasTxLimit)
|
|
56654
|
+
missingCaps.push("txLimit");
|
|
56655
|
+
if (!sol)
|
|
56656
|
+
missingCaps.push("spendLimits.sol");
|
|
56657
|
+
if (!usdc)
|
|
56658
|
+
missingCaps.push("spendLimits.usdc");
|
|
56659
|
+
return { tradeReady: { sol: hasTxLimit && sol, usdc: hasTxLimit && usdc }, missingCaps };
|
|
56660
|
+
}
|
|
56661
|
+
function targetWarnings(target) {
|
|
56662
|
+
return capWarnings(target.keyPrefix, {
|
|
56663
|
+
keyPrefix: target.keyPrefix,
|
|
56664
|
+
label: target.label,
|
|
56665
|
+
paused: target.paused,
|
|
56666
|
+
walletScope: target.walletScope,
|
|
56667
|
+
tradeReady: target.tradeReady,
|
|
56668
|
+
missingCaps: target.missingCaps,
|
|
56669
|
+
launchScope: target.launchScope
|
|
56670
|
+
});
|
|
56671
|
+
}
|
|
56672
|
+
async function preflightToKey(ctx, raw, opts) {
|
|
56673
|
+
const { deps, json } = ctx;
|
|
56674
|
+
const refuse2 = (failure) => {
|
|
56675
|
+
writeLocalFailure(deps, failure, json);
|
|
56676
|
+
return { ok: false, exit: 1 };
|
|
56677
|
+
};
|
|
56678
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
56679
|
+
if (!deviceToken)
|
|
56680
|
+
return refuse2(DEVICE_TOKEN_REQUIRED);
|
|
56681
|
+
const listing = await listAccountKeys(ctx, deviceToken);
|
|
56682
|
+
if (!listing.ok) {
|
|
56683
|
+
return refuse2({
|
|
56684
|
+
code: listing.code ?? `HTTP ${listing.status}`,
|
|
56685
|
+
message: `Could not read this account's keys to check --to-key (${listing.status === 0 ? listing.message : `HTTP ${listing.status}`}). Nothing was written.`,
|
|
56686
|
+
suggestion: "Check the device token with: candle auth status"
|
|
56687
|
+
});
|
|
56688
|
+
}
|
|
56689
|
+
const keys = listing.body?.keys ?? [];
|
|
56690
|
+
const resolved = await resolveTargetKey(ctx, deviceToken, raw, { keys });
|
|
56691
|
+
if (!resolved.ok)
|
|
56692
|
+
return { ok: false, exit: resolved.code };
|
|
56693
|
+
const keyPrefix = resolved.keyPrefix;
|
|
56694
|
+
const row = keys.find((key) => key.keyPrefix === keyPrefix);
|
|
56695
|
+
const refusal = targetKeyRefusal(row, keyPrefix, deps.now());
|
|
56696
|
+
if (refusal !== null)
|
|
56697
|
+
return refuse2(refusal);
|
|
56698
|
+
const target = row;
|
|
56699
|
+
const walletScope = target.walletScope === "selected" ? "selected" : "all";
|
|
56700
|
+
if (walletScope === "selected") {
|
|
56701
|
+
const room = await readSelectedScopeRoom(ctx, keyPrefix);
|
|
56702
|
+
if (room.ok) {
|
|
56703
|
+
const moving = opts.labels.filter((label) => !room.heldLabels.has(label)).length;
|
|
56704
|
+
if (room.held + moving > SELECTED_SCOPE_LIMIT) {
|
|
56705
|
+
return refuse2({
|
|
56706
|
+
code: "REBIND_SCOPE_FULL",
|
|
56707
|
+
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.`,
|
|
56708
|
+
suggestion: "Widen the key's wallet scope from the portal, or name a key with room."
|
|
56709
|
+
});
|
|
56710
|
+
}
|
|
56711
|
+
} else {
|
|
56712
|
+
deps.stderr.write(`Could not read key ${keyPrefix}'s wallet set (${room.reason}); its selected-scope room is checked at the rebind.
|
|
56713
|
+
`);
|
|
56714
|
+
}
|
|
56715
|
+
}
|
|
56716
|
+
const readiness = tradeReadinessOf(target);
|
|
56717
|
+
return {
|
|
56718
|
+
ok: true,
|
|
56719
|
+
deviceToken,
|
|
56720
|
+
target: {
|
|
56721
|
+
keyPrefix,
|
|
56722
|
+
label: typeof target.label === "string" && target.label.length > 0 ? target.label : null,
|
|
56723
|
+
walletScope,
|
|
56724
|
+
paused: target.pausedAt !== undefined && target.pausedAt !== null,
|
|
56725
|
+
launchScope: target.scopes.includes("launch:write"),
|
|
56726
|
+
...readiness
|
|
56727
|
+
}
|
|
56728
|
+
};
|
|
56729
|
+
}
|
|
56730
|
+
async function readSelectedScopeRoom(ctx, keyPrefix) {
|
|
56731
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
56732
|
+
if (!apiKey)
|
|
56733
|
+
return { ok: false, reason: "no API key" };
|
|
56734
|
+
const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, {
|
|
56735
|
+
auth: "key",
|
|
56736
|
+
credentials: { apiKey },
|
|
56737
|
+
apiUrl: ctx.apiUrl,
|
|
56738
|
+
fetch: ctx.deps.fetch,
|
|
56739
|
+
env: ctx.deps.env
|
|
56740
|
+
});
|
|
56741
|
+
if (!result.ok)
|
|
56742
|
+
return { ok: false, reason: result.status === 0 ? result.message : `HTTP ${result.status}` };
|
|
56743
|
+
const wallets2 = result.body?.wallets;
|
|
56744
|
+
if (!Array.isArray(wallets2))
|
|
56745
|
+
return { ok: false, reason: "no wallet list in the response" };
|
|
56746
|
+
const heldLabels = new Set;
|
|
56747
|
+
for (const wallet of wallets2)
|
|
56748
|
+
if (typeof wallet?.label === "string")
|
|
56749
|
+
heldLabels.add(wallet.label);
|
|
56750
|
+
return { ok: true, held: wallets2.length, heldLabels };
|
|
56751
|
+
}
|
|
56752
|
+
function chunkWallets(wallets2, size = REBIND_CHUNK) {
|
|
56753
|
+
const out = [];
|
|
56754
|
+
for (let at = 0;at < wallets2.length; at += size)
|
|
56755
|
+
out.push(wallets2.slice(at, at + size));
|
|
56756
|
+
return out;
|
|
56757
|
+
}
|
|
56758
|
+
async function rebindPromoted(ctx, deviceToken, toKeyPrefix, wallets2) {
|
|
56759
|
+
const run = { ok: true, requests: 0, outcomes: new Map };
|
|
56760
|
+
for (const wallet of wallets2)
|
|
56761
|
+
run.outcomes.set(wallet.id, { state: "not-reached" });
|
|
56762
|
+
const chunks = chunkWallets(wallets2);
|
|
56763
|
+
for (const [at, chunk] of chunks.entries()) {
|
|
56764
|
+
run.requests += 1;
|
|
56765
|
+
const preview = await postRebind(ctx, deviceToken, {
|
|
56766
|
+
dryRun: true,
|
|
56767
|
+
toKeyPrefix,
|
|
56768
|
+
wallets: chunk.map((wallet) => wallet.id)
|
|
56769
|
+
});
|
|
56770
|
+
if (!preview.ok) {
|
|
56771
|
+
run.ok = false;
|
|
56772
|
+
run.failure = { ...rebindFailureDetails(preview, {}), chunk: at + 1 };
|
|
56773
|
+
return run;
|
|
56774
|
+
}
|
|
56775
|
+
const shown = preview.body;
|
|
56776
|
+
if (run.toKey === undefined)
|
|
56777
|
+
run.toKey = shown.toKey;
|
|
56778
|
+
for (const row of shown.unchanged)
|
|
56779
|
+
run.outcomes.set(row.id, { state: "unchanged" });
|
|
56780
|
+
const moving = shown.rebound;
|
|
56781
|
+
if (moving.length === 0)
|
|
56782
|
+
continue;
|
|
56783
|
+
run.requests += 1;
|
|
56784
|
+
const committed = await postRebind(ctx, deviceToken, {
|
|
56785
|
+
toKeyPrefix,
|
|
56786
|
+
walletIds: moving.map((row) => row.id),
|
|
56787
|
+
expect: Object.fromEntries(moving.map((row) => [row.id, row.fromKeyPrefix]))
|
|
56788
|
+
});
|
|
56789
|
+
if (!committed.ok) {
|
|
56790
|
+
run.ok = false;
|
|
56791
|
+
run.failure = {
|
|
56792
|
+
...rebindFailureDetails(committed, {
|
|
56793
|
+
fromKeyPrefixes: Array.from(new Set(moving.map((row) => row.fromKeyPrefix)))
|
|
56794
|
+
}),
|
|
56795
|
+
chunk: at + 1
|
|
56796
|
+
};
|
|
56797
|
+
for (const row of moving)
|
|
56798
|
+
run.outcomes.set(row.id, { state: "failed" });
|
|
56799
|
+
return run;
|
|
56800
|
+
}
|
|
56801
|
+
const result = committed.body;
|
|
56802
|
+
for (const row of result.rebound) {
|
|
56803
|
+
run.outcomes.set(row.id, { state: "rebound", ...row.auditId !== undefined ? { auditId: row.auditId } : {} });
|
|
56804
|
+
}
|
|
56805
|
+
for (const row of result.unchanged)
|
|
56806
|
+
run.outcomes.set(row.id, { state: "unchanged" });
|
|
56807
|
+
}
|
|
56808
|
+
return run;
|
|
56809
|
+
}
|
|
56810
|
+
function finishingCommands(addresses, toKeyPrefix) {
|
|
56811
|
+
return chunkWallets(addresses).map((chunk) => `candle tee rebind ${chunk.join(" ")} --to-key ${toKeyPrefix}`);
|
|
56812
|
+
}
|
|
56813
|
+
function pendingWallets(wallets2, run) {
|
|
56814
|
+
return wallets2.filter((wallet) => {
|
|
56815
|
+
const state = run?.outcomes.get(wallet.id)?.state;
|
|
56816
|
+
return state !== "rebound" && state !== "unchanged";
|
|
56817
|
+
});
|
|
56818
|
+
}
|
|
56819
|
+
function renderRebindReport(input) {
|
|
56820
|
+
const { target, wallets: wallets2, run, notRebindable } = input;
|
|
56821
|
+
const lines = [];
|
|
56822
|
+
const name = (wallet) => `${wallet.label} (${wallet.address})`;
|
|
56823
|
+
if (run !== undefined) {
|
|
56824
|
+
const rebound = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "rebound").length;
|
|
56825
|
+
const unchanged = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "unchanged").length;
|
|
56826
|
+
if (rebound > 0 || unchanged > 0) {
|
|
56827
|
+
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"}.`);
|
|
56828
|
+
}
|
|
56829
|
+
if (run.failure !== undefined) {
|
|
56830
|
+
lines.push(`Rebind ${run.failure.code}: ${run.failure.message}${run.failure.suggestion ? ` ${run.failure.suggestion}` : ""}`);
|
|
56831
|
+
}
|
|
56832
|
+
}
|
|
56833
|
+
const pending = pendingWallets(wallets2, run);
|
|
56834
|
+
if (pending.length > 0) {
|
|
56835
|
+
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}`));
|
|
56836
|
+
}
|
|
56837
|
+
if (notRebindable.length > 0) {
|
|
56838
|
+
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}`));
|
|
56839
|
+
}
|
|
56840
|
+
return lines.join(`
|
|
56841
|
+
`);
|
|
56842
|
+
}
|
|
56843
|
+
function walletRebindJson(wallet, run) {
|
|
56844
|
+
if (wallet.id === null || wallet.remoteAuthority !== "verified-active") {
|
|
56845
|
+
return {
|
|
56846
|
+
state: "not-rebindable",
|
|
56847
|
+
reason: wallet.id === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
|
|
56848
|
+
};
|
|
56849
|
+
}
|
|
56850
|
+
const outcome = run?.outcomes.get(wallet.id);
|
|
56851
|
+
if (outcome === undefined)
|
|
56852
|
+
return { state: "not-reached" };
|
|
56853
|
+
return { state: outcome.state, ...outcome.auditId !== undefined ? { auditId: outcome.auditId } : {} };
|
|
56854
|
+
}
|
|
56855
|
+
function finalBoundKey(wallet, run, toKeyPrefix) {
|
|
56856
|
+
const state = wallet.id === null ? undefined : run?.outcomes.get(wallet.id)?.state;
|
|
56857
|
+
return state === "rebound" || state === "unchanged" ? toKeyPrefix : wallet.importedTo;
|
|
56858
|
+
}
|
|
56859
|
+
function rebindJson(input) {
|
|
56860
|
+
const { target, wallets: wallets2, run } = input;
|
|
56861
|
+
const pending = pendingWallets(wallets2, run);
|
|
56862
|
+
const count = (state) => wallets2.filter((wallet) => run?.outcomes.get(wallet.id)?.state === state).length;
|
|
56863
|
+
return {
|
|
56864
|
+
toKey: {
|
|
56865
|
+
keyPrefix: target.keyPrefix,
|
|
56866
|
+
label: target.label,
|
|
56867
|
+
tradeReady: run?.toKey?.tradeReady ?? target.tradeReady,
|
|
56868
|
+
missingCaps: run?.toKey?.missingCaps ?? target.missingCaps
|
|
56869
|
+
},
|
|
56870
|
+
rebind: {
|
|
56871
|
+
ok: run !== undefined && run.ok && input.notRebindable.length === 0,
|
|
56872
|
+
reached: run !== undefined,
|
|
56873
|
+
requests: run?.requests ?? 0,
|
|
56874
|
+
rebound: count("rebound"),
|
|
56875
|
+
unchanged: count("unchanged"),
|
|
56876
|
+
pending: pending.map((wallet) => wallet.address),
|
|
56877
|
+
notRebindable: input.notRebindable.length,
|
|
56878
|
+
...run?.failure !== undefined ? {
|
|
56879
|
+
code: run.failure.code,
|
|
56880
|
+
message: run.failure.message,
|
|
56881
|
+
...run.failure.suggestion !== undefined ? { suggestion: run.failure.suggestion } : {}
|
|
56882
|
+
} : {},
|
|
56883
|
+
finishWith: finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix)
|
|
56884
|
+
}
|
|
56885
|
+
};
|
|
56886
|
+
}
|
|
56887
|
+
|
|
56888
|
+
// src/commands/vault-promote.ts
|
|
54979
56889
|
init_signer_roles();
|
|
54980
56890
|
init_store();
|
|
54981
56891
|
init_vault_support();
|
|
54982
56892
|
async function vaultPromote(args, ctx) {
|
|
54983
56893
|
const parsed = parseArgs(args, {
|
|
54984
|
-
valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore"],
|
|
56894
|
+
valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore", "--to-key"],
|
|
54985
56895
|
booleanFlags: ["--accept-unknown-exposure", "--accept-older-copy"],
|
|
54986
56896
|
pathFlags: ["--keystore"]
|
|
54987
56897
|
});
|
|
@@ -54995,21 +56905,73 @@ async function vaultPromote(args, ctx) {
|
|
|
54995
56905
|
return usage(ctx, "Use either --from or --in-place, not both.");
|
|
54996
56906
|
}
|
|
54997
56907
|
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>");
|
|
56908
|
+
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
56909
|
}
|
|
56910
|
+
const toKeyRaw = parsed.values["--to-key"];
|
|
56911
|
+
if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
|
|
56912
|
+
return usage(ctx, "--to-key needs a label or a prefix.");
|
|
55000
56913
|
if (!refuseEnvPassphrase(ctx))
|
|
55001
56914
|
return 1;
|
|
55002
56915
|
if (!requireTty(ctx, "vault promote"))
|
|
55003
56916
|
return 1;
|
|
55004
|
-
if (fromLabel !== undefined) {
|
|
55005
|
-
|
|
55006
|
-
|
|
56917
|
+
if (fromLabel !== undefined && parsed.values["--sweep-to"] !== undefined) {
|
|
56918
|
+
return usage(ctx, "Fresh-key promote takes --from, not --sweep-to.");
|
|
56919
|
+
}
|
|
56920
|
+
let toKey;
|
|
56921
|
+
if (toKeyRaw !== undefined) {
|
|
56922
|
+
const label = parsed.values["--label"] ?? inPlaceLabel ?? (fromLabel !== undefined ? "<fresh>" : undefined);
|
|
56923
|
+
const preflight = await preflightToKey(ctx, toKeyRaw, { labels: label !== undefined ? [label] : [] });
|
|
56924
|
+
if (!preflight.ok)
|
|
56925
|
+
return preflight.exit;
|
|
56926
|
+
toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
|
|
56927
|
+
}
|
|
56928
|
+
if (fromLabel !== undefined)
|
|
56929
|
+
return promoteFresh(ctx, parsed, fromLabel, toKey);
|
|
56930
|
+
return promoteInPlace(ctx, parsed, inPlaceLabel, toKey);
|
|
56931
|
+
}
|
|
56932
|
+
async function rebindAfterImport(ctx, toKey, wallet, callingKeyPrefix) {
|
|
56933
|
+
const rebindable = wallet.linkedWalletId !== null && wallet.remoteAuthority === "verified-active" ? [{ id: wallet.linkedWalletId, address: wallet.address, label: wallet.label }] : [];
|
|
56934
|
+
const notRebindable = rebindable.length === 0 ? [
|
|
56935
|
+
{
|
|
56936
|
+
address: wallet.address,
|
|
56937
|
+
label: wallet.label,
|
|
56938
|
+
reason: wallet.linkedWalletId === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
|
|
56939
|
+
}
|
|
56940
|
+
] : [];
|
|
56941
|
+
const run = rebindable.length > 0 ? await rebindPromoted(ctx, toKey.deviceToken, toKey.target.keyPrefix, rebindable) : undefined;
|
|
56942
|
+
const input = { target: toKey.target, callingKeyPrefix, wallets: rebindable, run, notRebindable };
|
|
56943
|
+
const report = renderRebindReport(input);
|
|
56944
|
+
if (report.length > 0)
|
|
56945
|
+
ctx.deps.stderr.write(`${report}
|
|
56946
|
+
`);
|
|
56947
|
+
const id = wallet.linkedWalletId;
|
|
56948
|
+
return {
|
|
56949
|
+
exit: run !== undefined && !run.ok ? 1 : 0,
|
|
56950
|
+
json: {
|
|
56951
|
+
...rebindJson(input),
|
|
56952
|
+
boundKeyPrefix: finalBoundKey({ id, importedTo: wallet.importedTo }, run, toKey.target.keyPrefix),
|
|
56953
|
+
walletRebind: walletRebindJson({ id, remoteAuthority: wallet.remoteAuthority }, run)
|
|
55007
56954
|
}
|
|
55008
|
-
|
|
56955
|
+
};
|
|
56956
|
+
}
|
|
56957
|
+
async function confirmResumeRebind(ctx, toKey) {
|
|
56958
|
+
const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
|
|
56959
|
+
ctx.deps.stderr.write(`${[
|
|
56960
|
+
`This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
|
|
56961
|
+
...targetWarnings(toKey.target)
|
|
56962
|
+
].join(`
|
|
56963
|
+
`)}
|
|
56964
|
+
`);
|
|
56965
|
+
const typed = await ctx.deps.promptLine(`Type ${CONFIRM_WORD} to move this wallet to ${toKey.target.keyPrefix}: `);
|
|
56966
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
56967
|
+
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
56968
|
}
|
|
55010
|
-
return promoteInPlace(ctx, parsed, inPlaceLabel);
|
|
55011
56969
|
}
|
|
55012
|
-
async function
|
|
56970
|
+
async function callingKeyPrefixFor(ctx) {
|
|
56971
|
+
const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
|
|
56972
|
+
return (apiKey !== undefined ? apiKeyPrefix(apiKey) : undefined) ?? "(the calling key)";
|
|
56973
|
+
}
|
|
56974
|
+
async function promoteFresh(ctx, parsed, fromLabel, toKey) {
|
|
55013
56975
|
if ("error" in parsed)
|
|
55014
56976
|
return usage(ctx, parsed.error);
|
|
55015
56977
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
@@ -55088,10 +57050,19 @@ async function promoteFresh(ctx, parsed, fromLabel) {
|
|
|
55088
57050
|
addKeys: [blob]
|
|
55089
57051
|
}, ctx.deps));
|
|
55090
57052
|
await confirmLastSix(ctx, destination.address, "the sweep vault destination");
|
|
57053
|
+
if (toKey !== undefined) {
|
|
57054
|
+
const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
|
|
57055
|
+
ctx.deps.stderr.write(`${[
|
|
57056
|
+
`This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
|
|
57057
|
+
...targetWarnings(toKey.target)
|
|
57058
|
+
].join(`
|
|
57059
|
+
`)}
|
|
57060
|
+
`);
|
|
57061
|
+
}
|
|
55091
57062
|
const privateKey = base58.encode(secret64);
|
|
55092
57063
|
wipe(secret64);
|
|
55093
57064
|
const importCount = { n: 0 };
|
|
55094
|
-
const { exit: code } = await runTeeImport(ctx, {
|
|
57065
|
+
const { exit: code, submitted } = await runTeeImport(ctx, {
|
|
55095
57066
|
address,
|
|
55096
57067
|
privateKey,
|
|
55097
57068
|
label: entry.label,
|
|
@@ -55110,22 +57081,35 @@ async function promoteFresh(ctx, parsed, fromLabel) {
|
|
|
55110
57081
|
if (target === undefined) {
|
|
55111
57082
|
throw new VaultError("VAULT_INDEX_INVALID", `Entry ${keyId} missing after import.`);
|
|
55112
57083
|
}
|
|
57084
|
+
let rebound;
|
|
57085
|
+
if (toKey !== undefined) {
|
|
57086
|
+
const importedTo = submitted?.boundKeyPrefix ?? null;
|
|
57087
|
+
rebound = await rebindAfterImport(ctx, toKey, {
|
|
57088
|
+
linkedWalletId: target.linkedWalletId ?? null,
|
|
57089
|
+
address,
|
|
57090
|
+
label: entry.label,
|
|
57091
|
+
remoteAuthority: target.tee?.remoteAuthority ?? submitted?.remoteAuthority ?? null,
|
|
57092
|
+
importedTo
|
|
57093
|
+
}, await callingKeyPrefixFor(ctx));
|
|
57094
|
+
}
|
|
57095
|
+
const exit = Math.max(code, rebound?.exit ?? 0);
|
|
55113
57096
|
if (ctx.json) {
|
|
55114
57097
|
writeJson(ctx.deps, {
|
|
55115
|
-
ok:
|
|
57098
|
+
ok: rebound === undefined || rebound.exit === 0,
|
|
55116
57099
|
mode: "fresh",
|
|
55117
57100
|
address,
|
|
55118
57101
|
label: entry.label,
|
|
55119
57102
|
vaultDestination: destination.address,
|
|
55120
57103
|
lifecycle: target.tee?.lifecycle,
|
|
55121
57104
|
linkedWalletId: target.linkedWalletId ?? null,
|
|
55122
|
-
importCalls: importCount.n
|
|
57105
|
+
importCalls: importCount.n,
|
|
57106
|
+
...rebound?.json ?? {}
|
|
55123
57107
|
});
|
|
55124
57108
|
} else {
|
|
55125
57109
|
ctx.deps.stdout.write(`Promoted fresh TEE wallet ${address} (sweep to ${destination.address}).
|
|
55126
57110
|
`);
|
|
55127
57111
|
}
|
|
55128
|
-
return
|
|
57112
|
+
return exit;
|
|
55129
57113
|
});
|
|
55130
57114
|
}
|
|
55131
57115
|
async function refuseWithoutRoom(ctx) {
|
|
@@ -55147,7 +57131,7 @@ async function reopenFromDisk(path, reopen, previous) {
|
|
|
55147
57131
|
});
|
|
55148
57132
|
return reopen(path, raw);
|
|
55149
57133
|
}
|
|
55150
|
-
async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
57134
|
+
async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
|
|
55151
57135
|
if ("error" in parsed)
|
|
55152
57136
|
return usage(ctx, parsed.error);
|
|
55153
57137
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
@@ -55174,7 +57158,9 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55174
57158
|
if (sweepTo !== undefined) {
|
|
55175
57159
|
return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
|
|
55176
57160
|
}
|
|
55177
|
-
|
|
57161
|
+
if (toKey !== undefined)
|
|
57162
|
+
await confirmResumeRebind(ctx, toKey);
|
|
57163
|
+
const resumed = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
|
|
55178
57164
|
confirmAccount: (account) => confirmLastSix(ctx, account, "that account"),
|
|
55179
57165
|
confirmDestination: async (destination2) => {
|
|
55180
57166
|
try {
|
|
@@ -55183,9 +57169,36 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55183
57169
|
} catch {
|
|
55184
57170
|
return false;
|
|
55185
57171
|
}
|
|
55186
|
-
}
|
|
57172
|
+
},
|
|
57173
|
+
report: toKey !== undefined ? "return" : "write"
|
|
55187
57174
|
});
|
|
55188
|
-
|
|
57175
|
+
if (toKey === undefined)
|
|
57176
|
+
return resumed.exit;
|
|
57177
|
+
if (resumed.failure !== undefined || resumed.exit !== 0) {
|
|
57178
|
+
writeLocalFailure(ctx.deps, resumed.failure ?? { code: "PROMOTE_OUTCOME_UNRESOLVED", message: "The resume did not complete." }, ctx.json);
|
|
57179
|
+
return resumed.exit;
|
|
57180
|
+
}
|
|
57181
|
+
const rebound2 = await rebindAfterImport(ctx, toKey, {
|
|
57182
|
+
linkedWalletId: resumed.adopted?.linkedWalletId ?? null,
|
|
57183
|
+
address: existing.address,
|
|
57184
|
+
label: existing.label,
|
|
57185
|
+
remoteAuthority: resumed.adopted?.remoteAuthority ?? null,
|
|
57186
|
+
importedTo: existing.tee?.boundKeyPrefix ?? null
|
|
57187
|
+
}, await callingKeyPrefixFor(ctx));
|
|
57188
|
+
if (ctx.json) {
|
|
57189
|
+
writeJson(ctx.deps, {
|
|
57190
|
+
ok: rebound2.exit === 0,
|
|
57191
|
+
mode: "resume",
|
|
57192
|
+
address: existing.address,
|
|
57193
|
+
lifecycle: "enabled",
|
|
57194
|
+
importCalls: 0,
|
|
57195
|
+
...rebound2.json
|
|
57196
|
+
});
|
|
57197
|
+
} else {
|
|
57198
|
+
ctx.deps.stdout.write(`Resumed ${existing.address}: grant adopted; no re-import.
|
|
57199
|
+
`);
|
|
57200
|
+
}
|
|
57201
|
+
return rebound2.exit;
|
|
55189
57202
|
}
|
|
55190
57203
|
if (sweepTo === undefined || rpcUrl2 === undefined) {
|
|
55191
57204
|
return usage(ctx, "Usage: candle vault promote --in-place <label> --sweep-to <label> --rpc-url <url> [--label] [--accept-unknown-exposure]");
|
|
@@ -55197,7 +57210,12 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55197
57210
|
return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
|
|
55198
57211
|
}
|
|
55199
57212
|
await refuseWithoutRoom(ctx);
|
|
55200
|
-
const
|
|
57213
|
+
const live = await readControlledBy(ctx);
|
|
57214
|
+
const controlledBy = toKey === undefined ? live : withToKey(live, {
|
|
57215
|
+
keyPrefix: toKey.target.keyPrefix,
|
|
57216
|
+
label: toKey.target.label,
|
|
57217
|
+
warnings: targetWarnings(toKey.target)
|
|
57218
|
+
});
|
|
55201
57219
|
const rpc2 = createSolanaRpc(rpcUrl2, ctx.deps.fetch);
|
|
55202
57220
|
const host = new URL(rpcUrl2).host;
|
|
55203
57221
|
await displayHoldings(ctx, rpc2, first.subject.address);
|
|
@@ -55267,24 +57285,37 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
|
|
|
55267
57285
|
writeLocalFailure(ctx.deps, failure, ctx.json);
|
|
55268
57286
|
return code;
|
|
55269
57287
|
}
|
|
57288
|
+
let rebound;
|
|
57289
|
+
if (toKey !== undefined && imported.submitted !== undefined) {
|
|
57290
|
+
const importedTo = imported.submitted.boundKeyPrefix ?? null;
|
|
57291
|
+
rebound = await rebindAfterImport(ctx, toKey, {
|
|
57292
|
+
linkedWalletId: imported.submitted.id ?? null,
|
|
57293
|
+
address: subject.address,
|
|
57294
|
+
label: parsed.values["--label"] ?? subject.label,
|
|
57295
|
+
remoteAuthority: imported.submitted.remoteAuthority ?? null,
|
|
57296
|
+
importedTo
|
|
57297
|
+
}, controlledBy.keyPrefix);
|
|
57298
|
+
}
|
|
57299
|
+
const exit = Math.max(code, rebound?.exit ?? 0);
|
|
55270
57300
|
if (ctx.json) {
|
|
55271
57301
|
const reopened = hold(await reopenFromDisk(path, opened.reopen, vault));
|
|
55272
57302
|
const updated = reopened.index.entries.find((e) => e.id === subject.id);
|
|
55273
57303
|
writeJson(ctx.deps, {
|
|
55274
|
-
ok: code === 0 || code === 3,
|
|
57304
|
+
ok: (code === 0 || code === 3) && (rebound === undefined || rebound.exit === 0),
|
|
55275
57305
|
mode: "in-place",
|
|
55276
57306
|
address: subject.address,
|
|
55277
57307
|
vaultDestination: destination.address,
|
|
55278
57308
|
lifecycle: updated?.tee?.lifecycle ?? null,
|
|
55279
57309
|
linkedWalletId: updated?.linkedWalletId ?? null,
|
|
55280
57310
|
controlledBy: controlledByJson(controlledBy),
|
|
55281
|
-
authorities: authoritiesJson(roles)
|
|
57311
|
+
authorities: authoritiesJson(roles),
|
|
57312
|
+
...rebound?.json ?? {}
|
|
55282
57313
|
});
|
|
55283
57314
|
} else if (code === 0 || code === 3) {
|
|
55284
57315
|
ctx.deps.stdout.write(`Promoted ${subject.address} in place (sweep to ${destination.address}). This address never returns to cold.
|
|
55285
57316
|
`);
|
|
55286
57317
|
}
|
|
55287
|
-
return
|
|
57318
|
+
return exit;
|
|
55288
57319
|
});
|
|
55289
57320
|
}
|
|
55290
57321
|
async function resumePromote(ctx, vault, entry, reopen, path, hold, confirmations) {
|
|
@@ -55870,12 +57901,12 @@ function parseValueUsdCell(raw) {
|
|
|
55870
57901
|
const value = Number(cleaned);
|
|
55871
57902
|
return Number.isFinite(value) ? value : undefined;
|
|
55872
57903
|
}
|
|
55873
|
-
function
|
|
57904
|
+
function formatUsd2(value) {
|
|
55874
57905
|
const [whole, fraction] = value.toFixed(2).split(".");
|
|
55875
57906
|
const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
55876
57907
|
return `$${grouped}.${fraction ?? "00"}`;
|
|
55877
57908
|
}
|
|
55878
|
-
function
|
|
57909
|
+
function formatSol4(lamports) {
|
|
55879
57910
|
const whole = lamports / 1000000000n;
|
|
55880
57911
|
const fraction = lamports % 1000000000n;
|
|
55881
57912
|
return `${whole}.${fraction.toString().padStart(9, "0").slice(0, 6)}`;
|
|
@@ -55904,14 +57935,37 @@ init_promote_support();
|
|
|
55904
57935
|
init_signer_roles();
|
|
55905
57936
|
init_store();
|
|
55906
57937
|
init_vault_support();
|
|
55907
|
-
var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings] [--accept-unknown-exposure]";
|
|
57938
|
+
var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--to-key <label|prefix>] [--token-holdings] [--accept-unknown-exposure]";
|
|
55908
57939
|
var rowObserver = null;
|
|
57940
|
+
function splitRebindable(results) {
|
|
57941
|
+
const wallets2 = [];
|
|
57942
|
+
const notRebindable = [];
|
|
57943
|
+
for (const row of results) {
|
|
57944
|
+
if (row.linkedWalletId !== null && row.remoteAuthority === "verified-active") {
|
|
57945
|
+
wallets2.push({ id: row.linkedWalletId, address: row.address, label: row.label });
|
|
57946
|
+
} else {
|
|
57947
|
+
notRebindable.push({
|
|
57948
|
+
address: row.address,
|
|
57949
|
+
label: row.label,
|
|
57950
|
+
reason: row.linkedWalletId === null ? `no linked wallet id (${row.lifecycle})` : `remote authority is ${row.remoteAuthority ?? "unknown"}, not verified-active`
|
|
57951
|
+
});
|
|
57952
|
+
}
|
|
57953
|
+
}
|
|
57954
|
+
return { wallets: wallets2, notRebindable };
|
|
57955
|
+
}
|
|
57956
|
+
function keysWithRebind(results, phase, run) {
|
|
57957
|
+
return results.map((row) => ({
|
|
57958
|
+
...row,
|
|
57959
|
+
boundKeyPrefix: finalBoundKey({ id: row.linkedWalletId, importedTo: phase.importedTo.get(row.address) ?? null }, run, phase.toKey.target.keyPrefix),
|
|
57960
|
+
rebind: walletRebindJson({ id: row.linkedWalletId, remoteAuthority: row.remoteAuthority }, run)
|
|
57961
|
+
}));
|
|
57962
|
+
}
|
|
55909
57963
|
async function vaultPromoteBatch(args, ctx) {
|
|
55910
57964
|
if (args.some((arg) => arg === "--from" || arg.startsWith("--from="))) {
|
|
55911
57965
|
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
57966
|
}
|
|
55913
57967
|
const parsed = parseArgs(args, {
|
|
55914
|
-
valueFlags: ["--pairs-from", "--rpc-url", "--keystore"],
|
|
57968
|
+
valueFlags: ["--pairs-from", "--rpc-url", "--keystore", "--to-key"],
|
|
55915
57969
|
booleanFlags: ["--token-holdings", "--accept-unknown-exposure", "--accept-older-copy"],
|
|
55916
57970
|
pathFlags: ["--keystore", "--pairs-from"]
|
|
55917
57971
|
});
|
|
@@ -55922,6 +57976,9 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55922
57976
|
const pairsFile = parsed.values["--pairs-from"];
|
|
55923
57977
|
if (pairsFile === undefined)
|
|
55924
57978
|
return usage(ctx, USAGE_LINE);
|
|
57979
|
+
const toKeyRaw = parsed.values["--to-key"];
|
|
57980
|
+
if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
|
|
57981
|
+
return usage(ctx, "--to-key needs a label or a prefix.");
|
|
55925
57982
|
if (!refuseEnvPassphrase(ctx))
|
|
55926
57983
|
return 1;
|
|
55927
57984
|
if (!requireTty(ctx, "vault promote-batch"))
|
|
@@ -55946,6 +58003,13 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55946
58003
|
if (!parsedFile.ok)
|
|
55947
58004
|
return usage(ctx, renderPhaseAFindings(pairsFile, parsedFile.findings));
|
|
55948
58005
|
const { rows, hasValueUsd } = parsedFile;
|
|
58006
|
+
let toKey;
|
|
58007
|
+
if (toKeyRaw !== undefined) {
|
|
58008
|
+
const preflight = await preflightToKey(ctx, toKeyRaw, { labels: rows.map((row) => row.label) });
|
|
58009
|
+
if (!preflight.ok)
|
|
58010
|
+
return preflight.exit;
|
|
58011
|
+
toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
|
|
58012
|
+
}
|
|
55949
58013
|
return runVaultCommand(ctx, async ({ hold }) => {
|
|
55950
58014
|
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
55951
58015
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
@@ -55981,7 +58045,17 @@ async function vaultPromoteBatch(args, ctx) {
|
|
|
55981
58045
|
if (roomRefusal !== null)
|
|
55982
58046
|
throw roomRefusal;
|
|
55983
58047
|
const room = roomRead.room;
|
|
55984
|
-
const
|
|
58048
|
+
const live = await readControlledBy(ctx);
|
|
58049
|
+
const controlledBy = toKey === undefined ? live : withToKey(live, {
|
|
58050
|
+
keyPrefix: toKey.target.keyPrefix,
|
|
58051
|
+
label: toKey.target.label,
|
|
58052
|
+
warnings: targetWarnings(toKey.target)
|
|
58053
|
+
});
|
|
58054
|
+
const rebindPhase = toKey === undefined ? undefined : { toKey, callingKeyPrefix: live.keyPrefix, importedTo: new Map };
|
|
58055
|
+
for (const item of planned) {
|
|
58056
|
+
if (item.kind === "skip")
|
|
58057
|
+
rebindPhase?.importedTo.set(item.subject.address, item.subject.tee?.boundKeyPrefix ?? null);
|
|
58058
|
+
}
|
|
55985
58059
|
const addresses = planned.map((item) => item.subject.address);
|
|
55986
58060
|
const rpc2 = createSolanaRpc(rpcUrl2, deps.fetch);
|
|
55987
58061
|
const host = new URL(rpcUrl2).host;
|
|
@@ -56040,28 +58114,58 @@ ${footer}
|
|
|
56040
58114
|
});
|
|
56041
58115
|
}
|
|
56042
58116
|
if (acting.length === 0) {
|
|
56043
|
-
|
|
58117
|
+
const roles2 = {
|
|
58118
|
+
checked: [...ROLE_GROUP_IDS],
|
|
58119
|
+
notChecked: [],
|
|
58120
|
+
found: [],
|
|
58121
|
+
requests: 0,
|
|
58122
|
+
planned: 0,
|
|
58123
|
+
rateLimited: 0,
|
|
58124
|
+
elapsedMs: 0
|
|
58125
|
+
};
|
|
58126
|
+
const skipped = planned.map((item) => skippedResult(item));
|
|
58127
|
+
const rebindable = rebindPhase === undefined ? undefined : splitRebindable(skipped);
|
|
58128
|
+
if (rebindPhase === undefined || rebindable === undefined || rebindable.wallets.length === 0) {
|
|
58129
|
+
deps.stderr.write(`
|
|
56044
58130
|
${table}
|
|
56045
58131
|
|
|
56046
58132
|
${footer}
|
|
56047
58133
|
Nothing to do: every row already landed.
|
|
56048
58134
|
`);
|
|
58135
|
+
return finish(ctx, {
|
|
58136
|
+
file: pairsFile,
|
|
58137
|
+
rows: rows.length,
|
|
58138
|
+
keys: skipped,
|
|
58139
|
+
destinations,
|
|
58140
|
+
exit: 0,
|
|
58141
|
+
controlledBy,
|
|
58142
|
+
roles: roles2
|
|
58143
|
+
});
|
|
58144
|
+
}
|
|
58145
|
+
const n = rebindable.wallets.length;
|
|
58146
|
+
deps.stderr.write(`
|
|
58147
|
+
${table}
|
|
58148
|
+
|
|
58149
|
+
${footer}
|
|
58150
|
+
Every row already landed; ${n} wallet${n === 1 ? "" : "s"} to rebind.
|
|
58151
|
+
`);
|
|
58152
|
+
deps.stderr.write(`
|
|
58153
|
+
${renderControlledBy(controlledBy, n)}
|
|
58154
|
+
`);
|
|
58155
|
+
const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to move ${n === 1 ? "this wallet" : `these ${n} wallets`} to ${rebindPhase.toKey.target.keyPrefix}: `);
|
|
58156
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
58157
|
+
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.` });
|
|
58158
|
+
}
|
|
58159
|
+
const rebound2 = await runRebindPhase(ctx, rebindPhase, skipped);
|
|
56049
58160
|
return finish(ctx, {
|
|
56050
58161
|
file: pairsFile,
|
|
56051
58162
|
rows: rows.length,
|
|
56052
|
-
keys:
|
|
58163
|
+
keys: skipped,
|
|
56053
58164
|
destinations,
|
|
56054
58165
|
exit: 0,
|
|
56055
58166
|
controlledBy,
|
|
56056
|
-
roles:
|
|
56057
|
-
|
|
56058
|
-
notChecked: [],
|
|
56059
|
-
found: [],
|
|
56060
|
-
requests: 0,
|
|
56061
|
-
planned: 0,
|
|
56062
|
-
rateLimited: 0,
|
|
56063
|
-
elapsedMs: 0
|
|
56064
|
-
}
|
|
58167
|
+
roles: roles2,
|
|
58168
|
+
rebound: rebound2
|
|
56065
58169
|
});
|
|
56066
58170
|
}
|
|
56067
58171
|
const checked = roles;
|
|
@@ -56152,6 +58256,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56152
58256
|
worst = Math.max(worst, imported.exit);
|
|
56153
58257
|
done += 1;
|
|
56154
58258
|
last = { address: subject2.address, keyId: subject2.id };
|
|
58259
|
+
rebindPhase?.importedTo.set(subject2.address, imported.submitted.boundKeyPrefix ?? null);
|
|
56155
58260
|
results.push({
|
|
56156
58261
|
line: row2.line,
|
|
56157
58262
|
label: subject2.label,
|
|
@@ -56191,6 +58296,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56191
58296
|
await rowObserver?.({ line: row.line, stage: "resumed", vault: current });
|
|
56192
58297
|
done += 1;
|
|
56193
58298
|
last = { address: subject.address, keyId: subject.id };
|
|
58299
|
+
rebindPhase?.importedTo.set(subject.address, subject.tee?.boundKeyPrefix ?? null);
|
|
56194
58300
|
results.push({
|
|
56195
58301
|
line: row.line,
|
|
56196
58302
|
label: subject.label,
|
|
@@ -56216,6 +58322,20 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56216
58322
|
}
|
|
56217
58323
|
if (stopped !== undefined) {
|
|
56218
58324
|
reportPartial(ctx, { landed: done, acting: acting.length, failure: stopped });
|
|
58325
|
+
let notReached;
|
|
58326
|
+
if (rebindPhase !== undefined) {
|
|
58327
|
+
const { wallets: wallets2, notRebindable } = splitRebindable(results);
|
|
58328
|
+
notReached = {
|
|
58329
|
+
target: rebindPhase.toKey.target,
|
|
58330
|
+
callingKeyPrefix: rebindPhase.callingKeyPrefix,
|
|
58331
|
+
wallets: wallets2,
|
|
58332
|
+
notRebindable
|
|
58333
|
+
};
|
|
58334
|
+
const report = renderRebindReport(notReached);
|
|
58335
|
+
if (report.length > 0)
|
|
58336
|
+
deps.stderr.write(`${report}
|
|
58337
|
+
`);
|
|
58338
|
+
}
|
|
56219
58339
|
if (ctx.json) {
|
|
56220
58340
|
writeJson(deps, {
|
|
56221
58341
|
ok: false,
|
|
@@ -56226,13 +58346,14 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56226
58346
|
resumed: results.filter((r) => r.state === "resumed").length,
|
|
56227
58347
|
skipped: results.filter((r) => r.state === "skipped").length,
|
|
56228
58348
|
destinations: destinations.map(({ label, address, keys }) => ({ label, address, keys })),
|
|
56229
|
-
keys: results,
|
|
58349
|
+
keys: rebindPhase === undefined ? results : keysWithRebind(results, rebindPhase, undefined),
|
|
56230
58350
|
failedLine: stopped.line,
|
|
56231
58351
|
code: stopped.code,
|
|
56232
58352
|
message: stopped.message,
|
|
56233
58353
|
...stopped.suggestion !== undefined ? { suggestion: stopped.suggestion } : {},
|
|
56234
58354
|
controlledBy: controlledByJson(controlledBy),
|
|
56235
|
-
authorities: authoritiesJson(checked)
|
|
58355
|
+
authorities: authoritiesJson(checked),
|
|
58356
|
+
...notReached !== undefined ? rebindJson(notReached) : {}
|
|
56236
58357
|
});
|
|
56237
58358
|
} else {
|
|
56238
58359
|
deps.stderr.write(`${stopped.message}${stopped.suggestion ? ` ${stopped.suggestion}` : ""}
|
|
@@ -56242,6 +58363,7 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56242
58363
|
}
|
|
56243
58364
|
if (last !== undefined)
|
|
56244
58365
|
await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
|
|
58366
|
+
const rebound = rebindPhase === undefined ? undefined : await runRebindPhase(ctx, rebindPhase, results);
|
|
56245
58367
|
return finish(ctx, {
|
|
56246
58368
|
file: pairsFile,
|
|
56247
58369
|
rows: rows.length,
|
|
@@ -56249,10 +58371,27 @@ ${renderControlledBy(controlledBy, acting.length)}
|
|
|
56249
58371
|
destinations,
|
|
56250
58372
|
exit: worst,
|
|
56251
58373
|
controlledBy,
|
|
56252
|
-
roles: checked
|
|
58374
|
+
roles: checked,
|
|
58375
|
+
...rebound !== undefined ? { rebound } : {}
|
|
56253
58376
|
});
|
|
56254
58377
|
});
|
|
56255
58378
|
}
|
|
58379
|
+
async function runRebindPhase(ctx, phase, results) {
|
|
58380
|
+
const { wallets: wallets2, notRebindable } = splitRebindable(results);
|
|
58381
|
+
const run = wallets2.length > 0 ? await rebindPromoted(ctx, phase.toKey.deviceToken, phase.toKey.target.keyPrefix, wallets2) : undefined;
|
|
58382
|
+
const input = {
|
|
58383
|
+
target: phase.toKey.target,
|
|
58384
|
+
callingKeyPrefix: phase.callingKeyPrefix,
|
|
58385
|
+
wallets: wallets2,
|
|
58386
|
+
run,
|
|
58387
|
+
notRebindable
|
|
58388
|
+
};
|
|
58389
|
+
const report = renderRebindReport(input);
|
|
58390
|
+
if (report.length > 0)
|
|
58391
|
+
ctx.deps.stderr.write(`${report}
|
|
58392
|
+
`);
|
|
58393
|
+
return { phase, run, input };
|
|
58394
|
+
}
|
|
56256
58395
|
async function verifySubjectSecret(vault, subject) {
|
|
56257
58396
|
const secret = await decryptKey(vault, subject.id);
|
|
56258
58397
|
try {
|
|
@@ -56375,7 +58514,7 @@ function renderBatchTable(planned, opts) {
|
|
|
56375
58514
|
const authority = item.kind === "skip" ? "" : opts.roles === undefined ? "?" : authorityCell(address, opts.roles);
|
|
56376
58515
|
const cells = [String(item.row.line), state, item.subject.label, address, destination, authority];
|
|
56377
58516
|
const lamports = opts.lamports?.get(address);
|
|
56378
|
-
cells.push(lamports === undefined ? "unread" :
|
|
58517
|
+
cells.push(lamports === undefined ? "unread" : formatSol4(lamports));
|
|
56379
58518
|
if (opts.tokenCounts !== undefined)
|
|
56380
58519
|
cells.push(String(opts.tokenCounts.get(address) ?? 0));
|
|
56381
58520
|
if (opts.hasValueUsd)
|
|
@@ -56414,7 +58553,7 @@ function renderFooter(opts) {
|
|
|
56414
58553
|
else
|
|
56415
58554
|
total += value;
|
|
56416
58555
|
}
|
|
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: ${
|
|
58556
|
+
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
58557
|
}
|
|
56419
58558
|
const accepted = opts.acting.filter((item) => item.kind === "promote" && item.acceptedUnknown);
|
|
56420
58559
|
if (accepted.length > 0) {
|
|
@@ -56439,10 +58578,12 @@ function finish(ctx, opts) {
|
|
|
56439
58578
|
const resumed = opts.keys.filter((r) => r.state === "resumed").length;
|
|
56440
58579
|
const skipped = opts.keys.filter((r) => r.state === "skipped").length;
|
|
56441
58580
|
const unverified = opts.keys.filter((r) => r.state !== "skipped" && r.remoteAuthority !== "verified-active");
|
|
56442
|
-
const
|
|
58581
|
+
const rebindFailed = opts.rebound?.run !== undefined && !opts.rebound.run.ok;
|
|
58582
|
+
const exit = rebindFailed ? Math.max(opts.exit, 1) : opts.exit;
|
|
58583
|
+
const complete = exit === 0;
|
|
56443
58584
|
if (ctx.json) {
|
|
56444
58585
|
writeJson(ctx.deps, {
|
|
56445
|
-
ok:
|
|
58586
|
+
ok: !rebindFailed,
|
|
56446
58587
|
complete,
|
|
56447
58588
|
file: opts.file,
|
|
56448
58589
|
rows: opts.rows,
|
|
@@ -56450,11 +58591,12 @@ function finish(ctx, opts) {
|
|
|
56450
58591
|
resumed,
|
|
56451
58592
|
skipped,
|
|
56452
58593
|
destinations: opts.destinations.map(({ label, address, keys }) => ({ label, address, keys })),
|
|
56453
|
-
keys: opts.keys,
|
|
58594
|
+
keys: opts.rebound === undefined ? opts.keys : keysWithRebind(opts.keys, opts.rebound.phase, opts.rebound.run),
|
|
56454
58595
|
controlledBy: controlledByJson(opts.controlledBy),
|
|
56455
|
-
authorities: authoritiesJson(opts.roles)
|
|
58596
|
+
authorities: authoritiesJson(opts.roles),
|
|
58597
|
+
...opts.rebound !== undefined ? rebindJson(opts.rebound.input) : {}
|
|
56456
58598
|
});
|
|
56457
|
-
return
|
|
58599
|
+
return exit;
|
|
56458
58600
|
}
|
|
56459
58601
|
const parts = [];
|
|
56460
58602
|
if (promoted > 0)
|
|
@@ -56465,11 +58607,19 @@ function finish(ctx, opts) {
|
|
|
56465
58607
|
parts.push(`${skipped} already promoted (skipped)`);
|
|
56466
58608
|
ctx.deps.stdout.write(`${parts.join("; ")}.
|
|
56467
58609
|
`);
|
|
56468
|
-
if (
|
|
58610
|
+
if (unverified.length > 0) {
|
|
56469
58611
|
ctx.deps.stdout.write(`${unverified.length} import${unverified.length === 1 ? "" : "s"} did not report verified-active: ${unverified.map((r) => r.address).join(", ")}
|
|
56470
58612
|
`);
|
|
56471
58613
|
}
|
|
56472
|
-
|
|
58614
|
+
if (opts.rebound !== undefined) {
|
|
58615
|
+
const { input, run } = opts.rebound;
|
|
58616
|
+
const moved = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "rebound").length;
|
|
58617
|
+
const already = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "unchanged").length;
|
|
58618
|
+
const pending = input.wallets.length - moved - already;
|
|
58619
|
+
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` : ""}.
|
|
58620
|
+
`);
|
|
58621
|
+
}
|
|
58622
|
+
return exit;
|
|
56473
58623
|
}
|
|
56474
58624
|
|
|
56475
58625
|
// src/commands/vault-rename.ts
|
|
@@ -57555,8 +59705,78 @@ function describeEnvelope(envelope, facts) {
|
|
|
57555
59705
|
|
|
57556
59706
|
// src/commands/vault-transfer.ts
|
|
57557
59707
|
init_args();
|
|
59708
|
+
init_deps();
|
|
57558
59709
|
init_promote_support();
|
|
57559
59710
|
init_store();
|
|
59711
|
+
|
|
59712
|
+
// src/vault/tee-transfer.ts
|
|
59713
|
+
init_errors();
|
|
59714
|
+
function assertNoPendingSweep(entry) {
|
|
59715
|
+
const pending = entry.tee?.sweepPending?.length ?? 0;
|
|
59716
|
+
if (pending === 0)
|
|
59717
|
+
return;
|
|
59718
|
+
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>` });
|
|
59719
|
+
}
|
|
59720
|
+
async function readTeeServerState(ctx, entry, apiKey) {
|
|
59721
|
+
if (entry.linkedWalletId === undefined) {
|
|
59722
|
+
return { read: false, reason: "this wallet has no linked wallet id recorded" };
|
|
59723
|
+
}
|
|
59724
|
+
if (apiKey === undefined)
|
|
59725
|
+
return { read: false, reason: "no API key for this profile" };
|
|
59726
|
+
try {
|
|
59727
|
+
const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/lifecycle`, {
|
|
59728
|
+
auth: "key",
|
|
59729
|
+
credentials: { apiKey },
|
|
59730
|
+
apiUrl: ctx.apiUrl,
|
|
59731
|
+
fetch: ctx.deps.fetch,
|
|
59732
|
+
env: ctx.deps.env
|
|
59733
|
+
});
|
|
59734
|
+
if (!result.ok)
|
|
59735
|
+
return { read: false, reason: result.message };
|
|
59736
|
+
const state = result.body.state;
|
|
59737
|
+
if (typeof state !== "string")
|
|
59738
|
+
return { read: false, reason: "the lifecycle response carried no state" };
|
|
59739
|
+
return { read: true, state };
|
|
59740
|
+
} catch (error) {
|
|
59741
|
+
return { read: false, reason: error instanceof Error ? error.message : String(error) };
|
|
59742
|
+
}
|
|
59743
|
+
}
|
|
59744
|
+
function serverStateNotice(state) {
|
|
59745
|
+
if (!state.read) {
|
|
59746
|
+
return `Could not read this wallet's Candle state (${state.reason}); proceeding. An agent may be trading it.`;
|
|
59747
|
+
}
|
|
59748
|
+
if (state.state === "enabled") {
|
|
59749
|
+
return "Warning: this wallet is enabled, so an agent may be trading it now; a trade may fail if this transfer leaves too little.";
|
|
59750
|
+
}
|
|
59751
|
+
return;
|
|
59752
|
+
}
|
|
59753
|
+
async function reportTransferActivity(ctx, apiKey, signature) {
|
|
59754
|
+
const unseen = "Candle's history will not show this transfer.";
|
|
59755
|
+
if (apiKey === undefined)
|
|
59756
|
+
return { outcome: "no-api-key", line: `No API key for this profile, so ${unseen}` };
|
|
59757
|
+
try {
|
|
59758
|
+
const result = await apiRequest("/api/v1/activity/report", {
|
|
59759
|
+
method: "POST",
|
|
59760
|
+
body: { chain: "solana", signature },
|
|
59761
|
+
auth: "key",
|
|
59762
|
+
credentials: { apiKey },
|
|
59763
|
+
apiUrl: ctx.apiUrl,
|
|
59764
|
+
fetch: ctx.deps.fetch,
|
|
59765
|
+
env: ctx.deps.env
|
|
59766
|
+
});
|
|
59767
|
+
if (result.ok)
|
|
59768
|
+
return { outcome: "reported", line: "Reported to Candle's history." };
|
|
59769
|
+
if (result.code === "SCOPE_MISSING") {
|
|
59770
|
+
return { outcome: "scope-missing", line: `This profile's API key lacks activity:write, so ${unseen}` };
|
|
59771
|
+
}
|
|
59772
|
+
return { outcome: "failed", line: `Could not report this transfer (${result.message}); ${unseen}` };
|
|
59773
|
+
} catch (error) {
|
|
59774
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
59775
|
+
return { outcome: "failed", line: `Could not report this transfer (${reason}); ${unseen}` };
|
|
59776
|
+
}
|
|
59777
|
+
}
|
|
59778
|
+
|
|
59779
|
+
// src/commands/vault-transfer.ts
|
|
57560
59780
|
init_vault_support();
|
|
57561
59781
|
async function vaultTransfer(args, ctx) {
|
|
57562
59782
|
const parsed = parseArgs(args, {
|
|
@@ -57597,11 +59817,17 @@ async function vaultTransfer(args, ctx) {
|
|
|
57597
59817
|
promptText: "Vault passphrase (input hidden): "
|
|
57598
59818
|
});
|
|
57599
59819
|
const vault = hold(opened.vault);
|
|
57600
|
-
const fromEntry =
|
|
59820
|
+
const fromEntry = findTransferSource(vault.index, fromLabel);
|
|
57601
59821
|
if (fromEntry === undefined) {
|
|
57602
|
-
return usage(ctx, `No vault key matches --from ${fromLabel}.`);
|
|
59822
|
+
return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
|
|
57603
59823
|
}
|
|
57604
|
-
|
|
59824
|
+
assertTransferSigner(fromEntry);
|
|
59825
|
+
const promoted = fromEntry.role === "tee-wallet";
|
|
59826
|
+
if (promoted)
|
|
59827
|
+
assertNoPendingSweep(fromEntry);
|
|
59828
|
+
const note = (line) => (ctx.json ? ctx.deps.stderr : ctx.deps.stdout).write(`${line}
|
|
59829
|
+
`);
|
|
59830
|
+
const apiKey = promoted ? await resolveApiKey(ctx.deps, ctx.profile) : undefined;
|
|
57605
59831
|
const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], rpcUrl2, ctx);
|
|
57606
59832
|
if (reconciled !== null)
|
|
57607
59833
|
return reconciled;
|
|
@@ -57615,11 +59841,25 @@ async function vaultTransfer(args, ctx) {
|
|
|
57615
59841
|
});
|
|
57616
59842
|
const feeQuote = await quoteTransferFee(rpcUrl2, ctx.deps.fetch, plan.from, plan.instructions);
|
|
57617
59843
|
displayTransferPlan(ctx, plan, feeQuote);
|
|
59844
|
+
if (promoted) {
|
|
59845
|
+
const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
|
|
59846
|
+
if (notice !== undefined)
|
|
59847
|
+
note(notice);
|
|
59848
|
+
}
|
|
57618
59849
|
await confirmLastSix(ctx, to, "the destination");
|
|
57619
59850
|
await opened.confirm(`sign transfer of ${plan.amount} ${plan.asset} to ${to}`);
|
|
57620
59851
|
const secret = await decryptKey(vault, fromEntry.id);
|
|
57621
59852
|
try {
|
|
57622
59853
|
const result = await signAndBroadcastTransfer({ ctx, rpcUrl: rpcUrl2, secret64: secret, plan });
|
|
59854
|
+
let activityReport;
|
|
59855
|
+
if (promoted && result.finalized) {
|
|
59856
|
+
const report = await reportTransferActivity(ctx, apiKey, result.signature);
|
|
59857
|
+
activityReport = report.outcome;
|
|
59858
|
+
note(report.line);
|
|
59859
|
+
} else if (promoted) {
|
|
59860
|
+
activityReport = "not-finalized";
|
|
59861
|
+
note("Not reported to Candle's history: finality is not yet confirmed.");
|
|
59862
|
+
}
|
|
57623
59863
|
if (ctx.json) {
|
|
57624
59864
|
writeJson(ctx.deps, {
|
|
57625
59865
|
ok: result.finalized,
|
|
@@ -57629,7 +59869,8 @@ async function vaultTransfer(args, ctx) {
|
|
|
57629
59869
|
amount: plan.amount,
|
|
57630
59870
|
asset: plan.asset,
|
|
57631
59871
|
amountRaw: plan.amountRaw.toString(),
|
|
57632
|
-
finalized: result.finalized
|
|
59872
|
+
finalized: result.finalized,
|
|
59873
|
+
...activityReport !== undefined ? { activityReport } : {}
|
|
57633
59874
|
});
|
|
57634
59875
|
} else {
|
|
57635
59876
|
ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
|
|
@@ -58063,6 +60304,9 @@ function extractGlobalFlags(argv) {
|
|
|
58063
60304
|
var COMMANDS = {
|
|
58064
60305
|
swap: { bare: swap, subcommands: { status: swapStatus } },
|
|
58065
60306
|
launch: { bare: launch },
|
|
60307
|
+
pnl: { bare: pnl },
|
|
60308
|
+
portfolio: { bare: portfolio },
|
|
60309
|
+
lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
|
|
58066
60310
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
58067
60311
|
keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
|
|
58068
60312
|
wallets: {
|