@candledottv/agent-sdk 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -260,6 +260,406 @@ async function waitForReceipt(rpc, txHash, opts = {}) {
260
260
  }
261
261
  }
262
262
 
263
+ // src/hyperliquid.ts
264
+ import { keccak_256 } from "@noble/hashes/sha3";
265
+ var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
266
+ "order",
267
+ "cancel",
268
+ "modify",
269
+ "updateLeverage",
270
+ "updateIsolatedMargin"
271
+ ];
272
+ var HYPERLIQUID_EXCHANGE_URLS = {
273
+ mainnet: "https://api.hyperliquid.xyz/exchange",
274
+ testnet: "https://api.hyperliquid-testnet.xyz/exchange"
275
+ };
276
+ var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
277
+ var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
278
+ var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
279
+ var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
280
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
281
+ var UINT64_MAX = (1n << 64n) - 1n;
282
+ var INT64_MIN = -(1n << 63n);
283
+ function pushUint(out, value, width) {
284
+ for (let i = width - 1;i >= 0; i--)
285
+ out.push(Number(value >> BigInt(i * 8) & 0xffn));
286
+ }
287
+ function packInteger(out, value) {
288
+ if (value >= 0n) {
289
+ if (value > UINT64_MAX)
290
+ throw new Error("msgpack: integer above uint64");
291
+ if (value < 0x80n)
292
+ out.push(Number(value));
293
+ else if (value <= 0xffn) {
294
+ out.push(204);
295
+ pushUint(out, value, 1);
296
+ } else if (value <= 0xffffn) {
297
+ out.push(205);
298
+ pushUint(out, value, 2);
299
+ } else if (value <= 0xffffffffn) {
300
+ out.push(206);
301
+ pushUint(out, value, 4);
302
+ } else {
303
+ out.push(207);
304
+ pushUint(out, value, 8);
305
+ }
306
+ return;
307
+ }
308
+ if (value < INT64_MIN)
309
+ throw new Error("msgpack: integer below int64");
310
+ if (value >= -32n)
311
+ out.push(Number(value & 0xffn));
312
+ else if (value >= -128n) {
313
+ out.push(208);
314
+ pushUint(out, value & 0xffn, 1);
315
+ } else if (value >= -32768n) {
316
+ out.push(209);
317
+ pushUint(out, value & 0xffffn, 2);
318
+ } else if (value >= -2147483648n) {
319
+ out.push(210);
320
+ pushUint(out, value & 0xffffffffn, 4);
321
+ } else {
322
+ out.push(211);
323
+ pushUint(out, value & UINT64_MAX, 8);
324
+ }
325
+ }
326
+ function packHeader(out, length, fixBase, fixMax, codes) {
327
+ if (length <= fixMax) {
328
+ out.push(fixBase | length);
329
+ return;
330
+ }
331
+ const [c8, c16, c32] = codes;
332
+ if (c8 !== undefined && c8 !== 0 && length <= 255) {
333
+ out.push(c8);
334
+ pushUint(out, BigInt(length), 1);
335
+ } else if (length <= 65535) {
336
+ out.push(c16);
337
+ pushUint(out, BigInt(length), 2);
338
+ } else {
339
+ out.push(c32);
340
+ pushUint(out, BigInt(length), 4);
341
+ }
342
+ }
343
+ function packValue(out, value) {
344
+ if (value === null)
345
+ out.push(192);
346
+ else if (value === false)
347
+ out.push(194);
348
+ else if (value === true)
349
+ out.push(195);
350
+ else if (typeof value === "bigint")
351
+ packInteger(out, value);
352
+ else if (typeof value === "number") {
353
+ if (!Number.isSafeInteger(value))
354
+ throw new Error(`msgpack: ${value} is not a safe integer`);
355
+ packInteger(out, BigInt(value));
356
+ } else if (typeof value === "string") {
357
+ const bytes = new TextEncoder().encode(value);
358
+ packHeader(out, bytes.length, 160, 31, [217, 218, 219]);
359
+ for (const b of bytes)
360
+ out.push(b);
361
+ } else if (Array.isArray(value)) {
362
+ packHeader(out, value.length, 144, 15, [0, 220, 221]);
363
+ for (const item of value)
364
+ packValue(out, item);
365
+ } else if (typeof value === "object") {
366
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
367
+ for (const [key] of entries) {
368
+ if (/^(0|[1-9][0-9]*)$/.test(key))
369
+ throw new Error(`msgpack: integer-like key "${key}"`);
370
+ }
371
+ packHeader(out, entries.length, 128, 15, [0, 222, 223]);
372
+ for (const [key, item] of entries) {
373
+ packValue(out, key);
374
+ packValue(out, item);
375
+ }
376
+ } else {
377
+ throw new Error(`msgpack: cannot encode a ${typeof value}`);
378
+ }
379
+ }
380
+ function msgpackEncode(value) {
381
+ const out = [];
382
+ packValue(out, value);
383
+ return Uint8Array.from(out);
384
+ }
385
+ function toHex(bytes) {
386
+ let out = "";
387
+ for (const b of bytes)
388
+ out += b.toString(16).padStart(2, "0");
389
+ return out;
390
+ }
391
+ function hyperliquidActionHash(action, nonce) {
392
+ const n = BigInt(nonce);
393
+ if (n < 0n || n > UINT64_MAX)
394
+ throw new Error("hyperliquidActionHash: nonce out of range");
395
+ const out = Array.from(msgpackEncode(action));
396
+ pushUint(out, n, 8);
397
+ out.push(0);
398
+ return `0x${toHex(keccak_256(Uint8Array.from(out)))}`;
399
+ }
400
+ function hyperliquidL1TypedData(connectionId, network) {
401
+ return {
402
+ domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS },
403
+ types: {
404
+ Agent: [
405
+ { name: "source", type: "string" },
406
+ { name: "connectionId", type: "bytes32" }
407
+ ]
408
+ },
409
+ primary_type: "Agent",
410
+ message: { source: network === "mainnet" ? "a" : "b", connectionId }
411
+ };
412
+ }
413
+ function hyperliquidApproveBuilderFeeTypedData(builder, nonce) {
414
+ return {
415
+ domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS },
416
+ types: {
417
+ "HyperliquidTransaction:ApproveBuilderFee": [
418
+ { name: "hyperliquidChain", type: "string" },
419
+ { name: "maxFeeRate", type: "string" },
420
+ { name: "builder", type: "address" },
421
+ { name: "nonce", type: "uint64" }
422
+ ]
423
+ },
424
+ primary_type: "HyperliquidTransaction:ApproveBuilderFee",
425
+ message: {
426
+ hyperliquidChain: "Mainnet",
427
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
428
+ builder: builder.toLowerCase(),
429
+ nonce
430
+ }
431
+ };
432
+ }
433
+ function canonical(value) {
434
+ if (value === null || typeof value !== "object")
435
+ return JSON.stringify(value);
436
+ if (Array.isArray(value))
437
+ return `[${value.map(canonical).join(",")}]`;
438
+ const record = value;
439
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
440
+ }
441
+ function sameValue(a, b) {
442
+ return canonical(a) === canonical(b);
443
+ }
444
+ var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
445
+ function checkBuilderField(action, builder) {
446
+ if (!("builder" in action) || action.builder === undefined)
447
+ return null;
448
+ const field = action.builder;
449
+ if (typeof field !== "object" || field === null)
450
+ return "builder field is not an object";
451
+ if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
452
+ return "the order names a builder that is not Candle's";
453
+ }
454
+ if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS) {
455
+ return "the builder fee is outside Candle's 0.1% maximum";
456
+ }
457
+ return null;
458
+ }
459
+ function decimal(value) {
460
+ if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
461
+ return null;
462
+ const negative = value.startsWith("-");
463
+ const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
464
+ const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
465
+ return negative ? -n : n;
466
+ }
467
+ function checkActionIntent(action, intent) {
468
+ const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
469
+ const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
470
+ if (!Array.isArray(orderList))
471
+ return "invalid orders";
472
+ for (const raw of orderList) {
473
+ if (!raw || typeof raw !== "object")
474
+ return "invalid order";
475
+ const o = raw;
476
+ if (!mainAsset(o.a))
477
+ return "order asset is outside the main perp universe";
478
+ if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
479
+ return "invalid order side, size, price or reduce-only flag";
480
+ }
481
+ if (action.type === "cancel") {
482
+ if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
483
+ return "invalid cancel asset";
484
+ }
485
+ if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
486
+ return "invalid action asset";
487
+ if (!intent)
488
+ return null;
489
+ const { method, params: p } = intent;
490
+ const types = {
491
+ setup: "approveBuilderFee",
492
+ open: "order",
493
+ close: "order",
494
+ cancel: "cancel",
495
+ modify: "modify",
496
+ leverage: "updateLeverage",
497
+ margin: "updateIsolatedMargin"
498
+ };
499
+ if (action.type !== types[method])
500
+ return "action type does not match the requested method";
501
+ if (method === "leverage") {
502
+ if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
503
+ return "leverage or mode does not match the request";
504
+ }
505
+ if (method === "margin") {
506
+ const amount = decimal(p.amount);
507
+ if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
508
+ return "isolated margin amount does not match the request";
509
+ }
510
+ if (method === "open" || method === "close" || method === "modify") {
511
+ const list = orderList;
512
+ const first = list[0];
513
+ if (!first)
514
+ return "missing requested order";
515
+ const typ = first.t;
516
+ if (!typ?.limit)
517
+ return "requested order is not a limit/IOC order";
518
+ if (method === "open") {
519
+ const buy = p.side === "long" || p.side === "buy";
520
+ if (first.b !== buy || first.r !== false || decimal(first.s) !== decimal(p.size))
521
+ return "order side, size or reduce-only flag does not match the request";
522
+ const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
523
+ if (typ.limit.tif !== tif)
524
+ return "order time in force does not match the request";
525
+ const triggers = [
526
+ ["takeProfit", "tp"],
527
+ ["stopLoss", "sl"]
528
+ ].filter(([field]) => p[field] != null);
529
+ if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
530
+ return "unexpected extra orders or grouping";
531
+ for (let i = 0;i < triggers.length; i++) {
532
+ const [field, kind] = triggers[i];
533
+ const leg = list[i + 1];
534
+ if (!leg)
535
+ return "missing trigger order";
536
+ const trigger = leg.t?.trigger;
537
+ if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal(leg.s) !== decimal(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal(trigger.triggerPx) !== decimal(p[field]))
538
+ return "trigger side, size, reduce-only flag or price does not match the request";
539
+ }
540
+ } else {
541
+ if (list.length !== 1)
542
+ return "unexpected extra orders";
543
+ if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
544
+ return "close size or reduce-only flag does not match the request";
545
+ if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
546
+ return "replacement size does not match the request";
547
+ }
548
+ if (p.price != null && decimal(first.p) !== decimal(p.price))
549
+ return "order price does not match the request";
550
+ }
551
+ return null;
552
+ }
553
+ async function hyperliquidCloseOrder(fetcher, network, address, params) {
554
+ const read = async (body) => {
555
+ const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
556
+ method: "POST",
557
+ headers: { "content-type": "application/json" },
558
+ body: JSON.stringify(body)
559
+ });
560
+ if (!res.ok)
561
+ throw new Error("Cannot read the close position from Hyperliquid");
562
+ return JSON.parse(await res.text());
563
+ };
564
+ const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
565
+ const coin = String(params.coin).toUpperCase();
566
+ const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
567
+ const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
568
+ const signed = decimal(szi);
569
+ if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
570
+ throw new Error("No main-universe position to close");
571
+ const held = signed < 0n ? -signed : signed;
572
+ const asked = params.size == null ? held : decimal(params.size);
573
+ if (asked === null || asked <= 0n)
574
+ throw new Error("Invalid close size");
575
+ const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
576
+ return { asset, isBuy: signed < 0n, size };
577
+ }
578
+ function verifyPerpsBuild(build, opts) {
579
+ try {
580
+ const network = opts.network ?? "mainnet";
581
+ if (!EVM_ADDRESS_RE.test(opts.builder))
582
+ return { ok: false, reason: "no valid Candle builder address to check against" };
583
+ if (build.network !== undefined && build.network !== network) {
584
+ return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
585
+ }
586
+ if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
587
+ return { ok: false, reason: "the build is for a different wallet" };
588
+ }
589
+ if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
590
+ return { ok: false, reason: "invalid nonce" };
591
+ const typed = build.typedData;
592
+ if (!typed || typeof typed !== "object")
593
+ return { ok: false, reason: "no typed data" };
594
+ const action = build.action;
595
+ if (!action || typeof action !== "object")
596
+ return { ok: false, reason: "no action" };
597
+ const intentRefusal = checkActionIntent(action, opts.intent);
598
+ if (intentRefusal)
599
+ return { ok: false, reason: intentRefusal };
600
+ if (opts.closeOrder) {
601
+ const order = action.orders[0];
602
+ if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
603
+ return { ok: false, reason: "close side, asset or size does not match the requested position" };
604
+ }
605
+ if (typed.primary_type === "Agent") {
606
+ const type = action.type;
607
+ if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
608
+ return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
609
+ }
610
+ if (type === "order") {
611
+ const refusal = checkBuilderField(action, opts.builder);
612
+ if (refusal)
613
+ return { ok: false, reason: refusal };
614
+ } else if ("builder" in action) {
615
+ return { ok: false, reason: `a ${type} action carries no builder` };
616
+ }
617
+ if ("vaultAddress" in action)
618
+ return { ok: false, reason: "the action names a vault" };
619
+ const expected = hyperliquidL1TypedData(hyperliquidActionHash(action, build.nonce), network);
620
+ if (!sameValue(typed, expected)) {
621
+ return { ok: false, reason: "the typed data does not commit to this action and nonce" };
622
+ }
623
+ return { ok: true, kind: "l1" };
624
+ }
625
+ if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
626
+ const expected = hyperliquidApproveBuilderFeeTypedData(opts.builder, build.nonce);
627
+ if (!sameValue(typed, expected)) {
628
+ return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
629
+ }
630
+ const expectedAction = {
631
+ type: "approveBuilderFee",
632
+ hyperliquidChain: "Mainnet",
633
+ signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID,
634
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
635
+ builder: opts.builder.toLowerCase(),
636
+ nonce: build.nonce
637
+ };
638
+ if (!sameValue(action, expectedAction)) {
639
+ return { ok: false, reason: "the approval action does not match its typed data" };
640
+ }
641
+ return { ok: true, kind: "approveBuilderFee" };
642
+ }
643
+ return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
644
+ } catch (err) {
645
+ return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
646
+ }
647
+ }
648
+ function hyperliquidSplitSignature(signature) {
649
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
650
+ throw new Error("Not a 65-byte signature");
651
+ let v = Number.parseInt(signature.slice(130, 132), 16);
652
+ if (v < 27)
653
+ v += 27;
654
+ return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
655
+ }
656
+ function hyperliquidExchangeBody(action, nonce, signature) {
657
+ return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
658
+ }
659
+ function hyperliquidRelayBody(typedData) {
660
+ return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
661
+ }
662
+
263
663
  // src/internal/rpc-endpoint.ts
