@candledottv/agent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1186 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/authorization-signature.ts
5
+ import canonicalize from "canonicalize";
6
+
7
+ // src/internal/encoding.ts
8
+ function toArrayBuffer(view) {
9
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
10
+ }
11
+ function arrayBufferToBase64(data) {
12
+ return Buffer.from(data).toString("base64");
13
+ }
14
+ function base64ToArrayBuffer(base64) {
15
+ const buf = Buffer.from(base64, "base64");
16
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
17
+ }
18
+ function toBase64(bytes) {
19
+ return Buffer.from(bytes).toString("base64");
20
+ }
21
+ function fromBase64(base64) {
22
+ return new Uint8Array(Buffer.from(base64, "base64"));
23
+ }
24
+
25
+ // src/authorization-signature.ts
26
+ var PRIVY_API_BASE = "https://api.privy.io";
27
+ function canonicalAuthorizationPayload(params) {
28
+ return {
29
+ version: 1,
30
+ method: "POST",
31
+ url: `${PRIVY_API_BASE}/v1/wallets/${params.privyWalletId}/rpc`,
32
+ body: params.body,
33
+ headers: { "privy-app-id": params.appId }
34
+ };
35
+ }
36
+ function canonicalAuthorizationPayloadBytes(params) {
37
+ const json = canonicalize(canonicalAuthorizationPayload(params));
38
+ if (json === undefined) {
39
+ throw new Error("Failed to canonicalize the Privy authorization payload");
40
+ }
41
+ return new TextEncoder().encode(json);
42
+ }
43
+ function pemToPkcs8Bytes(pem) {
44
+ const base64 = pem.split(`
45
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("-----")).join("");
46
+ return Uint8Array.from(Buffer.from(base64, "base64"));
47
+ }
48
+ function derEncodeUnsignedInteger(bytes) {
49
+ let start = 0;
50
+ while (start < bytes.length - 1 && bytes[start] === 0)
51
+ start++;
52
+ const trimmed = bytes.slice(start);
53
+ const needsPad = ((trimmed[0] ?? 0) & 128) !== 0;
54
+ const value = needsPad ? Uint8Array.from([0, ...trimmed]) : trimmed;
55
+ return Uint8Array.from([2, value.length, ...value]);
56
+ }
57
+ function rawEcdsaSignatureToDer(raw) {
58
+ const half = raw.length / 2;
59
+ const r = derEncodeUnsignedInteger(raw.slice(0, half));
60
+ const s = derEncodeUnsignedInteger(raw.slice(half));
61
+ return Uint8Array.from([48, r.length + s.length, ...r, ...s]);
62
+ }
63
+ async function importPkcs8SigningKey(privateKeyPem) {
64
+ return crypto.subtle.importKey("pkcs8", toArrayBuffer(pemToPkcs8Bytes(privateKeyPem)), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
65
+ }
66
+ async function buildPrivyAuthorizationSignature(params) {
67
+ const payloadBytes = canonicalAuthorizationPayloadBytes(params);
68
+ const key = await importPkcs8SigningKey(params.privateKeyPem);
69
+ const rawSignature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, toArrayBuffer(payloadBytes));
70
+ const derSignature = rawEcdsaSignatureToDer(new Uint8Array(rawSignature));
71
+ return arrayBufferToBase64(toArrayBuffer(derSignature));
72
+ }
73
+
74
+ // src/errors.ts
75
+ class CandleApiError extends Error {
76
+ code;
77
+ status;
78
+ retryable;
79
+ field;
80
+ constructor(args) {
81
+ super(args.message);
82
+ this.name = "CandleApiError";
83
+ this.code = args.code;
84
+ this.status = args.status;
85
+ this.retryable = args.retryable;
86
+ if (args.field !== undefined)
87
+ this.field = args.field;
88
+ }
89
+ }
90
+
91
+ class JsonRpcError extends Error {
92
+ code;
93
+ data;
94
+ constructor(args) {
95
+ super(args.message);
96
+ this.name = "JsonRpcError";
97
+ this.code = args.code;
98
+ this.data = args.data;
99
+ }
100
+ }
101
+ function envelopeError(body) {
102
+ if (typeof body !== "object" || body === null)
103
+ return null;
104
+ const candidate = body;
105
+ if (candidate.success !== false)
106
+ return null;
107
+ if (typeof candidate.error !== "object" || candidate.error === null)
108
+ return null;
109
+ const error = candidate.error;
110
+ if (typeof error.code !== "string" || typeof error.message !== "string")
111
+ return null;
112
+ return {
113
+ code: error.code,
114
+ message: error.message,
115
+ ...typeof error.field === "string" ? { field: error.field } : {},
116
+ ...typeof error.retryable === "boolean" ? { retryable: error.retryable } : {}
117
+ };
118
+ }
119
+ function candleApiErrorFromResponse(status, bodyText) {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(bodyText);
123
+ } catch {
124
+ parsed = undefined;
125
+ }
126
+ const payload = envelopeError(parsed);
127
+ if (payload) {
128
+ return new CandleApiError({
129
+ code: payload.code,
130
+ message: payload.message,
131
+ status,
132
+ retryable: payload.retryable === true,
133
+ ...payload.field !== undefined ? { field: payload.field } : {}
134
+ });
135
+ }
136
+ return new CandleApiError({
137
+ code: `HTTP_${status}`,
138
+ message: bodyText || `HTTP ${status}`,
139
+ status,
140
+ retryable: false
141
+ });
142
+ }
143
+
144
+ // src/evm-tx.ts
145
+ var GAS_BUFFER_NUMERATOR = 12n;
146
+ var GAS_BUFFER_DENOMINATOR = 10n;
147
+ var DEFAULT_RECEIPT_TIMEOUT_MS = 120000;
148
+ var DEFAULT_RECEIPT_POLL_MS = 2000;
149
+ function sleep(ms) {
150
+ return new Promise((resolve) => setTimeout(resolve, ms));
151
+ }
152
+ function hexToBigInt(hex) {
153
+ return BigInt(hex);
154
+ }
155
+ function bigIntToHexQuantity(n) {
156
+ if (n < 0n) {
157
+ throw new Error(`bigIntToHexQuantity(): negative values are not valid hex quantities: ${n}`);
158
+ }
159
+ return `0x${n.toString(16)}`;
160
+ }
161
+ function decimalToHexQuantity(decimalString) {
162
+ return bigIntToHexQuantity(BigInt(decimalString));
163
+ }
164
+ async function fetchNonce(rpc, from) {
165
+ const hex = await rpc.call("eth_getTransactionCount", [from, "pending"]);
166
+ return Number(hexToBigInt(hex));
167
+ }
168
+ async function fetchChainId(rpc) {
169
+ const hex = await rpc.call("eth_chainId", []);
170
+ return Number(hexToBigInt(hex));
171
+ }
172
+ async function fetchFeeData(rpc) {
173
+ let priorityHex;
174
+ try {
175
+ priorityHex = await rpc.call("eth_maxPriorityFeePerGas", []);
176
+ } catch {
177
+ priorityHex = await rpc.call("eth_gasPrice", []);
178
+ }
179
+ const priority = hexToBigInt(priorityHex);
180
+ const block = await rpc.callRaw("eth_getBlockByNumber", ["latest", false]);
181
+ const baseFeePerGas = block?.baseFeePerGas;
182
+ if (typeof baseFeePerGas !== "string") {
183
+ throw new Error('fetchFeeData(): eth_getBlockByNumber("latest", false) response is missing baseFeePerGas -- ' + "this RPC node may not support EIP-1559");
184
+ }
185
+ const baseFee = hexToBigInt(baseFeePerGas);
186
+ const maxFeePerGas = baseFee * 2n + priority;
187
+ return {
188
+ maxFeePerGasHex: bigIntToHexQuantity(maxFeePerGas),
189
+ maxPriorityFeePerGasHex: bigIntToHexQuantity(priority)
190
+ };
191
+ }
192
+ async function estimateGas(rpc, params) {
193
+ const callParams = {
194
+ from: params.from,
195
+ to: params.to,
196
+ data: params.data
197
+ };
198
+ if (params.value !== undefined)
199
+ callParams.value = params.value;
200
+ const raw = await rpc.call("eth_estimateGas", [callParams]);
201
+ const buffered = hexToBigInt(raw) * GAS_BUFFER_NUMERATOR / GAS_BUFFER_DENOMINATOR;
202
+ return bigIntToHexQuantity(buffered);
203
+ }
204
+ function assembleEvmTx(input) {
205
+ return {
206
+ from: input.from,
207
+ to: input.to,
208
+ nonce: input.nonce,
209
+ chain_id: input.chainId,
210
+ data: input.data,
211
+ value: decimalToHexQuantity(input.valueDecimal),
212
+ type: 2,
213
+ gas_limit: input.gasLimitHex,
214
+ max_fee_per_gas: input.feeData.maxFeePerGasHex,
215
+ max_priority_fee_per_gas: input.feeData.maxPriorityFeePerGasHex
216
+ };
217
+ }
218
+ async function waitForReceipt(rpc, txHash, opts = {}) {
219
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_RECEIPT_TIMEOUT_MS;
220
+ const pollMs = opts.pollMs ?? DEFAULT_RECEIPT_POLL_MS;
221
+ const deadline = Date.now() + timeoutMs;
222
+ for (;; ) {
223
+ const receipt = await rpc.callRaw("eth_getTransactionReceipt", [txHash]);
224
+ if (receipt) {
225
+ if (receipt.status === "0x0") {
226
+ throw new Error(`waitForReceipt("${txHash}"): transaction reverted (receipt status 0x0)`);
227
+ }
228
+ return receipt;
229
+ }
230
+ if (Date.now() >= deadline) {
231
+ throw new Error(`waitForReceipt("${txHash}") timed out after ${timeoutMs}ms waiting for a transaction receipt`);
232
+ }
233
+ await sleep(pollMs);
234
+ }
235
+ }
236
+
237
+ // src/wallet-import.ts
238
+ import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
239
+ import { CipherSuite, DhkemP256HkdfSha256, HkdfSha256 } from "@hpke/core";
240
+ import { base58 } from "@scure/base";
241
+ function buildCipherSuite() {
242
+ return new CipherSuite({
243
+ kem: new DhkemP256HkdfSha256,
244
+ kdf: new HkdfSha256,
245
+ aead: new Chacha20Poly1305
246
+ });
247
+ }
248
+ function parseSolanaSecret(input) {
249
+ const trimmed = input.trim();
250
+ if (!trimmed.startsWith("[")) {
251
+ try {
252
+ return base58.decode(trimmed);
253
+ } catch {
254
+ throw new Error("Invalid Solana private key: expected a base58 string or an id.json byte array.");
255
+ }
256
+ }
257
+ const parsed = (() => {
258
+ try {
259
+ return JSON.parse(trimmed);
260
+ } catch {
261
+ return;
262
+ }
263
+ })();
264
+ const isByteArray = Array.isArray(parsed) && parsed.length === 64 && parsed.every((value) => Number.isInteger(value) && value >= 0 && value <= 255);
265
+ if (!isByteArray) {
266
+ throw new Error("This looks like a Solana keyfile (id.json) but is not a 64-byte array. Pass the file's contents, e.g. [12,34,...].");
267
+ }
268
+ return Uint8Array.from(parsed);
269
+ }
270
+ function decodeWalletPrivateKey(chain, privateKey) {
271
+ if (chain === "evm") {
272
+ const hex = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
273
+ if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
274
+ throw new Error('Invalid EVM private key: expected a hex string (optionally "0x"-prefixed)');
275
+ }
276
+ return Uint8Array.from(Buffer.from(hex, "hex"));
277
+ }
278
+ return parseSolanaSecret(privateKey);
279
+ }
280
+ async function encryptWalletKeyForImport(params) {
281
+ const plaintext = decodeWalletPrivateKey(params.chain, params.privateKey);
282
+ const suite = buildCipherSuite();
283
+ const recipientPublicKey = await suite.kem.deserializePublicKey(base64ToArrayBuffer(params.encryptionPublicKey));
284
+ const sender = await suite.createSenderContext({ recipientPublicKey });
285
+ const ciphertext = await sender.seal(toArrayBuffer(plaintext));
286
+ return {
287
+ ciphertext: arrayBufferToBase64(ciphertext),
288
+ encapsulatedKey: arrayBufferToBase64(sender.enc)
289
+ };
290
+ }
291
+ async function generateSignerKeypair() {
292
+ const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
293
+ const [publicKeyDer, privateKeyDer] = await Promise.all([
294
+ crypto.subtle.exportKey("spki", keyPair.publicKey),
295
+ crypto.subtle.exportKey("pkcs8", keyPair.privateKey)
296
+ ]);
297
+ return {
298
+ privateKeyPem: derToPem(privateKeyDer, "PRIVATE KEY"),
299
+ publicKeyDerBase64: arrayBufferToBase64(publicKeyDer)
300
+ };
301
+ }
302
+ function derToPem(der, label) {
303
+ const base64 = arrayBufferToBase64(der);
304
+ const lines = base64.match(/.{1,64}/g) ?? [base64];
305
+ return `-----BEGIN ${label}-----
306
+ ${lines.join(`
307
+ `)}
308
+ -----END ${label}-----
309
+ `;
310
+ }
311
+
312
+ // src/client.ts
313
+ var BACKOFF_BASE_MS = 250;
314
+ var BACKOFF_CAP_MS = 8000;
315
+ function retryDelayMs(retry) {
316
+ const base = Math.min(BACKOFF_BASE_MS * 2 ** retry, BACKOFF_CAP_MS);
317
+ return base / 2 + Math.random() * (base / 2);
318
+ }
319
+ function isRetryableLaunchFailure(error) {
320
+ if (!(error instanceof CandleApiError))
321
+ return true;
322
+ if (error.code.startsWith("HTTP_"))
323
+ return error.status >= 500;
324
+ if (!error.retryable)
325
+ return false;
326
+ return error.status >= 500 || error.status === 409;
327
+ }
328
+ function sleep2(ms) {
329
+ return new Promise((resolve) => setTimeout(resolve, ms));
330
+ }
331
+ var MAX_BLOCKHASH_REBUILDS = 2;
332
+ function isBlockhashExpiry(error) {
333
+ if (!(error instanceof JsonRpcError))
334
+ return false;
335
+ const data = error.data;
336
+ if (typeof data === "object" && data !== null && "err" in data && data.err === "BlockhashNotFound") {
337
+ return true;
338
+ }
339
+ return /blockhash not found|block height exceeded/i.test(error.message);
340
+ }
341
+ function withRpcLagHint(error) {
342
+ return new JsonRpcError({
343
+ code: error.code,
344
+ message: `${error.message} -- this transaction was rebuilt with a fresh blockhash ${MAX_BLOCKHASH_REBUILDS} times ` + "and still failed at broadcast, which usually means the configured Solana RPC is lagging or " + "rate-limited. Point solanaRpcUrl at a fast endpoint (for example Helius).",
345
+ data: error.data
346
+ });
347
+ }
348
+ function formatJsonRpcErrorMessage(method, url, rpcError) {
349
+ const base = `JSON-RPC ${method} against ${url} was rejected (code ${rpcError.code}): ${rpcError.message}`;
350
+ const data = rpcError.data;
351
+ if (typeof data !== "object" || data === null)
352
+ return base;
353
+ const d = data;
354
+ const parts = [];
355
+ if (d.err !== undefined) {
356
+ parts.push(`err: ${typeof d.err === "string" ? d.err : JSON.stringify(d.err)}`);
357
+ }
358
+ if (Array.isArray(d.logs) && d.logs.length > 0) {
359
+ parts.push(`logs: ${d.logs.slice(0, 3).join(" | ")}`);
360
+ }
361
+ return parts.length > 0 ? `${base} [${parts.join("; ")}]` : base;
362
+ }
363
+ function toAtomicWirePayer(payer) {
364
+ return payer.type === "main" ? { type: "main" } : { type: "linked", linkedWalletId: payer.linkedWalletId };
365
+ }
366
+ function isAtomicSubmitOutcome(value) {
367
+ if (typeof value !== "object" || value === null)
368
+ return false;
369
+ const v = value;
370
+ if (typeof v.bundleId !== "string")
371
+ return false;
372
+ if (v.status === "failed" || v.status === "timeout")
373
+ return typeof v.retryable === "boolean";
374
+ if (v.status === "landed") {
375
+ return typeof v.mint === "string" && Array.isArray(v.signatures) && v.signatures.every((s) => typeof s === "string");
376
+ }
377
+ return false;
378
+ }
379
+ var DEFAULT_MAX_RETRIES = 3;
380
+ var DEFAULT_POLL_MS = 2000;
381
+ var DEFAULT_WAIT_TIMEOUT_MS = 180000;
382
+
383
+ class CandleClient {
384
+ apiUrl;
385
+ apiKey;
386
+ fetchImpl;
387
+ maxRetries;
388
+ privyAppId;
389
+ secretStore;
390
+ solanaRpcUrl;
391
+ evmRpcUrl;
392
+ constructor(opts) {
393
+ this.apiUrl = opts.apiUrl.replace(/\/+$/, "");
394
+ if (opts.apiKey !== undefined)
395
+ this.apiKey = opts.apiKey;
396
+ this.fetchImpl = opts.fetch ?? fetch;
397
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
398
+ if (opts.privyAppId !== undefined)
399
+ this.privyAppId = opts.privyAppId;
400
+ if (opts.secretStore !== undefined)
401
+ this.secretStore = opts.secretStore;
402
+ if (opts.solanaRpcUrl !== undefined)
403
+ this.solanaRpcUrl = opts.solanaRpcUrl;
404
+ if (opts.evmRpcUrl !== undefined)
405
+ this.evmRpcUrl = opts.evmRpcUrl;
406
+ }
407
+ async getQuotePairs(chain) {
408
+ const query = chain ? `?chain=${chain}` : "";
409
+ const body = await this.requestJson("GET", `/api/v1/launch/quote-pairs${query}`);
410
+ return body.payload;
411
+ }
412
+ async getPresets() {
413
+ const body = await this.requestJson("GET", "/api/v1/launch/presets");
414
+ return body.payload;
415
+ }
416
+ expandPreset(presets, name, overrides = {}) {
417
+ const preset = presets.presets.find((p) => p.name === name);
418
+ if (!preset) {
419
+ const known = presets.presets.map((p) => p.name).join(", ");
420
+ throw new Error(`Unknown preset "${name}". Known presets: ${known}`);
421
+ }
422
+ return {
423
+ chain: preset.chain,
424
+ quoteAsset: preset.quoteAsset,
425
+ mode: preset.mode,
426
+ stakerAllocationBps: preset.stakerAllocationBps,
427
+ ...preset.dexVersion ? { dexVersion: preset.dexVersion } : {},
428
+ ...overrides
429
+ };
430
+ }
431
+ async getMarket(chain, mint) {
432
+ const body = await this.requestJson("GET", `/api/v1/markets/${chain}/${encodeURIComponent(mint)}`);
433
+ return body.market;
434
+ }
435
+ async getQuote(chain, mint, q) {
436
+ const params = new URLSearchParams({ side: q.side, amountIn: q.amountIn });
437
+ if (q.slippageBps !== undefined)
438
+ params.set("slippageBps", String(q.slippageBps));
439
+ return this.requestJson("GET", `/api/v1/markets/${chain}/${encodeURIComponent(mint)}/quote?${params.toString()}`);
440
+ }
441
+ async getFeed(bucket, chain) {
442
+ const params = new URLSearchParams({ bucket, ...chain ? { chain } : {} });
443
+ return this.requestJson("GET", `/api/v1/markets/feed?${params.toString()}`);
444
+ }
445
+ async verify(chain, mint) {
446
+ return this.requestJson("GET", `/api/v1/verify/${chain}/${encodeURIComponent(mint)}`);
447
+ }
448
+ async getAgentProfile(idOrWallet) {
449
+ const body = await this.requestJson("GET", `/api/v1/users/${encodeURIComponent(idOrWallet)}/agent`);
450
+ return body.agent;
451
+ }
452
+ async getAgentTier() {
453
+ return this.requestJson("GET", "/api/v1/agent/tier");
454
+ }
455
+ async dryRunLaunch(req) {
456
+ this.requireKey("dryRunLaunch()");
457
+ return this.requestJson("POST", "/api/v1/launch/headless/dry-run", req);
458
+ }
459
+ async launch(req) {
460
+ this.requireKey("launch()");
461
+ const body = { ...req, clientLaunchId: req.clientLaunchId ?? generateClientLaunchId() };
462
+ let lastError;
463
+ for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
464
+ if (attempt > 0)
465
+ await sleep2(retryDelayMs(attempt - 1));
466
+ try {
467
+ return await this.requestJson("POST", "/api/v1/launch/headless", body);
468
+ } catch (error) {
469
+ if (!isRetryableLaunchFailure(error))
470
+ throw error;
471
+ lastError = error;
472
+ }
473
+ }
474
+ throw lastError;
475
+ }
476
+ async launchAsync(req) {
477
+ this.requireKey("launchAsync()");
478
+ const body = { ...req, clientLaunchId: req.clientLaunchId ?? generateClientLaunchId(), async: true };
479
+ return this.requestJson("POST", "/api/v1/launch/headless", body);
480
+ }
481
+ async getLaunchJob(clientLaunchId) {
482
+ this.requireKey("getLaunchJob()");
483
+ const body = await this.requestJson("GET", `/api/v1/launch/headless/jobs/${encodeURIComponent(clientLaunchId)}`);
484
+ return body.job;
485
+ }
486
+ async waitForLaunch(clientLaunchId, opts = {}) {
487
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
488
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
489
+ const deadline = Date.now() + timeoutMs;
490
+ for (;; ) {
491
+ const job = await this.getLaunchJob(clientLaunchId);
492
+ if (job.status === "confirmed" || job.status === "failed")
493
+ return job;
494
+ if (Date.now() >= deadline) {
495
+ throw new Error(`waitForLaunch("${clientLaunchId}") timed out after ${timeoutMs}ms (last status: ${job.status})`);
496
+ }
497
+ await sleep2(pollMs);
498
+ }
499
+ }
500
+ async reportActivity(chain, signature) {
501
+ this.requireKey("reportActivity()");
502
+ return this.requestJson("POST", "/api/v1/activity/report", { chain, signature });
503
+ }
504
+ async uploadImage(bytes, contentType) {
505
+ this.requireKey("uploadImage()");
506
+ const res = await this.fetchImpl(`${this.apiUrl}/api/v1/uploads/agent-image`, {
507
+ method: "POST",
508
+ headers: this.headers({ contentType }),
509
+ body: bytes
510
+ });
511
+ const body = await this.parseResponse(res);
512
+ return { imageUrl: body.imageUrl };
513
+ }
514
+ async listWallets(opts = {}) {
515
+ this.requireKey("listWallets()");
516
+ const query = opts.includeRevoked === true ? "?includeRevoked=true" : "";
517
+ return this.requestJson("GET", `/api/v1/agent/wallets${query}`);
518
+ }
519
+ async getSpendLimits() {
520
+ this.requireKey("getSpendLimits()");
521
+ return this.requestJson("GET", "/api/v1/agent/keys/self/limits");
522
+ }
523
+ async swap(req) {
524
+ this.requireKey("swap()");
525
+ const body = await this.requestJson("POST", "/api/v1/agent/swap", req);
526
+ return body.payload;
527
+ }
528
+ async importWallet(params) {
529
+ this.requireKey("importWallet()");
530
+ const init = await this.requestJson("POST", "/api/v1/agent/wallets/import/init", { chain: params.chain, address: params.address });
531
+ const { ciphertext, encapsulatedKey } = await encryptWalletKeyForImport({
532
+ chain: params.chain,
533
+ privateKey: params.privateKey,
534
+ encryptionPublicKey: init.encryptionPublicKey
535
+ });
536
+ return this.requestJson("POST", "/api/v1/agent/wallets/import/submit", {
537
+ chain: params.chain,
538
+ address: params.address,
539
+ ciphertext,
540
+ encapsulatedKey,
541
+ signerPublicKey: params.signerPublicKey,
542
+ ...params.label !== undefined ? { label: params.label } : {}
543
+ });
544
+ }
545
+ async buildSelfLaunch(req) {
546
+ this.requireKey("buildSelfLaunch()");
547
+ return this.requestJson("POST", "/api/v1/launch/self/build", req);
548
+ }
549
+ async confirmSelfLaunch(req) {
550
+ this.requireKey("confirmSelfLaunch()");
551
+ return this.requestJson("POST", "/api/v1/launch/self/confirm", req);
552
+ }
553
+ async buildTrade(req) {
554
+ this.requireKey("buildTrade()");
555
+ return this.requestJson("POST", "/api/v1/trade/agent/build", req);
556
+ }
557
+ async confirmTrade(req) {
558
+ this.requireKey("confirmTrade()");
559
+ return this.requestJson("POST", "/api/v1/trade/agent/confirm", req);
560
+ }
561
+ async submit(req) {
562
+ this.requireKey("submit()");
563
+ return this.requestJson("POST", "/api/v1/trade/agent/submit", req);
564
+ }
565
+ async signLinkedTransaction(params) {
566
+ this.requireKey("signLinkedTransaction()");
567
+ if (!this.privyAppId) {
568
+ throw new Error("signLinkedTransaction() requires privyAppId: pass one in CandleClientOptions " + "(new CandleClient({ privyAppId })) -- the same Privy app id the sign relay authenticates under");
569
+ }
570
+ if (!this.secretStore) {
571
+ throw new Error("signLinkedTransaction() requires a secretStore: pass one in CandleClientOptions " + "(new CandleClient({ secretStore }))");
572
+ }
573
+ if (params.chain === "solana" && !params.unsignedTransactionBase64) {
574
+ throw new Error('signLinkedTransaction() for chain "solana" requires unsignedTransactionBase64');
575
+ }
576
+ if (params.chain === "evm" && !params.evmTxParams) {
577
+ throw new Error('signLinkedTransaction() for chain "evm" requires evmTxParams');
578
+ }
579
+ const privateKeyPem = await this.secretStore.get(params.linkedWalletId);
580
+ if (!privateKeyPem) {
581
+ throw new Error(`signLinkedTransaction(): no signer key stored for linked wallet "${params.linkedWalletId}" -- ` + "import or set one in the configured secretStore first");
582
+ }
583
+ const body = params.chain === "solana" ? { method: "signTransaction", params: { transaction: params.unsignedTransactionBase64, encoding: "base64" } } : { method: "eth_signTransaction", params: { transaction: params.evmTxParams } };
584
+ const authorizationSignature = await buildPrivyAuthorizationSignature({
585
+ privateKeyPem,
586
+ privyWalletId: params.privyWalletId,
587
+ appId: this.privyAppId,
588
+ body
589
+ });
590
+ const res = await this.requestJson("POST", `/api/v1/agent/wallets/${encodeURIComponent(params.linkedWalletId)}/sign`, { authorizationSignature, body });
591
+ return { signedTransaction: res.signedTransaction, encoding: res.encoding };
592
+ }
593
+ async broadcastSignedTransaction(chain, signedTransaction, encoding) {
594
+ if (chain === "solana") {
595
+ if (!this.solanaRpcUrl) {
596
+ throw new Error('broadcastSignedTransaction() for chain "solana" requires solanaRpcUrl: pass one in ' + "CandleClientOptions (new CandleClient({ solanaRpcUrl }))");
597
+ }
598
+ return this.jsonRpcCall(this.solanaRpcUrl, "sendTransaction", [signedTransaction, { encoding }]);
599
+ }
600
+ if (!this.evmRpcUrl) {
601
+ throw new Error('broadcastSignedTransaction() for chain "evm" requires evmRpcUrl: pass one in CandleClientOptions ' + "(new CandleClient({ evmRpcUrl }))");
602
+ }
603
+ return this.jsonRpcCall(this.evmRpcUrl, "eth_sendRawTransaction", [signedTransaction]);
604
+ }
605
+ async swapFromLinked(req) {
606
+ this.requireKey("swapFromLinked()");
607
+ const build = await this.requestJson("POST", "/api/v1/agent/swap/build", {
608
+ from: req.from,
609
+ to: req.to,
610
+ amountRaw: req.amountRaw,
611
+ ...req.maxSlippageBps !== undefined ? { maxSlippageBps: req.maxSlippageBps } : {},
612
+ payer: { type: "linked", linkedWalletId: req.payer.linkedWalletId },
613
+ ...req.toWalletId !== undefined ? { toWalletId: req.toWalletId } : {}
614
+ });
615
+ const signed = [];
616
+ for (const unsignedTransactionBase64 of build.payload.transactionsBase64) {
617
+ const result = await this.signLinkedTransaction({
618
+ chain: "solana",
619
+ linkedWalletId: req.payer.linkedWalletId,
620
+ privyWalletId: req.payer.privyWalletId,
621
+ unsignedTransactionBase64
622
+ });
623
+ signed.push(result.signedTransaction);
624
+ }
625
+ const submit = await this.requestJson("POST", "/api/v1/agent/swap/submit", { swapId: build.payload.swapId, signedTransactionsBase64: signed });
626
+ return submit.payload;
627
+ }
628
+ async trade(req) {
629
+ const clientTradeId = req.clientTradeId ?? generateClientTradeId();
630
+ const buildReq = {
631
+ clientTradeId,
632
+ mint: req.mint,
633
+ side: req.side,
634
+ amountRaw: req.amountRaw,
635
+ payer: req.from === "main" ? { type: "main" } : { type: "linked", linkedWalletId: req.from.linkedWalletId },
636
+ ...req.maxSlippageBps !== undefined ? { maxSlippageBps: req.maxSlippageBps } : {},
637
+ ...req.quoteAsset !== undefined ? { quoteAsset: req.quoteAsset } : {}
638
+ };
639
+ if (req.from === "main") {
640
+ const result = await this.buildTrade(buildReq);
641
+ if (result.status !== "executed") {
642
+ throw new Error(`trade({ from: "main" }) expected an executed result but got status "${result.status}"`);
643
+ }
644
+ return result;
645
+ }
646
+ const { linkedWalletId, privyWalletId } = req.from;
647
+ const built = await this.buildTrade(buildReq);
648
+ if (built.status !== "built") {
649
+ return built;
650
+ }
651
+ if (built.chain === "solana") {
652
+ const signed = await this.signLinkedTransaction({
653
+ linkedWalletId,
654
+ privyWalletId,
655
+ chain: "solana",
656
+ unsignedTransactionBase64: built.artifacts.transactionBase64
657
+ });
658
+ return this.submit({ clientTradeId: built.clientTradeId, signedTransactions: [signed.signedTransaction] });
659
+ }
660
+ if (!this.evmRpcUrl) {
661
+ throw new Error('trade({ from: <linked> }) on chain "hood" requires evmRpcUrl, because hood is an EVM chain: ' + "pass one in CandleClientOptions (new CandleClient({ evmRpcUrl }))");
662
+ }
663
+ const rpc = this.evmRpc();
664
+ const from = built.walletAddress;
665
+ const chainId = await fetchChainId(rpc);
666
+ const baseNonce = await fetchNonce(rpc, from);
667
+ const feeData = await fetchFeeData(rpc);
668
+ const legs = [];
669
+ if (built.artifacts.approval) {
670
+ legs.push({ kind: "approval", to: built.artifacts.approval.to, data: built.artifacts.approval.data, value: "0" });
671
+ }
672
+ legs.push({ kind: "trade", ...built.artifacts.trade });
673
+ if (built.artifacts.feeTransfer) {
674
+ legs.push({ kind: "feeTransfer", ...built.artifacts.feeTransfer });
675
+ }
676
+ let tradeTxHash;
677
+ let feeTxHash;
678
+ for (let i = 0;i < legs.length; i++) {
679
+ const leg = legs[i];
680
+ if (!leg)
681
+ continue;
682
+ const txHash = await this.signBroadcastAndWaitEvmLeg({
683
+ rpc,
684
+ from,
685
+ to: leg.to,
686
+ data: leg.data,
687
+ valueDecimal: leg.value,
688
+ nonce: baseNonce + i,
689
+ chainId,
690
+ feeData,
691
+ linkedWalletId,
692
+ privyWalletId
693
+ });
694
+ if (leg.kind === "trade")
695
+ tradeTxHash = txHash;
696
+ if (leg.kind === "feeTransfer")
697
+ feeTxHash = txHash;
698
+ }
699
+ if (!tradeTxHash) {
700
+ throw new Error("trade(): Hood leg sequence completed without a trade leg");
701
+ }
702
+ return this.confirmTrade({
703
+ clientTradeId: built.clientTradeId,
704
+ tradeTxHash,
705
+ ...feeTxHash ? { feeTxHash } : {}
706
+ });
707
+ }
708
+ async selfLaunch(req) {
709
+ const { privyWalletId, ...launchReq } = req;
710
+ const body = {
711
+ ...launchReq,
712
+ clientLaunchId: launchReq.clientLaunchId ?? generateClientLaunchId()
713
+ };
714
+ const built = await this.buildSelfLaunch(body);
715
+ if (typeof built.transaction === "string") {
716
+ let unsignedTransactionBase64 = built.transaction;
717
+ let clientLaunchId = built.clientLaunchId;
718
+ for (let attempt = 0;attempt <= MAX_BLOCKHASH_REBUILDS; attempt++) {
719
+ const signed = await this.signLinkedTransaction({
720
+ linkedWalletId: body.linkedWalletId,
721
+ privyWalletId,
722
+ chain: "solana",
723
+ unsignedTransactionBase64
724
+ });
725
+ try {
726
+ const signature = await this.broadcastSignedTransaction("solana", signed.signedTransaction, signed.encoding);
727
+ return this.confirmSelfLaunch({ clientLaunchId, signature });
728
+ } catch (error) {
729
+ if (!isBlockhashExpiry(error))
730
+ throw error;
731
+ if (attempt === MAX_BLOCKHASH_REBUILDS)
732
+ throw withRpcLagHint(error);
733
+ const rebuilt = await this.buildSelfLaunch(body);
734
+ if (typeof rebuilt.transaction !== "string")
735
+ throw error;
736
+ unsignedTransactionBase64 = rebuilt.transaction;
737
+ clientLaunchId = rebuilt.clientLaunchId;
738
+ }
739
+ }
740
+ throw new Error("selfLaunch(): blockhash-rebuild loop exited without returning or throwing");
741
+ }
742
+ const hoodBuilt = built;
743
+ if (!this.evmRpcUrl) {
744
+ throw new Error('selfLaunch() on chain "hood" requires evmRpcUrl, because hood is an EVM chain: ' + "pass one in CandleClientOptions (new CandleClient({ evmRpcUrl }))");
745
+ }
746
+ const rpc = this.evmRpc();
747
+ const from = hoodBuilt.walletAddress;
748
+ const chainId = await fetchChainId(rpc);
749
+ const baseNonce = await fetchNonce(rpc, from);
750
+ const feeData = await fetchFeeData(rpc);
751
+ const createCurveTxHash = await this.signBroadcastAndWaitEvmLeg({
752
+ rpc,
753
+ from,
754
+ to: built.transaction.to,
755
+ data: built.transaction.data,
756
+ valueDecimal: "0",
757
+ nonce: baseNonce,
758
+ chainId,
759
+ feeData,
760
+ linkedWalletId: body.linkedWalletId,
761
+ privyWalletId
762
+ });
763
+ let feeTxHash;
764
+ if (hoodBuilt.feeTransfer) {
765
+ feeTxHash = await this.signBroadcastAndWaitEvmLeg({
766
+ rpc,
767
+ from,
768
+ to: hoodBuilt.feeTransfer.to,
769
+ data: hoodBuilt.feeTransfer.data,
770
+ valueDecimal: hoodBuilt.feeTransfer.value,
771
+ nonce: baseNonce + 1,
772
+ chainId,
773
+ feeData,
774
+ linkedWalletId: body.linkedWalletId,
775
+ privyWalletId
776
+ });
777
+ }
778
+ return this.confirmSelfLaunch({
779
+ clientLaunchId: built.clientLaunchId,
780
+ signature: createCurveTxHash,
781
+ ...feeTxHash ? { feeTxHash } : {}
782
+ });
783
+ }
784
+ async buildAtomicLaunch(req) {
785
+ this.requireKey("buildAtomicLaunch()");
786
+ const body = { ...req, clientLaunchId: req.clientLaunchId ?? generateClientLaunchId() };
787
+ return this.requestJson("POST", "/api/v1/launch/atomic/build", body);
788
+ }
789
+ async submitAtomicLaunch(req) {
790
+ this.requireKey("submitAtomicLaunch()");
791
+ const res = await this.fetchImpl(`${this.apiUrl}/api/v1/launch/atomic/submit`, {
792
+ method: "POST",
793
+ headers: this.headers({ json: true }),
794
+ body: JSON.stringify(req)
795
+ });
796
+ const text = await res.text();
797
+ let parsed;
798
+ try {
799
+ parsed = JSON.parse(text);
800
+ } catch {
801
+ parsed = undefined;
802
+ }
803
+ if (isAtomicSubmitOutcome(parsed))
804
+ return parsed;
805
+ if (!res.ok)
806
+ throw candleApiErrorFromResponse(res.status, text);
807
+ throw new Error(`submitAtomicLaunch(): unexpected 200 response shape: ${text}`);
808
+ }
809
+ async launchAtomic(req) {
810
+ const { payer, firstBuys, ...launchFields } = req;
811
+ const buildReq = {
812
+ ...launchFields,
813
+ clientLaunchId: launchFields.clientLaunchId ?? generateClientLaunchId(),
814
+ payer: toAtomicWirePayer(payer),
815
+ firstBuys: firstBuys.map((leg) => ({ payer: toAtomicWirePayer(leg.payer), amountRaw: leg.amountRaw }))
816
+ };
817
+ const built = await this.buildAtomicLaunch(buildReq);
818
+ const signedTxsBase64 = [];
819
+ for (const leg of built.legs) {
820
+ if (leg.signer !== "client")
821
+ continue;
822
+ if (!leg.unsignedTxBase64) {
823
+ throw new Error(`launchAtomic(): build response's leg ${leg.index} is signer "client" but omitted unsignedTxBase64`);
824
+ }
825
+ const legPayer = leg.index === 0 ? payer : firstBuys[leg.index - 1]?.payer;
826
+ if (!legPayer || legPayer.type !== "linked") {
827
+ throw new Error(`launchAtomic(): build response's leg ${leg.index} is signer "client" but this request's own leg ${leg.index} is not a linked payer`);
828
+ }
829
+ const signed = await this.signLinkedTransaction({
830
+ linkedWalletId: legPayer.linkedWalletId,
831
+ privyWalletId: legPayer.privyWalletId,
832
+ chain: "solana",
833
+ unsignedTransactionBase64: leg.unsignedTxBase64
834
+ });
835
+ signedTxsBase64.push(signed.signedTransaction);
836
+ }
837
+ return this.submitAtomicLaunch({ bundleId: built.bundleId, signedTxsBase64 });
838
+ }
839
+ evmRpc() {
840
+ const url = this.evmRpcUrl;
841
+ if (!url) {
842
+ throw new Error("evmRpc(): evmRpcUrl is unset -- callers must check this first");
843
+ }
844
+ return {
845
+ call: (method, params) => this.jsonRpcCall(url, method, params),
846
+ callRaw: (method, params) => this.jsonRpcCallRaw(url, method, params)
847
+ };
848
+ }
849
+ async signBroadcastAndWaitEvmLeg(params) {
850
+ const gasLimitHex = await estimateGas(params.rpc, {
851
+ from: params.from,
852
+ to: params.to,
853
+ data: params.data,
854
+ value: decimalToHexQuantity(params.valueDecimal)
855
+ });
856
+ const evmTxParams = assembleEvmTx({
857
+ from: params.from,
858
+ to: params.to,
859
+ data: params.data,
860
+ valueDecimal: params.valueDecimal,
861
+ nonce: params.nonce,
862
+ chainId: params.chainId,
863
+ gasLimitHex,
864
+ feeData: params.feeData
865
+ });
866
+ const signed = await this.signLinkedTransaction({
867
+ linkedWalletId: params.linkedWalletId,
868
+ privyWalletId: params.privyWalletId,
869
+ chain: "evm",
870
+ evmTxParams
871
+ });
872
+ const txHash = await this.broadcastSignedTransaction("evm", signed.signedTransaction, signed.encoding);
873
+ await waitForReceipt(params.rpc, txHash);
874
+ return txHash;
875
+ }
876
+ requireKey(method) {
877
+ if (!this.apiKey) {
878
+ throw new Error(`${method} requires an apiKey: pass one in CandleClientOptions (new CandleClient({ apiKey }))`);
879
+ }
880
+ }
881
+ headers(opts = {}) {
882
+ const headers = {};
883
+ if (opts.json)
884
+ headers["content-type"] = "application/json";
885
+ if (opts.contentType)
886
+ headers["content-type"] = opts.contentType;
887
+ if (this.apiKey)
888
+ headers["x-api-key"] = this.apiKey;
889
+ return headers;
890
+ }
891
+ async requestJson(method, path, body) {
892
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
893
+ method,
894
+ headers: this.headers({ json: body !== undefined }),
895
+ ...body !== undefined ? { body: JSON.stringify(body) } : {}
896
+ });
897
+ return this.parseResponse(res);
898
+ }
899
+ async parseResponse(res) {
900
+ const text = await res.text();
901
+ if (!res.ok)
902
+ throw candleApiErrorFromResponse(res.status, text);
903
+ return JSON.parse(text);
904
+ }
905
+ async jsonRpcCall(url, method, params) {
906
+ const res = await this.fetchImpl(url, {
907
+ method: "POST",
908
+ headers: { "content-type": "application/json" },
909
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
910
+ });
911
+ const text = await res.text();
912
+ if (!res.ok) {
913
+ throw new Error(`JSON-RPC ${method} against ${url} failed: HTTP ${res.status}: ${text}`);
914
+ }
915
+ const parsed = JSON.parse(text);
916
+ if (parsed.error) {
917
+ throw new JsonRpcError({
918
+ code: parsed.error.code,
919
+ message: formatJsonRpcErrorMessage(method, url, parsed.error),
920
+ data: parsed.error.data
921
+ });
922
+ }
923
+ if (typeof parsed.result !== "string") {
924
+ throw new Error(`JSON-RPC ${method} against ${url} returned a non-string result: ${JSON.stringify(parsed.result)}`);
925
+ }
926
+ return parsed.result;
927
+ }
928
+ async jsonRpcCallRaw(url, method, params) {
929
+ const res = await this.fetchImpl(url, {
930
+ method: "POST",
931
+ headers: { "content-type": "application/json" },
932
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
933
+ });
934
+ const text = await res.text();
935
+ if (!res.ok) {
936
+ throw new Error(`JSON-RPC ${method} against ${url} failed: HTTP ${res.status}: ${text}`);
937
+ }
938
+ const parsed = JSON.parse(text);
939
+ if (parsed.error) {
940
+ throw new JsonRpcError({
941
+ code: parsed.error.code,
942
+ message: formatJsonRpcErrorMessage(method, url, parsed.error),
943
+ data: parsed.error.data
944
+ });
945
+ }
946
+ return parsed.result;
947
+ }
948
+ }
949
+ function generateSdkId() {
950
+ return `sdk-${crypto.randomUUID()}`;
951
+ }
952
+ function generateClientLaunchId() {
953
+ return generateSdkId();
954
+ }
955
+ function generateClientTradeId() {
956
+ return generateSdkId();
957
+ }
958
+ // src/keychain-secret-store.ts
959
+ import { spawn, spawnSync } from "node:child_process";
960
+ var SERVICE = "tv.candle.cli";
961
+ function walletSignerRef(walletRef) {
962
+ return `wallet_signer_${walletRef}`;
963
+ }
964
+ function pemToStoredSigner(pem) {
965
+ return pem.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\s+/g, "");
966
+ }
967
+ function storedSignerToPem(stored) {
968
+ const lines = stored.match(/.{1,64}/g) ?? [stored];
969
+ return `-----BEGIN PRIVATE KEY-----
970
+ ${lines.join(`
971
+ `)}
972
+ -----END PRIVATE KEY-----
973
+ `;
974
+ }
975
+ function assertStorable(value) {
976
+ if (!/^[A-Za-z0-9+/]+=*$/.test(value)) {
977
+ throw new Error("Refusing to store a signer value that is not single-line base64");
978
+ }
979
+ }
980
+ var RUN_TIMEOUT_MS = 1e4;
981
+ var realExec = (binary, args, stdin) => new Promise((resolvePromise, reject) => {
982
+ const child = spawn(binary, args, { stdio: ["pipe", "pipe", "ignore"], env: process.env });
983
+ let stdout = "";
984
+ let settled = false;
985
+ const timeout = setTimeout(() => {
986
+ if (!settled)
987
+ child.kill("SIGKILL");
988
+ }, RUN_TIMEOUT_MS);
989
+ child.stdin.on("error", () => {});
990
+ child.stdout.on("data", (chunk) => {
991
+ stdout += chunk.toString("utf8");
992
+ });
993
+ child.on("error", (err) => {
994
+ if (settled)
995
+ return;
996
+ settled = true;
997
+ clearTimeout(timeout);
998
+ reject(err);
999
+ });
1000
+ child.on("close", (status) => {
1001
+ if (settled)
1002
+ return;
1003
+ settled = true;
1004
+ clearTimeout(timeout);
1005
+ resolvePromise({ status: status ?? 1, stdout });
1006
+ });
1007
+ if (stdin !== undefined)
1008
+ child.stdin.write(stdin);
1009
+ child.stdin.end();
1010
+ });
1011
+
1012
+ class KeychainSecretStore {
1013
+ backend;
1014
+ exec;
1015
+ constructor(opts = {}) {
1016
+ this.backend = opts.backend ?? (process.platform === "darwin" ? "security" : "secret-tool");
1017
+ this.exec = opts.exec ?? realExec;
1018
+ }
1019
+ static detect() {
1020
+ const backend = process.platform === "darwin" ? "security" : "secret-tool";
1021
+ const found = spawnSync("which", [backend], { env: process.env }).status === 0;
1022
+ return found ? new KeychainSecretStore({ backend }) : null;
1023
+ }
1024
+ async get(walletRef) {
1025
+ const ref = walletSignerRef(walletRef);
1026
+ const result = this.backend === "security" ? await this.exec("security", ["find-generic-password", "-s", SERVICE, "-a", ref, "-w"]) : await this.exec("secret-tool", ["lookup", "service", SERVICE, "account", ref]);
1027
+ if (result.status !== 0)
1028
+ return null;
1029
+ const value = result.stdout.replace(/\n$/, "");
1030
+ if (value.length === 0)
1031
+ return null;
1032
+ return value.includes("BEGIN PRIVATE KEY") ? value : storedSignerToPem(value);
1033
+ }
1034
+ async set(walletRef, privateKeyPem) {
1035
+ const ref = walletSignerRef(walletRef);
1036
+ const value = pemToStoredSigner(privateKeyPem);
1037
+ assertStorable(value);
1038
+ if (this.backend === "security") {
1039
+ const command = `add-generic-password -U -s "${SERVICE}" -a "${ref}" -w "${value}"
1040
+ `;
1041
+ const result2 = await this.exec("security", ["-i"], command);
1042
+ if (result2.status !== 0)
1043
+ throw new Error(`Failed to store signer in the macOS Keychain (${result2.status})`);
1044
+ return;
1045
+ }
1046
+ const result = await this.exec("secret-tool", ["store", "--label=Candle CLI", "service", SERVICE, "account", ref], value);
1047
+ if (result.status !== 0)
1048
+ throw new Error(`Failed to store signer via secret-tool (${result.status})`);
1049
+ }
1050
+ async delete(walletRef) {
1051
+ const ref = walletSignerRef(walletRef);
1052
+ if (this.backend === "security") {
1053
+ await this.exec("security", ["-i"], `delete-generic-password -s "${SERVICE}" -a "${ref}"
1054
+ `);
1055
+ return;
1056
+ }
1057
+ await this.exec("secret-tool", ["clear", "service", SERVICE, "account", ref]);
1058
+ }
1059
+ }
1060
+ // src/secret-store.ts
1061
+ class InMemorySecretStore {
1062
+ entries = new Map;
1063
+ async get(walletRef) {
1064
+ return this.entries.get(walletRef) ?? null;
1065
+ }
1066
+ async set(walletRef, privateKeyPem) {
1067
+ this.entries.set(walletRef, privateKeyPem);
1068
+ }
1069
+ async delete(walletRef) {
1070
+ this.entries.delete(walletRef);
1071
+ }
1072
+ }
1073
+ var PBKDF2_ITERATIONS = 210000;
1074
+ var SALT_LENGTH_BYTES = 16;
1075
+ var IV_LENGTH_BYTES = 12;
1076
+ async function deriveKey(passphrase, salt, iterations) {
1077
+ const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(passphrase), "PBKDF2", false, [
1078
+ "deriveKey"
1079
+ ]);
1080
+ return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
1081
+ }
1082
+
1083
+ class EncryptedFileSecretStore {
1084
+ path;
1085
+ passphrase;
1086
+ constructor(path, passphrase) {
1087
+ this.path = path;
1088
+ this.passphrase = passphrase;
1089
+ }
1090
+ async get(walletRef) {
1091
+ const contents = await this.readFile();
1092
+ const entry = contents[walletRef];
1093
+ if (!entry)
1094
+ return null;
1095
+ const salt = fromBase64(entry.salt);
1096
+ const key = await deriveKey(this.passphrase, salt, entry.iterations);
1097
+ const iv = fromBase64(entry.iv);
1098
+ const ciphertext = fromBase64(entry.ciphertext);
1099
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
1100
+ return new TextDecoder().decode(plaintext);
1101
+ }
1102
+ async set(walletRef, privateKeyPem) {
1103
+ const contents = await this.readFile();
1104
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH_BYTES));
1105
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
1106
+ const key = await deriveKey(this.passphrase, salt, PBKDF2_ITERATIONS);
1107
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(privateKeyPem));
1108
+ contents[walletRef] = {
1109
+ salt: toBase64(salt),
1110
+ iv: toBase64(iv),
1111
+ ciphertext: toBase64(new Uint8Array(ciphertext)),
1112
+ iterations: PBKDF2_ITERATIONS
1113
+ };
1114
+ await this.writeFile(contents);
1115
+ }
1116
+ async delete(walletRef) {
1117
+ const contents = await this.readFile();
1118
+ if (!(walletRef in contents))
1119
+ return;
1120
+ delete contents[walletRef];
1121
+ await this.writeFile(contents);
1122
+ }
1123
+ async readFile() {
1124
+ const { readFile } = await import("node:fs/promises");
1125
+ try {
1126
+ const raw = await readFile(this.path, "utf8");
1127
+ return JSON.parse(raw);
1128
+ } catch (err) {
1129
+ if (err.code === "ENOENT")
1130
+ return {};
1131
+ throw err;
1132
+ }
1133
+ }
1134
+ async writeFile(contents) {
1135
+ const { mkdir, writeFile } = await import("node:fs/promises");
1136
+ const { dirname } = await import("node:path");
1137
+ await mkdir(dirname(this.path), { recursive: true });
1138
+ await writeFile(this.path, JSON.stringify(contents, null, 2), "utf8");
1139
+ }
1140
+ }
1141
+ // src/webhooks.ts
1142
+ import { createHmac, timingSafeEqual } from "node:crypto";
1143
+ function verifyWebhookSignature(secret, header, body, nowSec, toleranceSec = 300) {
1144
+ if (!secret || !header)
1145
+ return false;
1146
+ let t;
1147
+ let v1;
1148
+ for (const part of header.split(",")) {
1149
+ const eq = part.indexOf("=");
1150
+ if (eq === -1)
1151
+ return false;
1152
+ const key = part.slice(0, eq).trim();
1153
+ const value = part.slice(eq + 1).trim();
1154
+ if (key === "t")
1155
+ t = value;
1156
+ else if (key === "v1")
1157
+ v1 = value;
1158
+ }
1159
+ if (!t || !v1)
1160
+ return false;
1161
+ if (!/^\d+$/.test(t))
1162
+ return false;
1163
+ if (!/^[0-9a-fA-F]+$/.test(v1))
1164
+ return false;
1165
+ const timestamp = Number(t);
1166
+ if (!Number.isSafeInteger(timestamp))
1167
+ return false;
1168
+ if (Math.abs(nowSec - timestamp) > toleranceSec)
1169
+ return false;
1170
+ const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
1171
+ const provided = Buffer.from(v1.toLowerCase(), "hex");
1172
+ if (provided.length !== expected.length)
1173
+ return false;
1174
+ return timingSafeEqual(provided, expected);
1175
+ }
1176
+ export {
1177
+ verifyWebhookSignature,
1178
+ generateSignerKeypair,
1179
+ encryptWalletKeyForImport,
1180
+ KeychainSecretStore,
1181
+ JsonRpcError,
1182
+ InMemorySecretStore,
1183
+ EncryptedFileSecretStore,
1184
+ CandleClient,
1185
+ CandleApiError
1186
+ };