@candledottv/agent-sdk 0.4.4 → 0.4.6

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 {
@@ -474,6 +874,8 @@ class CandleClient {
474
874
  secretStore;
475
875
  solanaRpcUrl;
476
876
  evmRpcUrl;
877
+ hyperliquidNetwork;
878
+ hyperliquidBuilder;
477
879
  wallets = {
478
880
  swapReceipt: async (hash) => {
479
881
  this.requireKey("wallets.swapReceipt()");
@@ -509,6 +911,10 @@ class CandleClient {
509
911
  this.solanaRpcUrl = opts.solanaRpcUrl;
510
912
  if (opts.evmRpcUrl !== undefined)
511
913
  this.evmRpcUrl = opts.evmRpcUrl;
914
+ this.hyperliquidNetwork = opts.hyperliquidNetwork ?? "mainnet";
915
+ const builder = opts.hyperliquidBuilder ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
916
+ if (builder)
917
+ this.hyperliquidBuilder = builder.toLowerCase();
512
918
  }
513
919
  async getQuotePairs(chain) {
514
920
  const query = chain ? `?chain=${chain}` : "";
@@ -635,6 +1041,10 @@ class CandleClient {
635
1041
  this.requireKey("getProfilePnl()");
636
1042
  return this.requestJson("GET", `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl`);
637
1043
  }
1044
+ async getPortfolio() {
1045
+ this.requireKey("getPortfolio()");
1046
+ return this.requestJson("GET", "/api/v1/agent/portfolio");
1047
+ }
638
1048
  async setProfileWallets(keyPrefix, walletIds) {
639
1049
  this.requireKey("setProfileWallets()");
640
1050
  return this.requestJson("PUT", `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, { walletIds });
@@ -1026,6 +1436,171 @@ class CandleClient {
1026
1436
  await waitForReceipt(params.rpc, txHash);
1027
1437
  return txHash;
1028
1438
  }
1439
+ async perpsConfig() {
1440
+ this.requireKey("perpsConfig()");
1441
+ return this.requestJson("GET", "/api/v1/agent/perps/config");
1442
+ }
1443
+ async perpsSetup(params) {
1444
+ this.requireKey("perpsSetup()");
1445
+ const res = await this.requestJson("POST", "/api/v1/agent/perps/setup", { walletId: params.walletId });
1446
+ const status = {
1447
+ walletId: res.walletId,
1448
+ address: res.address,
1449
+ network: res.network,
1450
+ mode: res.mode,
1451
+ standardMode: res.standardMode,
1452
+ accountValue: res.accountValue,
1453
+ withdrawable: res.withdrawable,
1454
+ builder: res.builder,
1455
+ approvedFeeTenthsBps: res.approvedFeeTenthsBps,
1456
+ ready: res.ready
1457
+ };
1458
+ if (res.ready)
1459
+ return status;
1460
+ return { ...status, action: await this.perpsComplete(res, params, "setup") };
1461
+ }
1462
+ async perpsOpen(params) {
1463
+ this.requireKey("perpsOpen()");
1464
+ const { walletId, privyWalletId: _p, submit: _s, ...order } = params;
1465
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/open", { walletId, ...order });
1466
+ return this.perpsComplete(build, params, "open");
1467
+ }
1468
+ async perpsClose(params) {
1469
+ this.requireKey("perpsClose()");
1470
+ const { walletId, privyWalletId: _p, submit: _s, ...close } = params;
1471
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/close", { walletId, ...close });
1472
+ return this.perpsComplete(build, params, "close");
1473
+ }
1474
+ async perpsCancel(params) {
1475
+ this.requireKey("perpsCancel()");
1476
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/cancel", {
1477
+ walletId: params.walletId,
1478
+ cloid: params.cloid
1479
+ });
1480
+ return this.perpsComplete(build, params, "cancel");
1481
+ }
1482
+ async perpsModify(params) {
1483
+ this.requireKey("perpsModify()");
1484
+ const { walletId, privyWalletId: _p, submit: _s, ...modify } = params;
1485
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/modify", { walletId, ...modify });
1486
+ return this.perpsComplete(build, params, "modify");
1487
+ }
1488
+ async perpsLeverage(params) {
1489
+ this.requireKey("perpsLeverage()");
1490
+ const { walletId, privyWalletId: _p, submit: _s, ...leverage } = params;
1491
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/leverage", { walletId, ...leverage });
1492
+ return this.perpsComplete(build, params, "leverage");
1493
+ }
1494
+ async perpsMargin(params) {
1495
+ this.requireKey("perpsMargin()");
1496
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/margin", {
1497
+ walletId: params.walletId,
1498
+ coin: params.coin,
1499
+ amount: params.amount
1500
+ });
1501
+ return this.perpsComplete(build, params, "margin");
1502
+ }
1503
+ async perpsPositions(walletId) {
1504
+ this.requireKey("perpsPositions()");
1505
+ return this.requestJson("GET", `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(walletId)}`);
1506
+ }
1507
+ async perpsOrders(walletId, opts = {}) {
1508
+ this.requireKey("perpsOrders()");
1509
+ const params = new URLSearchParams({ walletId });
1510
+ if (opts.limit !== undefined)
1511
+ params.set("limit", String(opts.limit));
1512
+ return this.requestJson("GET", `/api/v1/agent/perps/orders?${params}`);
1513
+ }
1514
+ async perpsFills(walletId) {
1515
+ this.requireKey("perpsFills()");
1516
+ return this.requestJson("GET", `/api/v1/agent/perps/fills?walletId=${encodeURIComponent(walletId)}`);
1517
+ }
1518
+ async perpsFunding(walletId, startTime) {
1519
+ this.requireKey("perpsFunding()");
1520
+ const params = new URLSearchParams({ walletId });
1521
+ if (startTime !== undefined)
1522
+ params.set("startTime", String(startTime));
1523
+ return this.requestJson("GET", `/api/v1/agent/perps/funding?${params}`);
1524
+ }
1525
+ async perpsBuilder() {
1526
+ if (this.hyperliquidBuilder)
1527
+ return this.hyperliquidBuilder;
1528
+ const config = await this.perpsConfig();
1529
+ if (!config.builder)
1530
+ throw new Error("perps: the server reports no Hyperliquid builder address");
1531
+ this.hyperliquidBuilder = config.builder.toLowerCase();
1532
+ return this.hyperliquidBuilder;
1533
+ }
1534
+ async perpsComplete(build, ref, method) {
1535
+ const check = verifyPerpsBuild(build, {
1536
+ builder: await this.perpsBuilder(),
1537
+ network: this.hyperliquidNetwork,
1538
+ intent: { method, params: ref }
1539
+ });
1540
+ if (!check.ok)
1541
+ throw new Error(`perps: refused to sign this build: ${check.reason}`);
1542
+ if (method === "close") {
1543
+ const closeOrder = await hyperliquidCloseOrder(this.fetchImpl, this.hyperliquidNetwork, build.address, ref);
1544
+ const closeCheck = verifyPerpsBuild(build, {
1545
+ builder: await this.perpsBuilder(),
1546
+ network: this.hyperliquidNetwork,
1547
+ intent: { method, params: ref },
1548
+ closeOrder
1549
+ });
1550
+ if (!closeCheck.ok)
1551
+ throw new Error(`perps: refused to sign this build: ${closeCheck.reason}`);
1552
+ }
1553
+ if (ref.submit === false)
1554
+ return { build, signature: null, submitted: false, exchange: null };
1555
+ const signature = await this.signLinkedTypedData({
1556
+ linkedWalletId: ref.walletId,
1557
+ privyWalletId: ref.privyWalletId,
1558
+ typedData: build.typedData
1559
+ });
1560
+ try {
1561
+ const res = await this.fetchImpl(HYPERLIQUID_EXCHANGE_URLS[this.hyperliquidNetwork], {
1562
+ method: "POST",
1563
+ headers: { "content-type": "application/json" },
1564
+ body: JSON.stringify(hyperliquidExchangeBody(build.action, build.nonce, signature))
1565
+ });
1566
+ const text = await res.text();
1567
+ if (!res.ok) {
1568
+ return { build, signature, submitted: false, exchange: null, submitError: `HTTP ${res.status}: ${text}` };
1569
+ }
1570
+ return { build, signature, submitted: true, exchange: JSON.parse(text) };
1571
+ } catch (err) {
1572
+ return {
1573
+ build,
1574
+ signature,
1575
+ submitted: false,
1576
+ exchange: null,
1577
+ submitError: err instanceof Error ? err.message : String(err)
1578
+ };
1579
+ }
1580
+ }
1581
+ async signLinkedTypedData(params) {
1582
+ if (!this.privyAppId) {
1583
+ throw new Error("perps signing requires privyAppId in CandleClientOptions (the relay's Privy app id)");
1584
+ }
1585
+ if (!this.secretStore)
1586
+ throw new Error("perps signing requires a secretStore in CandleClientOptions");
1587
+ const privateKeyPem = await this.secretStore.get(params.linkedWalletId);
1588
+ if (!privateKeyPem) {
1589
+ throw new Error(`perps: no signer key stored for linked wallet "${params.linkedWalletId}"`);
1590
+ }
1591
+ const body = hyperliquidRelayBody(params.typedData);
1592
+ const authorizationSignature = await buildPrivyAuthorizationSignature({
1593
+ privateKeyPem,
1594
+ privyWalletId: params.privyWalletId,
1595
+ appId: this.privyAppId,
1596
+ body
1597
+ });
1598
+ const res = await this.requestJson("POST", `/api/v1/agent/wallets/${encodeURIComponent(params.linkedWalletId)}/sign`, { authorizationSignature, body });
1599
+ if (typeof res.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(res.signature)) {
1600
+ throw new Error("perps: the relay returned no signature");
1601
+ }
1602
+ return res.signature;
1603
+ }
1029
1604
  requireKey(method) {
1030
1605
  if (!this.apiKey) {
1031
1606
  throw new Error(`${method} requires an apiKey: pass one in CandleClientOptions (new CandleClient({ apiKey }))`);
@@ -1109,7 +1684,7 @@ function generateClientLaunchId() {
1109
1684
  function generateClientTradeId() {
1110
1685
  return generateSdkId();
1111
1686
  }
1112
- var SDK_VERSION = "0.4.4";
1687
+ var SDK_VERSION = "0.4.6";
1113
1688
  var sdkUpdateWarned = false;
1114
1689
  function noteLatestSdkVersion(value) {
1115
1690
  if (sdkUpdateWarned || !value || !/^\d+\.\d+\.\d+$/.test(value))
@@ -1394,14 +1969,19 @@ function verifyWebhookSignature(secret, header, body, nowSec, toleranceSec = 300
1394
1969
  }
1395
1970
  export {
1396
1971
  verifyWebhookSignature,
1972
+ verifyPerpsBuild,
1397
1973
  isSolanaRpcErrorData,
1974
+ hyperliquidActionHash,
1398
1975
  generateSignerKeypair,
1399
1976
  encryptWalletKeyForImport,
1400
1977
  KeychainSecretStore,
1401
1978
  JsonRpcError,
1402
1979
  InMemorySecretStore,
1980
+ HYPERLIQUID_EXCHANGE_URLS,
1981
+ HYPERLIQUID_ALLOWED_ACTION_TYPES,
1403
1982
  EncryptedFileSecretStore,
1404
1983
  CandleClient,
1405
1984
  CandleApiError,
1985
+ CANDLE_HYPERLIQUID_BUILDER_ADDRESS,
1406
1986
  BRIDGE_ERROR_CODES
1407
1987
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/agent-sdk",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "TypeScript SDK for the Candle agent rail: typed headless launches, market reads, and webhook verification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,6 +49,7 @@
49
49
  "dependencies": {
50
50
  "@hpke/chacha20poly1305": "^1.6.2",
51
51
  "@hpke/core": "^1.7.2",
52
+ "@noble/hashes": "1.8.0",
52
53
  "@scure/base": "^1.2.6",
53
54
  "canonicalize": "^2.1.0"
54
55
  },