@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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/authorization-signature.d.ts +87 -0
- package/dist/authorization-signature.d.ts.map +1 -0
- package/dist/client.d.ts +1192 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/evm-tx.d.ts +100 -0
- package/dist/evm-tx.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1186 -0
- package/dist/internal/encoding.d.ts +19 -0
- package/dist/internal/encoding.d.ts.map +1 -0
- package/dist/keychain-secret-store.d.ts +51 -0
- package/dist/keychain-secret-store.d.ts.map +1 -0
- package/dist/secret-store.d.ts +82 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/wallet-import.d.ts +93 -0
- package/dist/wallet-import.d.ts.map +1 -0
- package/dist/webhooks.d.ts +20 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/package.json +60 -0
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
|
+
};
|