264
664
  function describeRpcEndpoint(url) {
265
665
  try {
@@ -350,6 +750,31 @@ ${lines.join(`
350
750
  }
351
751
 
352
752
  // src/client.ts
753
+ var HYPERLIQUID_RELAY_CHAIN_ID = 1337;
754
+ var HYPERLIQUID_RELAY_USDC = "0x00000000000000000000000000000000";
755
+ function perpsDepositProblem(build, params) {
756
+ const same = (a, b) => typeof a === "string" && a.toLowerCase() === b.toLowerCase();
757
+ if (build.walletId !== params.walletId)
758
+ return "the build names another paying wallet";
759
+ if (build.asset !== params.asset || build.amountRaw !== params.amountRaw)
760
+ return "the build is for another amount";
761
+ if (build.destination?.chainId !== HYPERLIQUID_RELAY_CHAIN_ID)
762
+ return "the build does not land on Hyperliquid";
763
+ if (build.destination.currency !== HYPERLIQUID_RELAY_USDC)
764
+ return "the build does not deliver Hyperliquid USDC";
765
+ if (!same(build.destination.address, params.account))
766
+ return "the build lands on another account";
767
+ if (build.fee?.bps !== 0 || build.fee.feeRaw !== "0")
768
+ return "the build carries a Candle fee";
769
+ const hood = params.asset === "ETH" || params.asset === "USDG";
770
+ if (hood !== (build.chain === "hood"))
771
+ return "the build pays from the wrong chain";
772
+ if (hood && !same(build.walletAddress, params.account))
773
+ return "a Hood deposit lands only on the paying wallet's own account";
774
+ if (!hood && build.transactionsBase64?.length !== 1)
775
+ return "the build is not one Solana deposit";
776
+ return null;
777
+ }
353
778
  var BACKOFF_BASE_MS = 250;
354
779
  var BACKOFF_CAP_MS = 8000;
355
780
  function retryDelayMs(retry) {
@@ -474,6 +899,8 @@ class CandleClient {
474
899
  secretStore;
475
900
  solanaRpcUrl;
476
901
  evmRpcUrl;
902
+ hyperliquidNetwork;
903
+ hyperliquidBuilder;
477
904
  wallets = {
478
905
  swapReceipt: async (hash) => {
479
906
  this.requireKey("wallets.swapReceipt()");
@@ -509,6 +936,10 @@ class CandleClient {
509
936
  this.solanaRpcUrl = opts.solanaRpcUrl;
510
937
  if (opts.evmRpcUrl !== undefined)
511
938
  this.evmRpcUrl = opts.evmRpcUrl;
939
+ this.hyperliquidNetwork = opts.hyperliquidNetwork ?? "mainnet";
940
+ const builder = opts.hyperliquidBuilder ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
941
+ if (builder)
942
+ this.hyperliquidBuilder = builder.toLowerCase();
512
943
  }
513
944
  async getQuotePairs(chain) {
514
945
  const query = chain ? `?chain=${chain}` : "";
@@ -558,6 +989,9 @@ class CandleClient {
558
989
  async getAgentTier() {
559
990
  return this.requestJson("GET", "/api/v1/agent/tier");
560
991
  }
992
+ async getPlans() {
993
+ return this.requestJson("GET", "/api/v1/agent/plans");
994
+ }
561
995
  async dryRunLaunch(req) {
562
996
  this.requireKey("dryRunLaunch()");
563
997
  return this.requestJson("POST", "/api/v1/launch/headless/dry-run", req);
@@ -1030,6 +1464,244 @@ class CandleClient {
1030
1464
  await waitForReceipt(params.rpc, txHash);
1031
1465
  return txHash;
1032
1466
  }
1467
+ async perpsConfig() {
1468
+ this.requireKey("perpsConfig()");
1469
+ return this.requestJson("GET", "/api/v1/agent/perps/config");
1470
+ }
1471
+ async perpsSetup(params) {
1472
+ this.requireKey("perpsSetup()");
1473
+ const res = await this.requestJson("POST", "/api/v1/agent/perps/setup", { walletId: params.walletId });
1474
+ const status = {
1475
+ walletId: res.walletId,
1476
+ address: res.address,
1477
+ network: res.network,
1478
+ mode: res.mode,
1479
+ standardMode: res.standardMode,
1480
+ accountValue: res.accountValue,
1481
+ withdrawable: res.withdrawable,
1482
+ builder: res.builder,
1483
+ approvedFeeTenthsBps: res.approvedFeeTenthsBps,
1484
+ ready: res.ready
1485
+ };
1486
+ if (res.ready)
1487
+ return status;
1488
+ return { ...status, action: await this.perpsComplete(res, params, "setup") };
1489
+ }
1490
+ async perpsOpen(params) {
1491
+ this.requireKey("perpsOpen()");
1492
+ const { walletId, privyWalletId: _p, submit: _s, ...order } = params;
1493
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/open", { walletId, ...order });
1494
+ return this.perpsComplete(build, params, "open");
1495
+ }
1496
+ async perpsClose(params) {
1497
+ this.requireKey("perpsClose()");
1498
+ const { walletId, privyWalletId: _p, submit: _s, ...close } = params;
1499
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/close", { walletId, ...close });
1500
+ return this.perpsComplete(build, params, "close");
1501
+ }
1502
+ async perpsCancel(params) {
1503
+ this.requireKey("perpsCancel()");
1504
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/cancel", {
1505
+ walletId: params.walletId,
1506
+ cloid: params.cloid
1507
+ });
1508
+ return this.perpsComplete(build, params, "cancel");
1509
+ }
1510
+ async perpsModify(params) {
1511
+ this.requireKey("perpsModify()");
1512
+ const { walletId, privyWalletId: _p, submit: _s, ...modify } = params;
1513
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/modify", { walletId, ...modify });
1514
+ return this.perpsComplete(build, params, "modify");
1515
+ }
1516
+ async perpsLeverage(params) {
1517
+ this.requireKey("perpsLeverage()");
1518
+ const { walletId, privyWalletId: _p, submit: _s, ...leverage } = params;
1519
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/leverage", { walletId, ...leverage });
1520
+ return this.perpsComplete(build, params, "leverage");
1521
+ }
1522
+ async perpsMargin(params) {
1523
+ this.requireKey("perpsMargin()");
1524
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/margin", {
1525
+ walletId: params.walletId,
1526
+ coin: params.coin,
1527
+ amount: params.amount
1528
+ });
1529
+ return this.perpsComplete(build, params, "margin");
1530
+ }
1531
+ async perpsPositions(walletId) {
1532
+ this.requireKey("perpsPositions()");
1533
+ return this.requestJson("GET", `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(walletId)}`);
1534
+ }
1535
+ async perpsOrders(walletId, opts = {}) {
1536
+ this.requireKey("perpsOrders()");
1537
+ const params = new URLSearchParams({ walletId });
1538
+ if (opts.limit !== undefined)
1539
+ params.set("limit", String(opts.limit));
1540
+ return this.requestJson("GET", `/api/v1/agent/perps/orders?${params}`);
1541
+ }
1542
+ async perpsFills(walletId) {
1543
+ this.requireKey("perpsFills()");
1544
+ return this.requestJson("GET", `/api/v1/agent/perps/fills?walletId=${encodeURIComponent(walletId)}`);
1545
+ }
1546
+ async perpsFunding(walletId, startTime) {
1547
+ this.requireKey("perpsFunding()");
1548
+ const params = new URLSearchParams({ walletId });
1549
+ if (startTime !== undefined)
1550
+ params.set("startTime", String(startTime));
1551
+ return this.requestJson("GET", `/api/v1/agent/perps/funding?${params}`);
1552
+ }
1553
+ async perpsDeposit(params) {
1554
+ this.requireKey("perpsDeposit()");
1555
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/deposit", {
1556
+ clientDepositId: params.clientDepositId,
1557
+ walletId: params.walletId,
1558
+ asset: params.asset,
1559
+ amountRaw: params.amountRaw,
1560
+ ...params.perpsWalletId !== undefined ? { perpsWalletId: params.perpsWalletId } : {},
1561
+ ...params.maxSlippageBps !== undefined ? { maxSlippageBps: params.maxSlippageBps } : {}
1562
+ });
1563
+ if (build.job !== undefined)
1564
+ throw new Error(`perpsDeposit(): ${params.clientDepositId} already names a deposit; read it with perpsDepositStatus()`);
1565
+ const problem = perpsDepositProblem(build, params);
1566
+ if (problem)
1567
+ throw new Error(`perps: refused to sign this deposit: ${problem}`);
1568
+ if (params.submit === false)
1569
+ return { build, submitted: false, result: null };
1570
+ const submitPath = "/api/v1/agent/perps/deposit/submit";
1571
+ const ids = { clientDepositId: params.clientDepositId, depositId: build.depositId };
1572
+ if (build.chain === "solana") {
1573
+ const signed = await this.signLinkedTransaction({
1574
+ chain: "solana",
1575
+ linkedWalletId: params.walletId,
1576
+ privyWalletId: params.privyWalletId,
1577
+ unsignedTransactionBase64: build.transactionsBase64[0]
1578
+ });
1579
+ const result = await this.requestJson("POST", submitPath, {
1580
+ ...ids,
1581
+ signedTransactionsBase64: [signed.signedTransaction]
1582
+ });
1583
+ return { build, submitted: true, result };
1584
+ }
1585
+ const allowed = params.asset === "USDG" ? ["approval", "bridgeDeposit"] : ["bridgeDeposit"];
1586
+ const planned = build.plannedLegCount ?? 0;
1587
+ const hex = (value) => `0x${BigInt(value).toString(16)}`;
1588
+ let body = build;
1589
+ const signedKinds = new Set;
1590
+ while (body.mode === "sequenced") {
1591
+ const leg = body.nextLeg;
1592
+ const kind = String(body.legKind);
1593
+ if (!leg || body.operationId !== build.operationId || body.plannedLegCount !== planned || planned > allowed.length || !allowed.includes(kind) || signedKinds.has(kind) || leg.chainId !== 4663) {
1594
+ throw new Error(`perps: refused to sign deposit leg ${kind}: it is not the plan this deposit was built with`);
1595
+ }
1596
+ signedKinds.add(kind);
1597
+ const signed = await this.signLinkedTransaction({
1598
+ chain: "evm",
1599
+ linkedWalletId: params.walletId,
1600
+ privyWalletId: params.privyWalletId,
1601
+ evmTxParams: {
1602
+ chain_id: leg.chainId,
1603
+ data: leg.data,
1604
+ from: build.walletAddress,
1605
+ gas_limit: hex(leg.gas),
1606
+ max_fee_per_gas: hex(leg.maxFeePerGas),
1607
+ max_priority_fee_per_gas: hex(leg.maxPriorityFeePerGas),
1608
+ nonce: leg.nonce,
1609
+ to: leg.to,
1610
+ type: 2,
1611
+ value: hex(leg.value)
1612
+ }
1613
+ });
1614
+ body = await this.requestJson("POST", submitPath, {
1615
+ ...ids,
1616
+ operationId: build.operationId,
1617
+ signedTransaction: signed.signedTransaction
1618
+ });
1619
+ }
1620
+ return { build, submitted: true, result: body };
1621
+ }
1622
+ async perpsDepositStatus(clientDepositId) {
1623
+ this.requireKey("perpsDepositStatus()");
1624
+ return this.requestJson("GET", `/api/v1/agent/perps/deposit/${encodeURIComponent(clientDepositId)}`);
1625
+ }
1626
+ async perpsBuilder() {
1627
+ if (this.hyperliquidBuilder)
1628
+ return this.hyperliquidBuilder;
1629
+ const config = await this.perpsConfig();
1630
+ if (!config.builder)
1631
+ throw new Error("perps: the server reports no Hyperliquid builder address");
1632
+ this.hyperliquidBuilder = config.builder.toLowerCase();
1633
+ return this.hyperliquidBuilder;
1634
+ }
1635
+ async perpsComplete(build, ref, method) {
1636
+ const check = verifyPerpsBuild(build, {
1637
+ builder: await this.perpsBuilder(),
1638
+ network: this.hyperliquidNetwork,
1639
+ intent: { method, params: ref }
1640
+ });
1641
+ if (!check.ok)
1642
+ throw new Error(`perps: refused to sign this build: ${check.reason}`);
1643
+ if (method === "close") {
1644
+ const closeOrder = await hyperliquidCloseOrder(this.fetchImpl, this.hyperliquidNetwork, build.address, ref);
1645
+ const closeCheck = verifyPerpsBuild(build, {
1646
+ builder: await this.perpsBuilder(),
1647
+ network: this.hyperliquidNetwork,
1648
+ intent: { method, params: ref },
1649
+ closeOrder
1650
+ });
1651
+ if (!closeCheck.ok)
1652
+ throw new Error(`perps: refused to sign this build: ${closeCheck.reason}`);
1653
+ }
1654
+ if (ref.submit === false)
1655
+ return { build, signature: null, submitted: false, exchange: null };
1656
+ const signature = await this.signLinkedTypedData({
1657
+ linkedWalletId: ref.walletId,
1658
+ privyWalletId: ref.privyWalletId,
1659
+ typedData: build.typedData
1660
+ });
1661
+ try {
1662
+ const res = await this.fetchImpl(HYPERLIQUID_EXCHANGE_URLS[this.hyperliquidNetwork], {
1663
+ method: "POST",
1664
+ headers: { "content-type": "application/json" },
1665
+ body: JSON.stringify(hyperliquidExchangeBody(build.action, build.nonce, signature))
1666
+ });
1667
+ const text = await res.text();
1668
+ if (!res.ok) {
1669
+ return { build, signature, submitted: false, exchange: null, submitError: `HTTP ${res.status}: ${text}` };
1670
+ }
1671
+ return { build, signature, submitted: true, exchange: JSON.parse(text) };
1672
+ } catch (err) {
1673
+ return {
1674
+ build,
1675
+ signature,
1676
+ submitted: false,
1677
+ exchange: null,
1678
+ submitError: err instanceof Error ? err.message : String(err)
1679
+ };
1680
+ }
1681
+ }
1682
+ async signLinkedTypedData(params) {
1683
+ if (!this.privyAppId) {
1684
+ throw new Error("perps signing requires privyAppId in CandleClientOptions (the relay's Privy app id)");
1685
+ }
1686
+ if (!this.secretStore)
1687
+ throw new Error("perps signing requires a secretStore in CandleClientOptions");
1688
+ const privateKeyPem = await this.secretStore.get(params.linkedWalletId);
1689
+ if (!privateKeyPem) {
1690
+ throw new Error(`perps: no signer key stored for linked wallet "${params.linkedWalletId}"`);
1691
+ }
1692
+ const body = hyperliquidRelayBody(params.typedData);
1693
+ const authorizationSignature = await buildPrivyAuthorizationSignature({
1694
+ privateKeyPem,
1695
+ privyWalletId: params.privyWalletId,
1696
+ appId: this.privyAppId,
1697
+ body
1698
+ });
1699
+ const res = await this.requestJson("POST", `/api/v1/agent/wallets/${encodeURIComponent(params.linkedWalletId)}/sign`, { authorizationSignature, body });
1700
+ if (typeof res.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(res.signature)) {
1701
+ throw new Error("perps: the relay returned no signature");
1702
+ }
1703
+ return res.signature;
1704
+ }
1033
1705
  requireKey(method) {
1034
1706
  if (!this.apiKey) {
1035
1707
  throw new Error(`${method} requires an apiKey: pass one in CandleClientOptions (new CandleClient({ apiKey }))`);
@@ -1113,7 +1785,7 @@ function generateClientLaunchId() {
1113
1785
  function generateClientTradeId() {
1114
1786
  return generateSdkId();
1115
1787
  }
1116
- var SDK_VERSION = "0.4.5";
1788
+ var SDK_VERSION = "0.4.7";
1117
1789
  var sdkUpdateWarned = false;
1118
1790
  function noteLatestSdkVersion(value) {
1119
1791
  if (sdkUpdateWarned || !value || !/^\d+\.\d+\.\d+$/.test(value))
@@ -1238,6 +1910,79 @@ class KeychainSecretStore {
1238
1910
  await this.exec("secret-tool", ["clear", "service", SERVICE, "account", ref]);
1239
1911
  }
1240
1912
  }
1913
+ // src/plans.ts
1914
+ var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.";
1915
+ var PLAN_CAPABILITY_LABELS = {
1916
+ tradeCandleTokens: "Trade Candle-launched tokens",
1917
+ tradeBaseAssets: "Trade base assets",
1918
+ freeBaseTransfers: "Base-pair swaps and own-wallet bridges (when enabled), no Candle fee",
1919
+ sellExternalTokens: "Sell tokens not launched on Candle",
1920
+ buyExternalTokens: "Buy tokens not launched on Candle",
1921
+ hyperliquidPerps: "Hyperliquid perps (when enabled)",
1922
+ selfLaunch: "Self-launch from a linked wallet",
1923
+ atomicLaunch: "Atomic launch with first buys",
1924
+ createLinkedWallets: "Create linked wallets",
1925
+ importLinkedWallets: "Import linked wallets",
1926
+ limitOrders: "Limit orders",
1927
+ quant: "Quant on Telegram"
1928
+ };
1929
+ var PLAN_LABELS = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
1930
+ function planLabel(plan) {
1931
+ return Object.hasOwn(PLAN_LABELS, plan) ? PLAN_LABELS[plan] : plan;
1932
+ }
1933
+ function formatPlanBps(bps) {
1934
+ return bps === 0 ? "none" : `${Number((bps / 100).toFixed(4))}%`;
1935
+ }
1936
+ function count(n) {
1937
+ return n.toLocaleString("en-US");
1938
+ }
1939
+ function priceCell(entry) {
1940
+ if (entry.price)
1941
+ return `$${count(entry.price.pricePerMonthUsd)} a month`;
1942
+ return entry.plan === "free" ? "free" : "not sold";
1943
+ }
1944
+ function planTableRows(table) {
1945
+ const plans = table.plans;
1946
+ const row = (label, cell) => [label, ...plans.map(cell)];
1947
+ const rows = [
1948
+ row("Price", priceCell),
1949
+ row("Agent trade fee", (e) => formatPlanBps(e.feeBps)),
1950
+ row("Perps builder fee", (e) => formatPlanBps(e.perpFeeBps)),
1951
+ row("Requests per minute", (e) => count(e.limits.rateLimitPerMin)),
1952
+ row("Launches per day", (e) => count(e.limits.dailyLaunchCap)),
1953
+ row("Uploads per minute", (e) => count(e.limits.uploadsPerMin)),
1954
+ row("Linked wallets", (e) => count(e.limits.linkedWallets))
1955
+ ];
1956
+ const known = Object.keys(PLAN_CAPABILITY_LABELS);
1957
+ const served = new Set;
1958
+ for (const entry of plans)
1959
+ for (const key of Object.keys(entry.capabilities ?? {}))
1960
+ served.add(key);
1961
+ const keys = [
1962
+ ...known.filter((k) => served.has(k)),
1963
+ ...[...served].filter((k) => !Object.hasOwn(PLAN_CAPABILITY_LABELS, k))
1964
+ ];
1965
+ for (const key of keys) {
1966
+ const label = Object.hasOwn(PLAN_CAPABILITY_LABELS, key) ? PLAN_CAPABILITY_LABELS[key] : key;
1967
+ rows.push(row(label, (e) => {
1968
+ const value = e.capabilities?.[key];
1969
+ return value === true ? "yes" : value === false ? "no" : "-";
1970
+ }));
1971
+ }
1972
+ return { headers: ["", ...plans.map((e) => planLabel(e.plan))], rows };
1973
+ }
1974
+ function planPromotionLine(table) {
1975
+ if (!(table.promoMaxDays > 0))
1976
+ return null;
1977
+ const days = table.promoMaxDays === 1 ? "1 day" : `${table.promoMaxDays} days`;
1978
+ return `A first Pro purchase includes Max for its first ${days}, then continues on Pro.`;
1979
+ }
1980
+ function planTableMarkdown(table) {
1981
+ const { headers, rows } = planTableRows(table);
1982
+ const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
1983
+ return [line(headers), line(headers.map(() => "---")), ...rows.map(line)].join(`
1984
+ `);
1985
+ }
1241
1986
  // src/file-lock.ts
1242
1987
  var STALE_MS = 30000;
1243
1988
  var RETRY_MS = 25;
@@ -1398,14 +2143,27 @@ function verifyWebhookSignature(secret, header, body, nowSec, toleranceSec = 300
1398
2143
  }
1399
2144
  export {
1400
2145
  verifyWebhookSignature,
2146
+ verifyPerpsBuild,
2147
+ planTableRows,
2148
+ planTableMarkdown,
2149
+ planPromotionLine,
2150
+ planLabel,
2151
+ perpsDepositProblem,
1401
2152
  isSolanaRpcErrorData,
2153
+ hyperliquidActionHash,
1402
2154
  generateSignerKeypair,
2155
+ formatPlanBps,
1403
2156
  encryptWalletKeyForImport,
2157
+ PLAN_CAPABILITY_NOTE,
2158
+ PLAN_CAPABILITY_LABELS,
1404
2159
  KeychainSecretStore,
1405
2160
  JsonRpcError,
1406
2161
  InMemorySecretStore,
2162
+ HYPERLIQUID_EXCHANGE_URLS,
2163
+ HYPERLIQUID_ALLOWED_ACTION_TYPES,
1407
2164
  EncryptedFileSecretStore,
1408
2165
  CandleClient,
1409
2166
  CandleApiError,
2167
+ CANDLE_HYPERLIQUID_BUILDER_ADDRESS,
1410
2168
  BRIDGE_ERROR_CODES
1411
2169
  };