@candledottv/agent-sdk 0.6.2 → 0.6.4

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
@@ -445,7 +445,8 @@ var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
445
445
  "cancel",
446
446
  "modify",
447
447
  "updateLeverage",
448
- "updateIsolatedMargin"
448
+ "updateIsolatedMargin",
449
+ "scheduleCancel"
449
450
  ];
450
451
  var HYPERLIQUID_EXCHANGE_URLS = {
451
452
  mainnet: "https://api.hyperliquid.xyz/exchange",
@@ -455,7 +456,13 @@ var HYPERLIQUID_INFO_URLS = {
455
456
  mainnet: "https://api.hyperliquid.xyz/info",
456
457
  testnet: "https://api.hyperliquid-testnet.xyz/info"
457
458
  };
458
- var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
459
+ var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = "0xc43ff1f6ad9f93fc94558e07c2071b8458e7470b";
460
+ function resolveHyperliquidBuilder(override, releasePin = CANDLE_HYPERLIQUID_BUILDER_ADDRESS) {
461
+ const builder = override === undefined ? releasePin : override;
462
+ if (typeof builder !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(builder) || /^0x0{40}$/.test(builder))
463
+ throw new Error("PERPS_BUILD_REFUSED: no valid nonzero Hyperliquid builder address in the explicit override or release pin.");
464
+ return builder.toLowerCase();
465
+ }
459
466
  var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
460
467
  var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
461
468
  var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
@@ -624,6 +631,43 @@ function sameValue(a, b) {
624
631
  return canonical(a) === canonical(b);
625
632
  }
626
633
  var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
634
+ var HYPERLIQUID_CLOID_RE = /^0x[0-9a-f]{32}$/;
635
+ var HYPERLIQUID_STANDARD_ACCOUNT_MODES = ["default", "disabled"];
636
+ async function hyperliquidAccountMode(fetcher, network, address) {
637
+ const res = await fetcher(HYPERLIQUID_INFO_URLS[network], {
638
+ method: "POST",
639
+ headers: { "content-type": "application/json" },
640
+ body: JSON.stringify({ type: "userAbstraction", user: address.toLowerCase() })
641
+ });
642
+ if (!res.ok)
643
+ throw new Error("Cannot read the account mode from Hyperliquid");
644
+ let mode;
645
+ try {
646
+ mode = JSON.parse(await res.text());
647
+ } catch {
648
+ throw new Error("Hyperliquid returned an unreadable account mode");
649
+ }
650
+ if (typeof mode !== "string" || mode.length === 0 || mode.length > 64)
651
+ throw new Error("Hyperliquid returned an unreadable account mode");
652
+ return mode;
653
+ }
654
+ function perpsSetupStatusProblem(status, expect) {
655
+ if (!HYPERLIQUID_STANDARD_ACCOUNT_MODES.includes(expect.mode))
656
+ return `the account is in ${expect.mode} mode on Hyperliquid; Candle trades only a standard account`;
657
+ if (status.network !== expect.network)
658
+ return `the setup answer is for ${String(status.network)}`;
659
+ if (typeof status.address !== "string" || status.address.toLowerCase() !== expect.address.toLowerCase())
660
+ return "the setup answer is for a different wallet";
661
+ if (status.mode !== expect.mode)
662
+ return "the setup answer reports a mode Hyperliquid does not";
663
+ if (status.standardMode !== true)
664
+ return "the setup answer does not report standard mode";
665
+ if (typeof status.builder !== "string" || status.builder.toLowerCase() !== expect.builder.toLowerCase())
666
+ return "the setup answer names a builder that is not Candle's";
667
+ if (status.ready === true && (typeof status.approvedFeeTenthsBps !== "number" || status.approvedFeeTenthsBps < HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS))
668
+ return "the setup answer reports ready without Candle's builder approval";
669
+ return null;
670
+ }
627
671
  function checkBuilderField(action, builder) {
628
672
  if (!("builder" in action) || action.builder === undefined)
629
673
  return null;
@@ -646,7 +690,67 @@ function decimal(value) {
646
690
  const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
647
691
  return negative ? -n : n;
648
692
  }
649
- function checkActionIntent(action, intent) {
693
+ var ACTION_KEYS = {
694
+ order: { required: ["type", "orders", "grouping"], optional: ["builder"] },
695
+ cancel: { required: ["type", "cancels"], optional: [] },
696
+ modify: { required: ["type", "oid", "order"], optional: [] },
697
+ updateLeverage: { required: ["type", "asset", "isCross", "leverage"], optional: [] },
698
+ updateIsolatedMargin: { required: ["type", "asset", "isBuy", "ntli"], optional: [] },
699
+ scheduleCancel: { required: ["type"], optional: ["time"] }
700
+ };
701
+ function exactKeys(value, required, optional = []) {
702
+ if (!value || typeof value !== "object" || Array.isArray(value))
703
+ return false;
704
+ const keys = Object.keys(value);
705
+ return required.every((k) => keys.includes(k)) && keys.every((k) => required.includes(k) || optional.includes(k));
706
+ }
707
+ function actionShapeProblem(action) {
708
+ const shape = ACTION_KEYS[String(action.type)];
709
+ if (!shape)
710
+ return null;
711
+ if (!exactKeys(action, shape.required, shape.optional))
712
+ return `the ${String(action.type)} action carries unexpected or missing fields`;
713
+ if (action.type === "order") {
714
+ if (!["na", "normalTpsl", "positionTpsl"].includes(action.grouping))
715
+ return "unexpected order grouping";
716
+ if (action.builder !== undefined && !exactKeys(action.builder, ["b", "f"]))
717
+ return "builder field is not an object";
718
+ }
719
+ const orders = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
720
+ if (!Array.isArray(orders))
721
+ return "invalid orders";
722
+ for (const raw of orders) {
723
+ if (!exactKeys(raw, ["a", "b", "p", "s", "r", "t"], ["c"]))
724
+ return "an order carries unexpected or missing fields";
725
+ const o = raw;
726
+ const t = o.t;
727
+ const limit = exactKeys(t, ["limit"]) && exactKeys(t.limit, ["tif"]);
728
+ const trigger = exactKeys(t, ["trigger"]) && exactKeys(t.trigger, ["isMarket", "triggerPx", "tpsl"]);
729
+ if (!limit && !trigger)
730
+ return "an order type carries unexpected or missing fields";
731
+ if (o.c !== undefined && (typeof o.c !== "string" || !HYPERLIQUID_CLOID_RE.test(o.c)))
732
+ return "an order carries a malformed client order id";
733
+ }
734
+ if (action.type === "cancel") {
735
+ const cancels = action.cancels;
736
+ if (!Array.isArray(cancels) || cancels.some((c) => !exactKeys(c, ["a", "o"]) || !Number.isSafeInteger(c.o) || c.o < 0))
737
+ return "a cancel carries unexpected or missing fields";
738
+ }
739
+ if (action.type === "modify") {
740
+ const oid = action.oid;
741
+ if (!(Number.isSafeInteger(oid) && oid >= 0) && !(typeof oid === "string" && HYPERLIQUID_CLOID_RE.test(oid)))
742
+ return "the modify names no order";
743
+ }
744
+ if (action.type === "scheduleCancel" && "time" in action && (!Number.isSafeInteger(action.time) || action.time <= 0))
745
+ return "the scheduled cancel carries an invalid time";
746
+ return null;
747
+ }
748
+ function withinBps(px, reference, isBuy, bps) {
749
+ const bound = BigInt(bps);
750
+ return isBuy ? px * 10000n <= reference * (10000n + bound) : px * 10000n >= reference * (10000n - bound);
751
+ }
752
+ function checkActionIntent(action, opts) {
753
+ const intent = opts.intent;
650
754
  const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
651
755
  const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
652
756
  if (!Array.isArray(orderList))
@@ -659,6 +763,8 @@ function checkActionIntent(action, intent) {
659
763
  return "order asset is outside the main perp universe";
660
764
  if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
661
765
  return "invalid order side, size, price or reduce-only flag";
766
+ if (opts.keyReduceOnly === true && o.r !== true)
767
+ return "this key is reduce-only and the build is not reducing";
662
768
  }
663
769
  if (action.type === "cancel") {
664
770
  if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
@@ -676,24 +782,101 @@ function checkActionIntent(action, intent) {
676
782
  cancel: "cancel",
677
783
  modify: "modify",
678
784
  leverage: "updateLeverage",
679
- margin: "updateIsolatedMargin"
785
+ margin: "updateIsolatedMargin",
786
+ tpsl: "order",
787
+ scheduleCancel: "scheduleCancel"
680
788
  };
681
789
  if (action.type !== types[method])
682
790
  return "action type does not match the requested method";
791
+ if (opts.keyReduceOnly === true && method === "open")
792
+ return "this key is reduce-only and an open is not reducing";
793
+ if (method === "cancel" && opts.cancelTarget) {
794
+ const target = action.cancels[0];
795
+ if (target?.a !== opts.cancelTarget.asset || target?.o !== opts.cancelTarget.oid)
796
+ return "the cancel does not name the order Hyperliquid shows for this request";
797
+ }
683
798
  if (method === "leverage") {
684
- if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
799
+ const wanted = p.leverage ?? opts.leverageTarget?.leverage;
800
+ const deferred = wanted === undefined && opts.awaitingVenue === true && Number.isSafeInteger(action.leverage);
801
+ if (!deferred && (typeof wanted !== "number" || action.leverage !== wanted) || action.isCross !== (p.mode !== "isolated"))
685
802
  return "leverage or mode does not match the request";
803
+ const target = opts.leverageTarget;
804
+ if (target) {
805
+ if (action.asset !== target.asset)
806
+ return "leverage asset is not the requested coin's on Hyperliquid";
807
+ const mode = action.isCross ? "cross" : "isolated";
808
+ if (mode !== target.mode && !target.flat)
809
+ return "a margin-mode change needs no position and no resting order in the coin; close and cancel first";
810
+ if (opts.keyReduceOnly === true && (mode !== target.mode || action.leverage > target.leverage))
811
+ return "this key is reduce-only: no leverage increase or margin-mode change";
812
+ }
686
813
  }
687
814
  if (method === "margin") {
688
815
  const amount = decimal(p.amount);
689
816
  if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
690
817
  return "isolated margin amount does not match the request";
818
+ if (opts.marginTarget && action.asset !== opts.marginTarget.asset)
819
+ return "margin asset is not the requested coin's on Hyperliquid";
820
+ if (opts.keyReduceOnly === true && action.ntli <= 0)
821
+ return "this key is reduce-only: margin may only be added";
822
+ }
823
+ if (method === "scheduleCancel") {
824
+ if (p.clear === true) {
825
+ if ("time" in action)
826
+ return "a clear must omit the time";
827
+ } else {
828
+ const seconds = p.seconds;
829
+ const time = action.time;
830
+ if (!Number.isSafeInteger(seconds) || seconds < 5)
831
+ return "a scheduled cancel needs at least 5 seconds";
832
+ if (!Number.isSafeInteger(time))
833
+ return "a scheduled cancel without a time";
834
+ if (opts.clock) {
835
+ const lead = time - opts.clock.now;
836
+ if (lead < HYPERLIQUID_SCHEDULE_CANCEL_MIN_LEAD_MS)
837
+ return "the scheduled time is less than 5 seconds ahead; build it again";
838
+ if (Math.abs(time - (opts.clock.now + seconds * 1000)) > HYPERLIQUID_SCHEDULE_CANCEL_DRIFT_MS)
839
+ return "the scheduled time is not the requested delay from now";
840
+ }
841
+ }
842
+ }
843
+ if (method === "tpsl") {
844
+ const list = orderList;
845
+ const leg = list[0];
846
+ if (list.length !== 1 || !leg || action.grouping !== "positionTpsl")
847
+ return "a take-profit / stop-loss attach is one positionTpsl order";
848
+ const trigger = leg.t?.trigger;
849
+ if (!trigger || leg.r !== true || trigger.isMarket !== true || trigger.tpsl !== p.leg || decimal(trigger.triggerPx) !== decimal(p.triggerPx))
850
+ return "trigger leg, reduce-only flag or price does not match the request";
851
+ if (typeof leg.c !== "string" || !HYPERLIQUID_CLOID_RE.test(leg.c))
852
+ return "the trigger leg carries no client order id";
853
+ const triggerPx = decimal(trigger.triggerPx) ?? 0n;
854
+ if (triggerPx <= 0n || !withinBps(decimal(leg.p) ?? 0n, triggerPx, leg.b === true, HYPERLIQUID_TPSL_SLIPPAGE_BPS + HYPERLIQUID_MID_DRIFT_BPS))
855
+ return "trigger order price is outside its slippage of the trigger price";
856
+ const target = opts.tpslTarget;
857
+ if (target) {
858
+ if (leg.a !== target.asset || leg.b !== target.isBuy || decimal(leg.s) !== decimal(target.size))
859
+ return "trigger side, asset or size does not match the position Hyperliquid shows";
860
+ const mark = decimal(target.markPx) ?? 0n;
861
+ const above = triggerPx > mark;
862
+ const ok = p.leg === "tp" === !target.isBuy ? above : triggerPx < mark;
863
+ if (mark <= 0n || !ok)
864
+ return "the trigger is already crossed or on the wrong side of the mark";
865
+ }
691
866
  }
692
867
  if (method === "open" || method === "close" || method === "modify") {
693
868
  const list = orderList;
694
869
  const first = list[0];
695
870
  if (!first)
696
871
  return "missing requested order";
872
+ if ((method === "open" || method === "close") && p.clientId != null) {
873
+ const clientId = typeof p.clientId === "string" ? p.clientId.toLowerCase() : null;
874
+ if (clientId === null || !HYPERLIQUID_CLOID_RE.test(clientId) || first.c !== clientId)
875
+ return "the order does not carry the requested client order id";
876
+ const ids = list.map((o) => o.c);
877
+ if (ids.some((c) => typeof c !== "string" || !HYPERLIQUID_CLOID_RE.test(c)) || new Set(ids).size !== ids.length)
878
+ return "the order legs do not carry distinct client order ids";
879
+ }
697
880
  const typ = first.t;
698
881
  if (!typ?.limit)
699
882
  return "requested order is not a limit/IOC order";
@@ -720,15 +903,15 @@ function checkActionIntent(action, intent) {
720
903
  return "trigger side, size, reduce-only flag or price does not match the request";
721
904
  const px = decimal(leg.p) ?? 0n;
722
905
  const triggerPx = decimal(trigger.triggerPx) ?? 0n;
723
- const bound = BigInt(HYPERLIQUID_TPSL_SLIPPAGE_BPS + HYPERLIQUID_MID_DRIFT_BPS);
724
- const within = leg.b === true ? px * 10000n <= triggerPx * (10000n + bound) : px * 10000n >= triggerPx * (10000n - bound);
725
- if (triggerPx <= 0n || !within)
906
+ if (triggerPx <= 0n || !withinBps(px, triggerPx, leg.b === true, HYPERLIQUID_TPSL_SLIPPAGE_BPS + HYPERLIQUID_MID_DRIFT_BPS))
726
907
  return "trigger order price is outside its slippage of the trigger price";
727
908
  }
728
909
  } else {
729
910
  if (list.length !== 1)
730
911
  return "unexpected extra orders";
731
- if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
912
+ if (method === "close" && p.pct != null && p.size != null)
913
+ return "a close takes a size or a percentage, not both";
914
+ if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || action.grouping !== "na" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
732
915
  return "close size or reduce-only flag does not match the request";
733
916
  if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
734
917
  return "replacement size does not match the request";
@@ -738,9 +921,38 @@ function checkActionIntent(action, intent) {
738
921
  }
739
922
  return null;
740
923
  }
924
+ function hyperliquidClosePct(value) {
925
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]{0,2})(\.[0-9]{1,4})?$/.test(value))
926
+ return null;
927
+ const [whole = "0", frac = ""] = value.split(".");
928
+ const millionths = BigInt(whole) * 10000n + BigInt(frac.padEnd(4, "0"));
929
+ return millionths > 0n && millionths <= 1000000n ? millionths : null;
930
+ }
931
+ function hyperliquidPctSize(held, pct, szDecimals) {
932
+ const scaled = decimal(held);
933
+ if (scaled === null || scaled < 0n)
934
+ throw new Error("Invalid held size");
935
+ if (pct === 1000000n)
936
+ return held;
937
+ return hyperliquidFloorSize(formatScaled(scaled * pct / 1000000n), szDecimals);
938
+ }
939
+ function hyperliquidFloorSize(value, szDecimals) {
940
+ const scaled = decimal(value);
941
+ if (scaled === null || scaled < 0n)
942
+ throw new Error("Invalid size");
943
+ const unit = 10n ** BigInt(18 - Math.max(0, Math.min(18, szDecimals)));
944
+ return formatScaled(scaled / unit * unit);
945
+ }
946
+ function formatScaled(value) {
947
+ const negative = value < 0n;
948
+ const abs = negative ? -value : value;
949
+ const whole = abs / 10n ** 18n;
950
+ const frac = (abs % 10n ** 18n).toString().padStart(18, "0").replace(/0+$/, "");
951
+ return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
952
+ }
741
953
  async function hyperliquidCloseOrder(fetcher, network, address, params) {
742
954
  const read = async (body) => {
743
- const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
955
+ const res = await fetcher(HYPERLIQUID_INFO_URLS[network], {
744
956
  method: "POST",
745
957
  headers: { "content-type": "application/json" },
746
958
  body: JSON.stringify(body)
@@ -751,16 +963,32 @@ async function hyperliquidCloseOrder(fetcher, network, address, params) {
751
963
  };
752
964
  const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
753
965
  const coin = String(params.coin).toUpperCase();
754
- const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
966
+ const universe = meta.universe;
967
+ const asset = universe.findIndex((m) => m.name.toUpperCase() === coin);
755
968
  const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
756
969
  const signed = decimal(szi);
757
970
  if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
758
971
  throw new Error("No main-universe position to close");
759
972
  const held = signed < 0n ? -signed : signed;
973
+ const heldSz = String(szi).replace(/^-/, "");
974
+ if (params.pct != null) {
975
+ if (params.size != null)
976
+ throw new Error("A close takes a size or a percentage, not both");
977
+ const pct = hyperliquidClosePct(params.pct);
978
+ const szDecimals = universe[asset]?.szDecimals;
979
+ if (pct === null)
980
+ throw new Error("pct must be above 0 and at most 100, with at most 4 decimals");
981
+ if (!Number.isInteger(szDecimals))
982
+ throw new Error("Hyperliquid did not report this market's size precision");
983
+ const size2 = hyperliquidPctSize(heldSz, pct, szDecimals);
984
+ if ((decimal(size2) ?? 0n) <= 0n)
985
+ throw new Error("This percentage of the position rounds to zero at the market's size step");
986
+ return { asset, isBuy: signed < 0n, size: size2 };
987
+ }
760
988
  const asked = params.size == null ? held : decimal(params.size);
761
989
  if (asked === null || asked <= 0n)
762
990
  throw new Error("Invalid close size");
763
- const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
991
+ const size = asked < held ? String(params.size) : heldSz;
764
992
  return { asset, isBuy: signed < 0n, size };
765
993
  }
766
994
  var HYPERLIQUID_DEFAULT_SLIPPAGE_BPS = 100;
@@ -784,8 +1012,18 @@ async function hyperliquidOrderMarket(fetcher, network, address, method, params)
784
1012
  const resting = status?.order?.order ?? null;
785
1013
  if (!resting || typeof resting.coin !== "string" || decimal(resting.limitPx) === null)
786
1014
  throw new Error("No resting order with this cloid on Hyperliquid");
1015
+ if (resting.isTrigger === true)
1016
+ throw new Error("A take-profit or stop-loss leg cannot be modified; cancel it instead");
787
1017
  coin = resting.coin.toUpperCase();
788
- order = { isBuy: resting.side === "B", limitPx: String(resting.limitPx) };
1018
+ const tif = typeof resting.tif === "string" && ["Gtc", "Alo", "Ioc"].includes(resting.tif) ? resting.tif : "Gtc";
1019
+ order = {
1020
+ isBuy: resting.side === "B",
1021
+ limitPx: String(resting.limitPx),
1022
+ ...decimal(resting.sz) !== null ? { size: String(resting.sz) } : {},
1023
+ reduceOnly: resting.reduceOnly === true,
1024
+ tif,
1025
+ ...Number.isSafeInteger(resting.oid) ? { oid: resting.oid } : {}
1026
+ };
789
1027
  }
790
1028
  const [meta, contexts] = await read({ type: "metaAndAssetCtxs" });
791
1029
  const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
@@ -822,13 +1060,24 @@ function checkOrderMarket(action, intent, market) {
822
1060
  return "modify changes the resting order's side";
823
1061
  if (p.price == null && decimal(first.p) !== decimal(market.order.limitPx))
824
1062
  return "modify changes the resting order's price";
1063
+ const requested = typeof p.cloid === "string" ? p.cloid.toLowerCase() : null;
1064
+ const byOid = market.order.oid !== undefined && action.oid === market.order.oid;
1065
+ const byCloid = requested !== null && typeof action.oid === "string" && action.oid.toLowerCase() === requested;
1066
+ if (!byOid && !byCloid)
1067
+ return "modify does not name the resting order Hyperliquid shows for this cloid";
1068
+ if (first.r !== market.order.reduceOnly)
1069
+ return "modify changes the resting order's reduce-only flag";
1070
+ if (first.t?.limit?.tif !== market.order.tif)
1071
+ return "modify changes the resting order's time in force";
1072
+ if (p.size == null && (market.order.size === undefined || decimal(first.s) !== decimal(market.order.size)))
1073
+ return "modify changes the resting order's size";
825
1074
  }
826
1075
  return null;
827
1076
  }
828
1077
  function verifyPerpsBuild(build, opts) {
829
1078
  try {
830
1079
  const network = opts.network ?? "mainnet";
831
- if (!EVM_ADDRESS_RE.test(opts.builder))
1080
+ if (!EVM_ADDRESS_RE.test(opts.builder) || opts.builder.toLowerCase() === ZERO_ADDRESS)
832
1081
  return { ok: false, reason: "no valid Candle builder address to check against" };
833
1082
  if (build.network !== undefined && build.network !== network) {
834
1083
  return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
@@ -844,7 +1093,7 @@ function verifyPerpsBuild(build, opts) {
844
1093
  const action = build.action;
845
1094
  if (!action || typeof action !== "object")
846
1095
  return { ok: false, reason: "no action" };
847
- const intentRefusal = checkActionIntent(action, opts.intent);
1096
+ const intentRefusal = checkActionIntent(action, opts);
848
1097
  if (intentRefusal)
849
1098
  return { ok: false, reason: intentRefusal };
850
1099
  if (opts.closeOrder) {
@@ -864,6 +1113,9 @@ function verifyPerpsBuild(build, opts) {
864
1113
  if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
865
1114
  return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
866
1115
  }
1116
+ const shapeRefusal = actionShapeProblem(action);
1117
+ if (shapeRefusal)
1118
+ return { ok: false, reason: shapeRefusal };
867
1119
  if (type === "order") {
868
1120
  const refusal = checkBuilderField(action, opts.builder);
869
1121
  if (refusal)
@@ -902,19 +1154,2084 @@ function verifyPerpsBuild(build, opts) {
902
1154
  return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
903
1155
  }
904
1156
  }
905
- function hyperliquidSplitSignature(signature) {
906
- if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
907
- throw new Error("Not a 65-byte signature");
908
- let v = Number.parseInt(signature.slice(130, 132), 16);
909
- if (v < 27)
910
- v += 27;
911
- return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
1157
+ var HYPERLIQUID_SCHEDULE_CANCEL_MIN_LEAD_MS = 5000;
1158
+ var HYPERLIQUID_SCHEDULE_CANCEL_DRIFT_MS = 60000;
1159
+ async function infoRead(fetcher, network, body) {
1160
+ const res = await fetcher(HYPERLIQUID_INFO_URLS[network], {
1161
+ method: "POST",
1162
+ headers: { "content-type": "application/json" },
1163
+ body: JSON.stringify(body)
1164
+ });
1165
+ if (!res.ok)
1166
+ throw new Error(`Hyperliquid ${String(body.type)} answered ${res.status ?? "an error"}`);
1167
+ try {
1168
+ return JSON.parse(await res.text());
1169
+ } catch {
1170
+ throw new Error(`Hyperliquid returned an unreadable ${String(body.type)}`);
1171
+ }
1172
+ }
1173
+ async function hyperliquidWalletSnapshot(fetcher, network, address, now = Date.now) {
1174
+ const user = address.toLowerCase();
1175
+ const [metaAndCtxs, state, open] = await Promise.all([
1176
+ infoRead(fetcher, network, { type: "metaAndAssetCtxs" }),
1177
+ infoRead(fetcher, network, { type: "clearinghouseState", user }),
1178
+ infoRead(fetcher, network, { type: "frontendOpenOrders", user })
1179
+ ]);
1180
+ const readAt = now();
1181
+ if (!Array.isArray(metaAndCtxs) || metaAndCtxs.length < 2 || !Array.isArray(metaAndCtxs[1]))
1182
+ throw new Error("Hyperliquid returned an unreadable market list");
1183
+ const rawUniverse = metaAndCtxs[0]?.universe;
1184
+ if (!Array.isArray(rawUniverse))
1185
+ throw new Error("Hyperliquid returned an unreadable market list");
1186
+ const universe = rawUniverse.map((m) => {
1187
+ const row = m;
1188
+ if (typeof row.name !== "string" || !Number.isInteger(row.szDecimals) || !Number.isInteger(row.maxLeverage))
1189
+ throw new Error("Hyperliquid returned an unreadable market");
1190
+ return {
1191
+ name: row.name,
1192
+ szDecimals: row.szDecimals,
1193
+ maxLeverage: row.maxLeverage,
1194
+ onlyIsolated: row.onlyIsolated === true
1195
+ };
1196
+ });
1197
+ const ctxs = metaAndCtxs[1];
1198
+ const marks = universe.map((_, i) => {
1199
+ const mark = ctxs[i]?.markPx;
1200
+ return typeof mark === "string" && (decimal(mark) ?? 0n) > 0n ? mark : null;
1201
+ });
1202
+ const assetOf = (coin) => universe.findIndex((m) => m.name === coin);
1203
+ const unsupported = [];
1204
+ const positionsRaw = state?.assetPositions;
1205
+ if (!Array.isArray(positionsRaw))
1206
+ throw new Error("Hyperliquid returned an unreadable account state");
1207
+ const positions = [];
1208
+ for (const entry of positionsRaw) {
1209
+ const p = entry?.position;
1210
+ const coin = p?.coin;
1211
+ const lev = p?.leverage;
1212
+ const szi = p?.szi;
1213
+ if (typeof coin !== "string" || typeof szi !== "string" || decimal(szi) === null) {
1214
+ unsupported.push(`an unreadable position (${JSON.stringify(coin ?? null)})`);
1215
+ continue;
1216
+ }
1217
+ const asset = assetOf(coin);
1218
+ if (asset < 0 || asset >= 1e4 || lev?.type !== "cross" && lev?.type !== "isolated" || !Number.isInteger(lev.value)) {
1219
+ unsupported.push(`position ${coin}`);
1220
+ continue;
1221
+ }
1222
+ if (decimal(szi) === 0n)
1223
+ continue;
1224
+ positions.push({ coin, asset, szi, leverage: { type: lev.type, value: lev.value } });
1225
+ }
1226
+ if (!Array.isArray(open))
1227
+ throw new Error("Hyperliquid returned an unreadable order list");
1228
+ const orders = [];
1229
+ for (const raw of open) {
1230
+ const o = raw;
1231
+ const coin = o?.coin;
1232
+ if (typeof coin !== "string") {
1233
+ unsupported.push("an unreadable order");
1234
+ continue;
1235
+ }
1236
+ const asset = assetOf(coin);
1237
+ const oid = o.oid;
1238
+ if (asset < 0 || asset >= 1e4 || !Number.isSafeInteger(oid) || o.side !== "A" && o.side !== "B" || decimal(o.sz) === null || decimal(o.limitPx) === null) {
1239
+ unsupported.push(`order ${coin}${Number.isSafeInteger(oid) ? ` ${oid}` : ""}`);
1240
+ continue;
1241
+ }
1242
+ const orderType = typeof o.orderType === "string" ? o.orderType : "";
1243
+ const isTrigger = o.isTrigger === true;
1244
+ const tpsl = isTrigger ? orderType.startsWith("Take Profit") ? "tp" : orderType.startsWith("Stop") ? "sl" : null : null;
1245
+ if (isTrigger && tpsl === null) {
1246
+ unsupported.push(`order ${coin} ${oid} (${orderType || "unknown trigger"})`);
1247
+ continue;
1248
+ }
1249
+ orders.push({
1250
+ coin,
1251
+ asset,
1252
+ oid,
1253
+ cloid: typeof o.cloid === "string" ? o.cloid.toLowerCase() : null,
1254
+ isBuy: o.side === "B",
1255
+ sz: o.sz,
1256
+ limitPx: o.limitPx,
1257
+ reduceOnly: o.reduceOnly === true,
1258
+ isTrigger,
1259
+ triggerPx: typeof o.triggerPx === "string" ? o.triggerPx : null,
1260
+ tpsl
1261
+ });
1262
+ }
1263
+ orders.sort((a, b) => a.asset - b.asset || a.oid - b.oid);
1264
+ positions.sort((a, b) => a.asset - b.asset);
1265
+ return { readAt, universe, marks, positions, orders, unsupported };
1266
+ }
1267
+ function snapshotPosition(snapshot, coin) {
1268
+ const upper = coin.toUpperCase();
1269
+ return snapshot.positions.find((p) => p.coin.toUpperCase() === upper) ?? null;
1270
+ }
1271
+ function hyperliquidTpslTarget(snapshot, coin) {
1272
+ const position = snapshotPosition(snapshot, coin);
1273
+ if (!position)
1274
+ throw new Error(`No ${coin} position on Hyperliquid to protect`);
1275
+ const mark = snapshot.marks[position.asset];
1276
+ const szDecimals = snapshot.universe[position.asset]?.szDecimals;
1277
+ if (!mark || szDecimals === undefined)
1278
+ throw new Error(`No Hyperliquid mark for ${position.coin}`);
1279
+ const held = position.szi.replace(/^-/, "");
1280
+ const size = hyperliquidFloorSize(held, szDecimals);
1281
+ return { asset: position.asset, isBuy: position.szi.startsWith("-"), size, markPx: mark };
1282
+ }
1283
+ async function venueEvidence(fetcher, network, address, intent, now) {
1284
+ const p = intent.params;
1285
+ switch (intent.method) {
1286
+ case "open":
1287
+ case "modify":
1288
+ return { market: await hyperliquidOrderMarket(fetcher, network, address, intent.method, p) };
1289
+ case "close":
1290
+ return { closeOrder: await hyperliquidCloseOrder(fetcher, network, address, p) };
1291
+ case "cancel": {
1292
+ if (p.oid !== undefined) {
1293
+ const snapshot = await hyperliquidWalletSnapshot(fetcher, network, address, now);
1294
+ const order = snapshot.orders.find((o) => o.oid === p.oid && (p.coin === undefined || o.coin.toUpperCase() === String(p.coin).toUpperCase()));
1295
+ if (!order)
1296
+ throw new Error("Hyperliquid shows no such open order on this wallet");
1297
+ return { cancelTarget: { asset: order.asset, oid: order.oid } };
1298
+ }
1299
+ const status = await infoRead(fetcher, network, {
1300
+ type: "orderStatus",
1301
+ user: address.toLowerCase(),
1302
+ oid: p.cloid
1303
+ });
1304
+ const resting = status?.order?.order;
1305
+ if (!resting || typeof resting.coin !== "string" || !Number.isSafeInteger(resting.oid))
1306
+ throw new Error("Hyperliquid shows no order with this cloid on this wallet");
1307
+ const meta = await infoRead(fetcher, network, { type: "meta" });
1308
+ const asset = (meta.universe ?? []).findIndex((m) => m.name === resting.coin);
1309
+ if (asset < 0 || asset >= 1e4)
1310
+ throw new Error("The order is not on Hyperliquid's main perp exchange");
1311
+ return { cancelTarget: { asset, oid: resting.oid } };
1312
+ }
1313
+ case "leverage": {
1314
+ const snapshot = await hyperliquidWalletSnapshot(fetcher, network, address, now);
1315
+ const coin = String(p.coin ?? "").toUpperCase();
1316
+ const asset = snapshot.universe.findIndex((m) => m.name.toUpperCase() === coin);
1317
+ if (asset < 0)
1318
+ throw new Error("No main-universe market for this coin on Hyperliquid");
1319
+ const name = snapshot.universe[asset]?.name;
1320
+ const data = await infoRead(fetcher, network, {
1321
+ type: "activeAssetData",
1322
+ user: address.toLowerCase(),
1323
+ coin: name
1324
+ });
1325
+ const lev = data?.leverage;
1326
+ if (lev?.type !== "cross" && lev?.type !== "isolated" || !Number.isInteger(lev.value))
1327
+ throw new Error("Hyperliquid did not report this market's leverage");
1328
+ const flat = !snapshot.positions.some((x) => x.asset === asset) && !snapshot.orders.some((o) => o.asset === asset);
1329
+ return { leverageTarget: { asset, leverage: lev.value, mode: lev.type, flat } };
1330
+ }
1331
+ case "margin": {
1332
+ const snapshot = await hyperliquidWalletSnapshot(fetcher, network, address, now);
1333
+ const position = snapshotPosition(snapshot, String(p.coin ?? ""));
1334
+ if (!position || position.leverage.type !== "isolated")
1335
+ throw new Error("Hyperliquid shows no isolated position in this coin");
1336
+ return { marginTarget: { asset: position.asset } };
1337
+ }
1338
+ case "tpsl": {
1339
+ const snapshot = await hyperliquidWalletSnapshot(fetcher, network, address, now);
1340
+ return { tpslTarget: hyperliquidTpslTarget(snapshot, String(p.coin ?? "")) };
1341
+ }
1342
+ case "scheduleCancel":
1343
+ return { clock: { now: now() } };
1344
+ default:
1345
+ return {};
1346
+ }
1347
+ }
1348
+ async function verifyPerpsBuildAgainstVenue(build, opts) {
1349
+ const { fetcher, now = Date.now, ...verify } = opts;
1350
+ const first = verifyPerpsBuild(build, { ...verify, awaitingVenue: true });
1351
+ if (!first.ok)
1352
+ return first;
1353
+ let evidence;
1354
+ try {
1355
+ evidence = await venueEvidence(fetcher, verify.network ?? "mainnet", opts.address, opts.intent, now);
1356
+ } catch (err) {
1357
+ return {
1358
+ ok: false,
1359
+ reason: `could not check it against Hyperliquid: ${err instanceof Error ? err.message : "read failed"}`
1360
+ };
1361
+ }
1362
+ return verifyPerpsBuild(build, { ...verify, ...evidence });
1363
+ }
1364
+ function hyperliquidExchangeOutcome(exchange) {
1365
+ if (!exchange || typeof exchange !== "object")
1366
+ return { status: "unknown", error: "Hyperliquid returned no answer" };
1367
+ const answer = exchange;
1368
+ if (answer.status === "err")
1369
+ return {
1370
+ status: "rejected",
1371
+ error: typeof answer.response === "string" ? answer.response : JSON.stringify(answer.response ?? null),
1372
+ legs: []
1373
+ };
1374
+ if (answer.status !== "ok")
1375
+ return { status: "unknown", error: "Hyperliquid's answer has no status" };
1376
+ const response = answer.response;
1377
+ const statuses = response?.data?.statuses;
1378
+ if (statuses === undefined) {
1379
+ return response?.type === "default" || response?.type === "scheduleCancel" || response?.type === "cancel" ? { status: "confirmed", legs: [] } : { status: "unknown", error: "Hyperliquid's answer is not one this client knows" };
1380
+ }
1381
+ if (!Array.isArray(statuses))
1382
+ return { status: "unknown", error: "Hyperliquid's answer has unreadable statuses" };
1383
+ const errors = statuses.filter((s) => s && typeof s === "object" && ("error" in s)).map((s) => String(s.error));
1384
+ if (errors.length > 0)
1385
+ return { status: "rejected", error: errors.join("; "), legs: statuses };
1386
+ return { status: "confirmed", legs: statuses };
1387
+ }
1388
+ function hyperliquidSplitSignature(signature) {
1389
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
1390
+ throw new Error("Not a 65-byte signature");
1391
+ let v = Number.parseInt(signature.slice(130, 132), 16);
1392
+ if (v < 27)
1393
+ v += 27;
1394
+ return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
1395
+ }
1396
+ function hyperliquidExchangeBody(action, nonce, signature) {
1397
+ return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
1398
+ }
1399
+ function hyperliquidRelayBody(typedData) {
1400
+ return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
1401
+ }
1402
+
1403
+ // src/hyperliquid-manage.ts
1404
+ var PERPS_FLATTEN_MAX_STEPS = 100;
1405
+
1406
+ class PerpsWorkflowError extends Error {
1407
+ code;
1408
+ details;
1409
+ constructor(code, message, details = {}) {
1410
+ super(message);
1411
+ this.code = code;
1412
+ this.details = details;
1413
+ }
1414
+ }
1415
+ function errorCode(err) {
1416
+ const code = err?.code;
1417
+ return typeof code === "string" ? code : "REQUEST_FAILED";
1418
+ }
1419
+ function errorText(err) {
1420
+ return err instanceof Error ? err.message : String(err);
1421
+ }
1422
+ async function runStep(io, step, path, body, intent) {
1423
+ let build;
1424
+ try {
1425
+ build = await io.build(path, { walletId: io.walletId, ...body });
1426
+ } catch (err) {
1427
+ step.status = "rejected";
1428
+ step.detail = `${errorCode(err)}: ${errorText(err)}`;
1429
+ return false;
1430
+ }
1431
+ if (build.replayed === true) {
1432
+ step.status = "skipped";
1433
+ step.detail = "This client id already names an operation; nothing was signed";
1434
+ return false;
1435
+ }
1436
+ step.perpOrderId = typeof build.perpOrderId === "string" ? build.perpOrderId : null;
1437
+ step.nonce = typeof build.nonce === "number" ? build.nonce : null;
1438
+ if (typeof build.cloid === "string")
1439
+ step.cloid = build.cloid;
1440
+ const check = await verifyPerpsBuildAgainstVenue(build, {
1441
+ builder: io.builder,
1442
+ network: io.network,
1443
+ address: io.address,
1444
+ intent,
1445
+ fetcher: io.fetcher,
1446
+ now: io.now,
1447
+ keyReduceOnly: io.keyReduceOnly
1448
+ });
1449
+ if (!check.ok) {
1450
+ step.status = "rejected";
1451
+ step.detail = `Refused to sign this build: ${check.reason}. Nothing was signed.`;
1452
+ return false;
1453
+ }
1454
+ step.status = "built";
1455
+ if (io.mode === "no-submit")
1456
+ return false;
1457
+ let signature;
1458
+ try {
1459
+ signature = await io.sign(build.typedData);
1460
+ } catch (err) {
1461
+ step.status = "rejected";
1462
+ step.detail = `The relay did not sign (${errorCode(err)}: ${errorText(err)}). Nothing was submitted.`;
1463
+ return false;
1464
+ }
1465
+ step.signature = signature;
1466
+ const exchangeBody = hyperliquidExchangeBody(build.action, build.nonce, signature);
1467
+ if (io.mode === "sign-only") {
1468
+ step.exchangeBody = exchangeBody;
1469
+ return false;
1470
+ }
1471
+ let exchange;
1472
+ try {
1473
+ const res = await io.fetcher(HYPERLIQUID_EXCHANGE_URLS[io.network], {
1474
+ method: "POST",
1475
+ headers: { "content-type": "application/json" },
1476
+ body: JSON.stringify(exchangeBody)
1477
+ });
1478
+ const text = await res.text();
1479
+ if (!res.ok)
1480
+ throw new Error(`HTTP ${res.status ?? "error"}${res.status === 429 ? " (rate limited)" : ""}`);
1481
+ exchange = JSON.parse(text);
1482
+ } catch (err) {
1483
+ step.status = "unknown";
1484
+ step.detail = `Signed, but the submit to Hyperliquid did not answer (${errorText(err)}). Reconcile before acting again.`;
1485
+ step.exchangeBody = exchangeBody;
1486
+ return false;
1487
+ }
1488
+ step.exchange = exchange;
1489
+ const outcome = hyperliquidExchangeOutcome(exchange);
1490
+ if (outcome.status === "confirmed") {
1491
+ step.status = "confirmed";
1492
+ return true;
1493
+ }
1494
+ step.status = outcome.status;
1495
+ step.detail = outcome.error;
1496
+ if (outcome.status === "unknown")
1497
+ step.exchangeBody = exchangeBody;
1498
+ return false;
1499
+ }
1500
+ function blankStep(n, kind, coin, extra = {}) {
1501
+ return {
1502
+ step: n,
1503
+ kind,
1504
+ coin,
1505
+ target: null,
1506
+ perpOrderId: null,
1507
+ nonce: null,
1508
+ cloid: null,
1509
+ status: "planned",
1510
+ ...extra
1511
+ };
1512
+ }
1513
+ var DECIMAL_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
1514
+ var CLOID_RE = /^0x[0-9a-f]{32}$/;
1515
+ function perpsTpslParamsProblem(p) {
1516
+ if (typeof p.coin !== "string" || !/^[A-Za-z0-9]{1,20}$/.test(p.coin))
1517
+ return "coin must be a market name";
1518
+ for (const [field, value] of [
1519
+ ["takeProfit", p.takeProfit],
1520
+ ["stopLoss", p.stopLoss]
1521
+ ])
1522
+ if (value !== undefined && (typeof value !== "string" || !DECIMAL_RE.test(value) || /^0+(\.0+)?$/.test(value)))
1523
+ return `${field} must be a positive decimal`;
1524
+ if (p.takeProfit !== undefined && p.removeTp)
1525
+ return "cannot set and remove the take-profit together";
1526
+ if (p.stopLoss !== undefined && p.removeSl)
1527
+ return "cannot set and remove the stop-loss together";
1528
+ if (p.takeProfit === undefined && p.stopLoss === undefined && !p.removeTp && !p.removeSl)
1529
+ return "name at least one change: a take-profit, a stop-loss, or a removal";
1530
+ for (const [field, value] of [
1531
+ ["targetTp", p.targetTp],
1532
+ ["targetSl", p.targetSl]
1533
+ ])
1534
+ if (value !== undefined && (typeof value !== "string" || !CLOID_RE.test(value.toLowerCase())))
1535
+ return `${field} must be 0x followed by 32 hex digits`;
1536
+ if (p.targetTp !== undefined && p.takeProfit === undefined && !p.removeTp)
1537
+ return "targetTp needs a take-profit change";
1538
+ if (p.targetSl !== undefined && p.stopLoss === undefined && !p.removeSl)
1539
+ return "targetSl needs a stop-loss change";
1540
+ return null;
1541
+ }
1542
+ function sameDecimal(a, b) {
1543
+ if (typeof a !== "string" || typeof b !== "string")
1544
+ return false;
1545
+ const norm = (v) => (v.includes(".") ? v.replace(/0+$/, "").replace(/\.$/, "") : v).replace(/^0+(?=\d)/, "");
1546
+ return norm(a) === norm(b);
1547
+ }
1548
+ async function runPerpsTpsl(io, params) {
1549
+ const problem = perpsTpslParamsProblem(params);
1550
+ if (problem)
1551
+ throw new PerpsWorkflowError("VALIDATION_FAILED", problem);
1552
+ const coinQuery = encodeURIComponent(params.coin);
1553
+ const server = await io.read(`/api/v1/agent/perps/tpsl?walletId=${encodeURIComponent(io.walletId)}&coin=${coinQuery}`);
1554
+ let snapshot;
1555
+ try {
1556
+ snapshot = await hyperliquidWalletSnapshot(io.fetcher, io.network, io.address, io.now);
1557
+ } catch (err) {
1558
+ throw new PerpsWorkflowError("PERPS_UNAVAILABLE", `Cannot read the account from Hyperliquid: ${errorText(err)}`);
1559
+ }
1560
+ const coin = typeof server.coin === "string" ? server.coin : params.coin.toUpperCase();
1561
+ const venueLegs = snapshot.orders.filter((o) => o.coin === coin && o.tpsl !== null);
1562
+ const tracked = [];
1563
+ for (const raw of Array.isArray(server.tracked) ? server.tracked : []) {
1564
+ const seen = venueLegs.find((o) => o.oid === raw.oid && o.tpsl === raw.leg && (o.cloid ?? null) === (raw.cloid ?? null));
1565
+ if (seen)
1566
+ tracked.push({ leg: raw.leg, oid: seen.oid, cloid: seen.cloid, triggerPx: seen.triggerPx });
1567
+ }
1568
+ const trackedOids = new Set(tracked.map((l) => l.oid));
1569
+ const unowned = venueLegs.filter((o) => !trackedOids.has(o.oid)).map((o) => ({ leg: o.tpsl, oid: o.oid, cloid: o.cloid, triggerPx: o.triggerPx }));
1570
+ const position = snapshotPosition(snapshot, coin);
1571
+ const warnings = [];
1572
+ if (unowned.length > 0)
1573
+ warnings.push(`${unowned.length} take-profit / stop-loss leg(s) on ${coin} were not built by Candle; they are left untouched.`);
1574
+ const steps = [];
1575
+ const plans = [];
1576
+ for (const leg of ["tp", "sl"]) {
1577
+ const set = leg === "tp" ? params.takeProfit : params.stopLoss;
1578
+ const remove = leg === "tp" ? params.removeTp === true : params.removeSl === true;
1579
+ if (set === undefined && !remove)
1580
+ continue;
1581
+ const target = (leg === "tp" ? params.targetTp : params.targetSl)?.toLowerCase();
1582
+ let legs = tracked.filter((l) => l.leg === leg);
1583
+ if (target !== undefined) {
1584
+ legs = legs.filter((l) => l.cloid === target);
1585
+ if (legs.length === 0)
1586
+ throw new PerpsWorkflowError("PERPS_ORDER_NOT_FOUND", `No open ${coin} ${leg} leg Candle built carries ${target}`);
1587
+ }
1588
+ if (legs.length > 1)
1589
+ throw new PerpsWorkflowError("PERPS_TPSL_CONFLICT", `${legs.length} ${coin} ${leg === "tp" ? "take-profit" : "stop-loss"} legs match; name one with --target-${leg} <cloid>`, { candidates: legs });
1590
+ const existing = legs[0] ?? null;
1591
+ if (set !== undefined && existing && sameDecimal(existing.triggerPx, set)) {
1592
+ steps.push(blankStep(steps.length, "attach", coin, {
1593
+ leg,
1594
+ target: { oid: existing.oid, cloid: existing.cloid },
1595
+ status: "skipped",
1596
+ detail: "Already in place at this trigger"
1597
+ }));
1598
+ continue;
1599
+ }
1600
+ if (set !== undefined && !position)
1601
+ throw new PerpsWorkflowError("PERPS_NO_POSITION", `No ${coin} position on Hyperliquid to protect`);
1602
+ if (remove && !existing) {
1603
+ steps.push(blankStep(steps.length, "cancel", coin, { leg, status: "skipped", detail: "No tracked leg to remove" }));
1604
+ continue;
1605
+ }
1606
+ const cancel = existing ? blankStep(steps.length, "cancel", coin, { leg, target: { oid: existing.oid, cloid: existing.cloid } }) : null;
1607
+ if (cancel)
1608
+ steps.push(cancel);
1609
+ const attach = set !== undefined ? blankStep(steps.length, "attach", coin, { leg }) : null;
1610
+ if (attach)
1611
+ steps.push(attach);
1612
+ if (cancel && attach)
1613
+ warnings.push(`Changing the ${leg === "tp" ? "take-profit" : "stop-loss"} cancels the current leg first: ${coin} is unprotected on that side until the new one is placed.`);
1614
+ if (cancel && !attach)
1615
+ warnings.push(`Removing the ${leg === "tp" ? "take-profit" : "stop-loss"} leaves ${coin} without it.`);
1616
+ plans.push({ leg, cancel, attach, ...set !== undefined ? { triggerPx: set } : {} });
1617
+ }
1618
+ if (plans.some((p) => p.attach))
1619
+ warnings.push("New legs are sized to the position now and do not grow with later additions; triggering does not guarantee a fill.");
1620
+ const result = {
1621
+ operation: "tpsl",
1622
+ walletId: io.walletId,
1623
+ address: io.address,
1624
+ network: io.network,
1625
+ readAt: snapshot.readAt,
1626
+ complete: false,
1627
+ steps,
1628
+ remaining: [],
1629
+ warnings,
1630
+ unowned
1631
+ };
1632
+ const active = steps.filter((s) => s.status === "planned");
1633
+ if (active.length === 0) {
1634
+ result.complete = true;
1635
+ return result;
1636
+ }
1637
+ if (io.mode !== "no-submit" && io.confirm && !await io.confirm({ steps, warnings }))
1638
+ throw new PerpsWorkflowError("CANCELLED", "Nothing was signed.");
1639
+ const workflowId = `tpsl-${io.cloid().slice(2, 18)}`;
1640
+ let stopped = false;
1641
+ for (const plan of plans) {
1642
+ if (stopped)
1643
+ break;
1644
+ if (plan.cancel) {
1645
+ const step = plan.cancel;
1646
+ const ok = await runStep(io, step, "/api/v1/agent/perps/tpsl", {
1647
+ coin,
1648
+ operation: "cancel",
1649
+ leg: plan.leg,
1650
+ ...step.target?.cloid ? { targetCloid: step.target.cloid } : {},
1651
+ workflowId,
1652
+ workflowStep: step.step
1653
+ }, { method: "cancel", params: { coin, oid: step.target?.oid } });
1654
+ if (!ok) {
1655
+ stopped = true;
1656
+ break;
1657
+ }
1658
+ try {
1659
+ const after = await hyperliquidWalletSnapshot(io.fetcher, io.network, io.address, io.now);
1660
+ result.readAt = after.readAt;
1661
+ if (after.orders.some((o) => o.oid === step.target?.oid)) {
1662
+ step.status = "unknown";
1663
+ step.detail = "Hyperliquid accepted the cancel but still shows the leg open; no replacement was placed";
1664
+ stopped = true;
1665
+ break;
1666
+ }
1667
+ if (plan.attach && !snapshotPosition(after, coin)) {
1668
+ plan.attach.status = "skipped";
1669
+ plan.attach.detail = "The position closed meanwhile; no replacement was placed";
1670
+ continue;
1671
+ }
1672
+ } catch (err) {
1673
+ step.detail = `Canceled, but the follow-up read failed (${errorText(err)}); no replacement was placed`;
1674
+ stopped = true;
1675
+ break;
1676
+ }
1677
+ }
1678
+ if (plan.attach) {
1679
+ const ok = await runStep(io, plan.attach, "/api/v1/agent/perps/tpsl", {
1680
+ coin,
1681
+ operation: "attach",
1682
+ leg: plan.leg,
1683
+ triggerPx: plan.triggerPx,
1684
+ workflowId,
1685
+ workflowStep: plan.attach.step
1686
+ }, { method: "tpsl", params: { coin, leg: plan.leg, triggerPx: plan.triggerPx } });
1687
+ if (!ok)
1688
+ stopped = true;
1689
+ }
1690
+ }
1691
+ for (const s of steps) {
1692
+ if (s.status === "planned")
1693
+ result.remaining.push({ step: s.step, kind: s.kind, leg: s.leg, coin });
1694
+ if (s.kind === "cancel" && s.status === "confirmed" && plans.find((p) => p.cancel === s)?.attach?.status !== "confirmed")
1695
+ result.remaining.push({ leg: s.leg, coin, state: "old leg canceled, no new leg in place" });
1696
+ }
1697
+ result.complete = steps.every((s) => s.status === "confirmed" || s.status === "skipped") && !stopped;
1698
+ return result;
1699
+ }
1700
+ function orderTarget(o) {
1701
+ return { oid: o.oid, cloid: o.cloid };
1702
+ }
1703
+ function snapshotOrError(io) {
1704
+ return hyperliquidWalletSnapshot(io.fetcher, io.network, io.address, io.now);
1705
+ }
1706
+ async function runPerpsFlatten(io) {
1707
+ let snapshot;
1708
+ try {
1709
+ snapshot = await snapshotOrError(io);
1710
+ } catch (err) {
1711
+ throw new PerpsWorkflowError("PERPS_UNAVAILABLE", `Cannot read the account from Hyperliquid: ${errorText(err)}`);
1712
+ }
1713
+ if (snapshot.unsupported.length > 0)
1714
+ throw new PerpsWorkflowError("PERPS_MARKET_REFUSED", `Flatten reaches Hyperliquid's main perp exchange only, and this wallet holds rows it cannot classify there: ${snapshot.unsupported.slice(0, 5).join(", ")}. Nothing was built.`, { unsupported: snapshot.unsupported });
1715
+ const warnings = [
1716
+ "Scope: Hyperliquid's main perp exchange only. Spot, HIP-3 and vault exposure is not read or closed.",
1717
+ "Every open order is canceled first, take-profit and stop-loss protection included.",
1718
+ "The final state is an observed snapshot; another client can open exposure afterwards."
1719
+ ];
1720
+ const steps = [];
1721
+ for (const o of snapshot.orders)
1722
+ steps.push(blankStep(steps.length, "cancel", o.coin, { target: orderTarget(o) }));
1723
+ const closeIds = new Map;
1724
+ for (const p of snapshot.positions) {
1725
+ const id = io.cloid();
1726
+ closeIds.set(p.coin, id);
1727
+ steps.push(blankStep(steps.length, "close", p.coin, { cloid: id }));
1728
+ }
1729
+ const result = {
1730
+ operation: "flatten",
1731
+ walletId: io.walletId,
1732
+ address: io.address,
1733
+ network: io.network,
1734
+ readAt: snapshot.readAt,
1735
+ complete: false,
1736
+ steps,
1737
+ remaining: [],
1738
+ warnings,
1739
+ scope: "main-perps"
1740
+ };
1741
+ if (steps.length === 0) {
1742
+ result.complete = true;
1743
+ return result;
1744
+ }
1745
+ if (steps.length > PERPS_FLATTEN_MAX_STEPS)
1746
+ warnings.push(`${steps.length} steps planned; this run signs at most ${PERPS_FLATTEN_MAX_STEPS} and reports the rest as remaining. Run flatten again.`);
1747
+ if (io.mode !== "no-submit" && io.confirm && !await io.confirm({ steps, warnings }))
1748
+ throw new PerpsWorkflowError("CANCELLED", "Nothing was signed.");
1749
+ const workflowId = `flat-${io.cloid().slice(2, 18)}`;
1750
+ const cancelStep = (step) => runStep(io, step, "/api/v1/agent/perps/flatten/cancel", { coin: step.coin, oid: step.target?.oid, workflowId, workflowStep: step.step }, { method: "cancel", params: { coin: step.coin, oid: step.target?.oid } });
1751
+ const closeStep = (step) => {
1752
+ const clientId = closeIds.get(step.coin);
1753
+ return runStep(io, step, "/api/v1/agent/perps/close", { coin: step.coin, clientId, workflowId, workflowStep: step.step }, { method: "close", params: { coin: step.coin, clientId } });
1754
+ };
1755
+ if (io.mode !== "submit") {
1756
+ const first = steps[0];
1757
+ await (first.kind === "cancel" ? cancelStep(first) : closeStep(first));
1758
+ for (const s of steps)
1759
+ if (s.status === "planned")
1760
+ result.remaining.push({ step: s.step, kind: s.kind, coin: s.coin, target: s.target });
1761
+ return result;
1762
+ }
1763
+ let signed = 0;
1764
+ let stopped = false;
1765
+ const budgetLeft = () => signed < PERPS_FLATTEN_MAX_STEPS;
1766
+ for (const step of steps.filter((s) => s.kind === "cancel")) {
1767
+ if (stopped || !budgetLeft())
1768
+ break;
1769
+ signed++;
1770
+ if (await cancelStep(step))
1771
+ continue;
1772
+ if (step.status === "rejected" && step.detail?.startsWith("PERPS_ORDER_NOT_OPEN")) {
1773
+ step.status = "skipped";
1774
+ continue;
1775
+ }
1776
+ stopped = true;
1777
+ }
1778
+ if (!stopped && budgetLeft()) {
1779
+ let after = null;
1780
+ try {
1781
+ after = await snapshotOrError(io);
1782
+ result.readAt = after.readAt;
1783
+ } catch (err) {
1784
+ warnings.push(`The read after the cancels failed (${errorText(err)}); no position was closed.`);
1785
+ stopped = true;
1786
+ }
1787
+ if (after) {
1788
+ const fresh = after.orders.filter((o) => !steps.some((s) => s.kind === "cancel" && s.target?.oid === o.oid));
1789
+ if (fresh.length > 0)
1790
+ warnings.push(`${fresh.length} order(s) appeared during the cancels and were not part of this run's plan.`);
1791
+ for (const p of after.positions)
1792
+ if (!closeIds.has(p.coin)) {
1793
+ const id = io.cloid();
1794
+ closeIds.set(p.coin, id);
1795
+ steps.push(blankStep(steps.length, "close", p.coin, { cloid: id }));
1796
+ }
1797
+ for (const step of steps.filter((s) => s.kind === "close")) {
1798
+ if (stopped || !budgetLeft())
1799
+ break;
1800
+ let now;
1801
+ try {
1802
+ now = await snapshotOrError(io);
1803
+ result.readAt = now.readAt;
1804
+ } catch (err) {
1805
+ step.detail = `The read before this close failed (${errorText(err)})`;
1806
+ stopped = true;
1807
+ break;
1808
+ }
1809
+ const held = snapshotPosition(now, step.coin);
1810
+ if (!held) {
1811
+ step.status = "skipped";
1812
+ step.detail = "No position left to close";
1813
+ continue;
1814
+ }
1815
+ signed++;
1816
+ if (await closeStep(step)) {
1817
+ const leg = step.exchange?.response?.data?.statuses?.[0];
1818
+ const filled = typeof leg?.filled?.totalSz === "string" ? leg.filled.totalSz : null;
1819
+ const planned = held.szi.replace(/^-/, "");
1820
+ if (filled !== null) {
1821
+ step.filledSz = filled;
1822
+ if (!sameDecimal(filled, planned)) {
1823
+ step.remainingSz = `${planned} planned, ${filled} filled`;
1824
+ step.detail = "Partly filled; the rest of the position is still open";
1825
+ }
1826
+ }
1827
+ continue;
1828
+ }
1829
+ if (step.status !== "rejected" || /429|rate limit/i.test(step.detail ?? ""))
1830
+ stopped = true;
1831
+ }
1832
+ }
1833
+ }
1834
+ let final = null;
1835
+ try {
1836
+ final = await snapshotOrError(io);
1837
+ result.readAt = final.readAt;
1838
+ } catch (err) {
1839
+ warnings.push(`The final read failed (${errorText(err)}); the outcome is not known.`);
1840
+ }
1841
+ for (const s of steps)
1842
+ if (s.status === "planned")
1843
+ result.remaining.push({ step: s.step, kind: s.kind, coin: s.coin, target: s.target });
1844
+ if (final) {
1845
+ for (const p of final.positions)
1846
+ result.remaining.push({ kind: "position", coin: p.coin, szi: p.szi });
1847
+ for (const o of final.orders)
1848
+ result.remaining.push({ kind: "order", coin: o.coin, oid: o.oid });
1849
+ }
1850
+ result.complete = final !== null && final.positions.length === 0 && final.orders.length === 0 && final.unsupported.length === 0 && !stopped && steps.every((s) => s.status === "confirmed" || s.status === "skipped");
1851
+ return result;
1852
+ }
1853
+
1854
+ // src/hyperliquid-markets.ts
1855
+ var INFO_URLS = {
1856
+ mainnet: "https://api.hyperliquid.xyz/info",
1857
+ testnet: "https://api.hyperliquid-testnet.xyz/info"
1858
+ };
1859
+ var ONE = 10n ** 18n;
1860
+ var DECIMAL = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/;
1861
+
1862
+ class PerpsMarketError extends Error {
1863
+ code;
1864
+ retryable;
1865
+ constructor(code, message) {
1866
+ super(message);
1867
+ this.code = code;
1868
+ this.name = "PerpsMarketError";
1869
+ this.retryable = code === "PERPS_UNAVAILABLE";
1870
+ }
1871
+ }
1872
+ function unavailable() {
1873
+ throw new PerpsMarketError("PERPS_UNAVAILABLE", "Hyperliquid market data is unavailable; retry shortly.");
1874
+ }
1875
+ function usage(message) {
1876
+ throw new PerpsMarketError("PERPS_USAGE", message);
1877
+ }
1878
+ function object(value) {
1879
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1880
+ }
1881
+ function decimal2(value) {
1882
+ return typeof value === "string" && value.length <= 100 && DECIMAL.test(value) && (value.split(".")[1]?.length ?? 0) <= 18;
1883
+ }
1884
+ function scaled(value) {
1885
+ const negative = value.startsWith("-");
1886
+ const [whole = "0", fraction = ""] = (negative ? value.slice(1) : value).split(".");
1887
+ const result = BigInt(whole) * ONE + BigInt(fraction.padEnd(18, "0"));
1888
+ return negative ? -result : result;
1889
+ }
1890
+ function nonnegative(value) {
1891
+ return decimal2(value) && !value.startsWith("-") ? scaled(value) : null;
1892
+ }
1893
+ function fixed(value, places) {
1894
+ const negative = value < 0n;
1895
+ const abs = negative ? -value : value;
1896
+ const divisor = 10n ** BigInt(18 - places);
1897
+ const rounded = (abs + divisor / 2n) / divisor;
1898
+ const digits = rounded.toString().padStart(places + 1, "0");
1899
+ return `${negative && rounded !== 0n ? "-" : ""}${places ? `${digits.slice(0, -places)}.${digits.slice(-places)}` : digits}`;
1900
+ }
1901
+ function at(values, index) {
1902
+ const value = values[index];
1903
+ if (value === undefined)
1904
+ return unavailable();
1905
+ return value;
1906
+ }
1907
+ function integer(value) {
1908
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1909
+ }
1910
+ function parseMarkets(value) {
1911
+ if (!Array.isArray(value) || value.length !== 2)
1912
+ return unavailable();
1913
+ const [meta, ctxs] = value;
1914
+ if (!object(meta) || !Array.isArray(meta.universe) || !Array.isArray(ctxs) || meta.universe.length !== ctxs.length)
1915
+ return unavailable();
1916
+ for (const row of meta.universe) {
1917
+ if (!object(row) || typeof row.name !== "string" || !integer(row.szDecimals) || row.szDecimals > 6 || !integer(row.maxLeverage))
1918
+ return unavailable();
1919
+ for (const key of ["isDelisted", "onlyIsolated"])
1920
+ if (row[key] !== undefined && typeof row[key] !== "boolean")
1921
+ return unavailable();
1922
+ if (row.marginTableId !== undefined && !integer(row.marginTableId))
1923
+ return unavailable();
1924
+ }
1925
+ for (const ctx of ctxs) {
1926
+ if (!object(ctx))
1927
+ return unavailable();
1928
+ for (const key of ["markPx", "midPx", "oraclePx", "openInterest", "funding", "dayNtlVlm", "prevDayPx", "premium"]) {
1929
+ if (ctx[key] !== undefined && ctx[key] !== null && !decimal2(ctx[key]))
1930
+ return unavailable();
1931
+ }
1932
+ if (ctx.impactPxs != null && (!Array.isArray(ctx.impactPxs) || ctx.impactPxs.length !== 2 || !ctx.impactPxs.every(decimal2)))
1933
+ return unavailable();
1934
+ }
1935
+ const marginTables = new Map;
1936
+ if (meta.marginTables !== undefined) {
1937
+ if (!Array.isArray(meta.marginTables))
1938
+ return unavailable();
1939
+ for (const entry of meta.marginTables) {
1940
+ if (!Array.isArray(entry) || !integer(entry[0]) || !object(entry[1]) || !Array.isArray(entry[1].marginTiers))
1941
+ return unavailable();
1942
+ const tiers = [];
1943
+ for (const tier of entry[1].marginTiers) {
1944
+ if (!object(tier) || nonnegative(tier.lowerBound) === null || !integer(tier.maxLeverage))
1945
+ return unavailable();
1946
+ tiers.push({ lowerBoundUsd: tier.lowerBound, maxLeverage: tier.maxLeverage });
1947
+ }
1948
+ marginTables.set(entry[0], tiers);
1949
+ }
1950
+ }
1951
+ return { universe: meta.universe, ctxs, marginTables };
1952
+ }
1953
+ async function info(fetcher, network, body) {
1954
+ try {
1955
+ const response = await fetcher(INFO_URLS[network], {
1956
+ method: "POST",
1957
+ headers: { "Content-Type": "application/json" },
1958
+ body: JSON.stringify(body),
1959
+ signal: AbortSignal.timeout(5000),
1960
+ redirect: "error"
1961
+ });
1962
+ if (!response.ok)
1963
+ return unavailable();
1964
+ return JSON.parse(await response.text());
1965
+ } catch {
1966
+ return unavailable();
1967
+ }
1968
+ }
1969
+ async function readHyperliquidMarkets(fetcher, network) {
1970
+ return parseMarkets(await info(fetcher, network, { type: "metaAndAssetCtxs" }));
1971
+ }
1972
+ function perpsMarketGate(meta, ctx, minOpenInterestUsd) {
1973
+ if (meta.isDelisted === true)
1974
+ return { tradeable: false, reason: "delisted" };
1975
+ const mark = nonnegative(ctx.markPx);
1976
+ const mid = nonnegative(typeof ctx.midPx === "string" ? ctx.midPx : ctx.markPx);
1977
+ const oi = nonnegative(ctx.openInterest);
1978
+ if (mark === null || mid === null || oi === null)
1979
+ return { tradeable: false, reason: "noPrice" };
1980
+ const divisor = 10n ** 30n;
1981
+ const micros = (oi * mark + divisor - 1n) / divisor;
1982
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER))
1983
+ return unavailable();
1984
+ if (minOpenInterestUsd > 0 && micros < BigInt(minOpenInterestUsd) * 1000000n)
1985
+ return { tradeable: false, reason: "minOpenInterest" };
1986
+ return { tradeable: true, reason: null };
1987
+ }
1988
+ function resolve(snapshot, coin) {
1989
+ if (/^[A-Za-z0-9]{1,20}$/.test(coin)) {
1990
+ const exact = snapshot.universe.findIndex((row) => row.name === coin);
1991
+ if (exact !== -1)
1992
+ return exact;
1993
+ const matches = snapshot.universe.map((row, i) => ({ row, i })).filter(({ row }) => row.name.toLowerCase() === coin.toLowerCase());
1994
+ if (matches.length === 1)
1995
+ return at(matches, 0).i;
1996
+ }
1997
+ throw new PerpsMarketError("PERPS_MARKET_NOT_FOUND", `${coin} is not a market on Hyperliquid's main perp exchange.`);
1998
+ }
1999
+ var PERPS_CANDLE_INTERVALS = [
2000
+ "1m",
2001
+ "3m",
2002
+ "5m",
2003
+ "15m",
2004
+ "30m",
2005
+ "1h",
2006
+ "2h",
2007
+ "4h",
2008
+ "8h",
2009
+ "12h",
2010
+ "1d",
2011
+ "3d",
2012
+ "1w",
2013
+ "1M"
2014
+ ];
2015
+ var INTERVAL_MS = [
2016
+ 60000,
2017
+ 180000,
2018
+ 300000,
2019
+ 900000,
2020
+ 1800000,
2021
+ 3600000,
2022
+ 7200000,
2023
+ 14400000,
2024
+ 28800000,
2025
+ 43200000,
2026
+ 86400000,
2027
+ 259200000,
2028
+ 604800000,
2029
+ 2678400000
2030
+ ];
2031
+ function perpsCandleWindow(opts, now) {
2032
+ const interval = opts.interval ?? "1h";
2033
+ const index = PERPS_CANDLE_INTERVALS.indexOf(interval);
2034
+ if (index < 0)
2035
+ return usage(`interval must be one of ${PERPS_CANDLE_INTERVALS.join(" ")}.`);
2036
+ const count = bounded(opts.count, 100, 1, 5000, "count");
2037
+ const raw = opts.end;
2038
+ const endTime = raw === undefined ? now : typeof raw === "number" || /^\d+$/.test(raw) ? Number(raw) : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) ? Date.parse(raw) : NaN;
2039
+ const startTime = endTime - count * at(INTERVAL_MS, index);
2040
+ if (!Number.isSafeInteger(endTime) || endTime < 0 || endTime > now || startTime < 0)
2041
+ return usage("end must be an ISO 8601 time or epoch milliseconds, no later than now.");
2042
+ return { interval, count, startTime, endTime };
2043
+ }
2044
+ function bounded(value, fallback, min, max, name) {
2045
+ const n = value ?? fallback;
2046
+ if (!Number.isInteger(n) || n < min || n > max)
2047
+ return usage(`${name} must be an integer from ${min} to ${max}.`);
2048
+ return n;
2049
+ }
2050
+
2051
+ class PerpsMarketReader {
2052
+ opts;
2053
+ configPromise;
2054
+ coreCache;
2055
+ extrasCache;
2056
+ corePending;
2057
+ extrasPending;
2058
+ now;
2059
+ constructor(opts) {
2060
+ this.opts = opts;
2061
+ this.now = opts.now ?? Date.now;
2062
+ }
2063
+ key() {
2064
+ if (!this.opts.apiKey?.trim())
2065
+ throw new PerpsMarketError("PERPS_KEY_REQUIRED", "Configure a Candle agent key (any scope) to read perps markets.");
2066
+ }
2067
+ config() {
2068
+ this.key();
2069
+ this.configPromise ??= (async () => {
2070
+ try {
2071
+ const value = await this.opts.readConfig();
2072
+ if (!object(value) || !object(value.limits) || !integer(value.limits.minOpenInterestUsd) || value.network !== "mainnet" && value.network !== "testnet")
2073
+ throw new Error("config shape");
2074
+ return {
2075
+ minOpenInterestUsd: value.limits.minOpenInterestUsd,
2076
+ source: "server",
2077
+ network: value.network,
2078
+ warnings: []
2079
+ };
2080
+ } catch {
2081
+ return {
2082
+ minOpenInterestUsd: 1e6,
2083
+ source: "default",
2084
+ warnings: ["Candle config unavailable; using the release's default OI floor."]
2085
+ };
2086
+ }
2087
+ })();
2088
+ return this.configPromise;
2089
+ }
2090
+ core() {
2091
+ const ttl = this.opts.cacheMs ?? 5000;
2092
+ if (this.coreCache && ttl > 0 && this.now() - this.coreCache.at < ttl)
2093
+ return Promise.resolve(this.coreCache.value);
2094
+ this.corePending ??= readHyperliquidMarkets(this.opts.fetcher, this.opts.network).then((value) => {
2095
+ this.coreCache = { value, at: this.now() };
2096
+ return value;
2097
+ }).finally(() => {
2098
+ this.corePending = undefined;
2099
+ });
2100
+ return this.corePending;
2101
+ }
2102
+ extras() {
2103
+ const ttl = this.opts.cacheMs ?? 5000;
2104
+ if (this.extrasCache && ttl > 0 && this.now() - this.extrasCache.at < ttl)
2105
+ return Promise.resolve(this.extrasCache.value);
2106
+ this.extrasPending ??= (async () => {
2107
+ const warnings = [];
2108
+ const [predicted, cap] = await Promise.all([
2109
+ info(this.opts.fetcher, this.opts.network, { type: "predictedFundings" }).then((value2) => {
2110
+ if (!Array.isArray(value2))
2111
+ return unavailable();
2112
+ const map = new Map;
2113
+ for (const entry of value2) {
2114
+ if (!Array.isArray(entry) || typeof entry[0] !== "string" || !Array.isArray(entry[1]))
2115
+ return unavailable();
2116
+ for (const venue of entry[1]) {
2117
+ if (!Array.isArray(venue) || typeof venue[0] !== "string")
2118
+ return unavailable();
2119
+ if (venue[0] === "HlPerp" && venue[1] !== null) {
2120
+ if (!object(venue[1]) || !decimal2(venue[1].fundingRate) || !integer(venue[1].nextFundingTime))
2121
+ return unavailable();
2122
+ map.set(entry[0], { fundingRate: venue[1].fundingRate, nextFundingTime: venue[1].nextFundingTime });
2123
+ }
2124
+ }
2125
+ }
2126
+ return map;
2127
+ }).catch(() => {
2128
+ warnings.push("Predicted funding unavailable.");
2129
+ return null;
2130
+ }),
2131
+ info(this.opts.fetcher, this.opts.network, { type: "perpsAtOpenInterestCap" }).then((value2) => {
2132
+ if (!Array.isArray(value2) || !value2.every((v) => typeof v === "string"))
2133
+ return unavailable();
2134
+ return new Set(value2);
2135
+ }).catch(() => {
2136
+ warnings.push("Open-interest caps unavailable.");
2137
+ return null;
2138
+ })
2139
+ ]);
2140
+ const value = { predicted, cap, warnings: warnings.sort() };
2141
+ this.extrasCache = { value, at: this.now() };
2142
+ return value;
2143
+ })().finally(() => {
2144
+ this.extrasPending = undefined;
2145
+ });
2146
+ return this.extrasPending;
2147
+ }
2148
+ envelope(config, warnings = [], readAt = this.now()) {
2149
+ const mismatch = config.network !== undefined && config.network !== this.opts.network;
2150
+ return {
2151
+ success: true,
2152
+ network: this.opts.network,
2153
+ readAt,
2154
+ gate: { minOpenInterestUsd: config.minOpenInterestUsd, source: config.source },
2155
+ warnings: [
2156
+ ...config.warnings,
2157
+ ...warnings,
2158
+ ...mismatch ? ["Candle and client networks differ; tradeability is unknown."] : []
2159
+ ]
2160
+ };
2161
+ }
2162
+ row(core, extras, config, asset) {
2163
+ const meta = at(core.universe, asset);
2164
+ const ctx = at(core.ctxs, asset);
2165
+ const mark = ctx.markPx == null ? null : scaled(ctx.markPx);
2166
+ const oi = ctx.openInterest == null ? null : scaled(ctx.openInterest);
2167
+ const prev = ctx.prevDayPx == null ? null : scaled(ctx.prevDayPx);
2168
+ const predicted = extras.predicted?.get(meta.name);
2169
+ return {
2170
+ coin: meta.name,
2171
+ asset,
2172
+ markPx: ctx.markPx ?? null,
2173
+ oraclePx: ctx.oraclePx ?? null,
2174
+ midPx: ctx.midPx ?? null,
2175
+ fundingRate: ctx.funding ?? null,
2176
+ fundingAprPct: ctx.funding == null ? null : fixed(scaled(ctx.funding) * 876000n, 2),
2177
+ predictedFundingRate: predicted?.fundingRate ?? null,
2178
+ nextFundingTime: predicted?.nextFundingTime ?? null,
2179
+ openInterest: ctx.openInterest ?? null,
2180
+ openInterestUsd: oi === null || mark === null ? null : fixed(oi * mark / ONE, 2),
2181
+ dayNotionalVolumeUsd: ctx.dayNtlVlm ?? null,
2182
+ prevDayPx: ctx.prevDayPx ?? null,
2183
+ change24hPct: prev === null || prev === 0n || mark === null ? null : fixed((mark - prev) * 100n * ONE / prev, 1),
2184
+ maxLeverage: meta.maxLeverage,
2185
+ onlyIsolated: meta.onlyIsolated === true,
2186
+ isDelisted: meta.isDelisted === true,
2187
+ atOpenInterestCap: extras.cap?.has(meta.name) ?? null,
2188
+ candle: config.network !== undefined && config.network !== this.opts.network ? { tradeable: null, reason: "networkMismatch" } : perpsMarketGate(meta, ctx, config.minOpenInterestUsd)
2189
+ };
2190
+ }
2191
+ async markets(opts = {}) {
2192
+ this.key();
2193
+ const sort = opts.sort ?? "volume";
2194
+ if (!["volume", "oi", "funding", "change", "name"].includes(sort))
2195
+ return usage("sort must be volume, oi, funding, change or name.");
2196
+ const limit = opts.limit === undefined ? undefined : bounded(opts.limit, 500, 1, 500, "limit");
2197
+ const [config, core, extras] = await Promise.all([this.config(), this.core(), this.extras()]);
2198
+ const rows = core.universe.map((_, asset) => this.row(core, extras, config, asset)).filter((row) => opts.all || row.candle.tradeable !== false);
2199
+ const field = {
2200
+ volume: "dayNotionalVolumeUsd",
2201
+ oi: "openInterestUsd",
2202
+ funding: "fundingRate",
2203
+ change: "change24hPct"
2204
+ };
2205
+ rows.sort((a, b) => {
2206
+ if (sort !== "name") {
2207
+ const av = a[field[sort]];
2208
+ const bv = b[field[sort]];
2209
+ if (av === null && bv !== null)
2210
+ return 1;
2211
+ if (bv === null && av !== null)
2212
+ return -1;
2213
+ if (av !== null && bv !== null) {
2214
+ const diff = scaled(bv) - scaled(av);
2215
+ if (diff !== 0n)
2216
+ return diff > 0n ? 1 : -1;
2217
+ }
2218
+ }
2219
+ return a.coin < b.coin ? -1 : a.coin > b.coin ? 1 : 0;
2220
+ });
2221
+ return {
2222
+ ...this.envelope(config, extras.warnings, Math.max(this.coreCache?.at ?? 0, this.extrasCache?.at ?? 0)),
2223
+ markets: limit === undefined ? rows : rows.slice(0, limit)
2224
+ };
2225
+ }
2226
+ async market(coin) {
2227
+ this.key();
2228
+ const [config, core, extras] = await Promise.all([this.config(), this.core(), this.extras()]);
2229
+ const asset = resolve(core, coin);
2230
+ const meta = at(core.universe, asset);
2231
+ const ctx = at(core.ctxs, asset);
2232
+ return {
2233
+ ...this.envelope(config, extras.warnings, Math.max(this.coreCache?.at ?? 0, this.extrasCache?.at ?? 0)),
2234
+ market: {
2235
+ ...this.row(core, extras, config, asset),
2236
+ szDecimals: meta.szDecimals,
2237
+ maxPriceDecimals: 6 - meta.szDecimals,
2238
+ priceRule: "at most 5 significant figures",
2239
+ minOrderUsd: 10,
2240
+ impactBidPx: ctx.impactPxs?.[0] ?? null,
2241
+ impactAskPx: ctx.impactPxs?.[1] ?? null,
2242
+ premium: ctx.premium ?? null,
2243
+ marginTiers: core.marginTables.get(meta.marginTableId ?? -1) ?? [
2244
+ { lowerBoundUsd: "0", maxLeverage: meta.maxLeverage }
2245
+ ]
2246
+ }
2247
+ };
2248
+ }
2249
+ async book(coin, opts = {}) {
2250
+ this.key();
2251
+ const depth = bounded(opts.depth, 20, 1, 20, "depth");
2252
+ if (opts.sigFigs !== undefined)
2253
+ bounded(opts.sigFigs, 5, 2, 5, "sigFigs");
2254
+ const [config, core] = await Promise.all([this.config(), this.core()]);
2255
+ const canonical2 = at(core.universe, resolve(core, coin)).name;
2256
+ const value = await info(this.opts.fetcher, this.opts.network, {
2257
+ type: "l2Book",
2258
+ coin: canonical2,
2259
+ ...opts.sigFigs === undefined ? {} : { nSigFigs: opts.sigFigs }
2260
+ });
2261
+ if (!object(value) || value.coin !== canonical2 || !integer(value.time) || !Array.isArray(value.levels) || value.levels.length !== 2)
2262
+ return unavailable();
2263
+ const sides = [];
2264
+ for (const side of value.levels) {
2265
+ if (!Array.isArray(side))
2266
+ return unavailable();
2267
+ const levels = [];
2268
+ for (const level of side) {
2269
+ if (!object(level) || nonnegative(level.px) === null || nonnegative(level.sz) === null || !integer(level.n))
2270
+ return unavailable();
2271
+ levels.push({ px: level.px, sz: level.sz, n: level.n });
2272
+ }
2273
+ sides.push(levels);
2274
+ }
2275
+ const bids = at(sides, 0).sort((a, b) => scaled(a.px) > scaled(b.px) ? -1 : scaled(a.px) < scaled(b.px) ? 1 : 0).slice(0, depth);
2276
+ const asks = at(sides, 1).sort((a, b) => scaled(a.px) < scaled(b.px) ? -1 : scaled(a.px) > scaled(b.px) ? 1 : 0).slice(0, depth);
2277
+ const bid = bids[0];
2278
+ const ask = asks[0];
2279
+ const spread = bid && ask ? scaled(ask.px) - scaled(bid.px) : null;
2280
+ const mid = bid && ask ? (scaled(ask.px) + scaled(bid.px)) / 2n : null;
2281
+ return {
2282
+ ...this.envelope(config),
2283
+ coin: canonical2,
2284
+ time: value.time,
2285
+ bids,
2286
+ asks,
2287
+ spreadPx: spread === null ? null : fixed(spread, 18).replace(/\.?0+$/, ""),
2288
+ midPx: mid === null ? null : fixed(mid, 18).replace(/\.?0+$/, ""),
2289
+ spreadBps: spread === null || mid === null || mid === 0n ? null : fixed(spread * 10000n * ONE / mid, 2)
2290
+ };
2291
+ }
2292
+ async candles(coin, opts = {}) {
2293
+ this.key();
2294
+ const window = perpsCandleWindow(opts, this.now());
2295
+ const [config, core] = await Promise.all([this.config(), this.core()]);
2296
+ const canonical2 = at(core.universe, resolve(core, coin)).name;
2297
+ const value = await info(this.opts.fetcher, this.opts.network, {
2298
+ type: "candleSnapshot",
2299
+ req: { coin: canonical2, interval: window.interval, startTime: window.startTime, endTime: window.endTime }
2300
+ });
2301
+ if (!Array.isArray(value))
2302
+ return unavailable();
2303
+ const candles = [];
2304
+ for (const c of value) {
2305
+ if (!object(c) || !integer(c.t) || !integer(c.T) || !integer(c.n) || ![c.o, c.h, c.l, c.c, c.v].every((v) => nonnegative(v) !== null) || c.s !== canonical2 || c.i !== window.interval)
2306
+ return unavailable();
2307
+ if (c.t <= window.endTime && c.T >= window.startTime)
2308
+ candles.push({
2309
+ t: c.t,
2310
+ T: c.T,
2311
+ o: c.o,
2312
+ h: c.h,
2313
+ l: c.l,
2314
+ c: c.c,
2315
+ v: c.v,
2316
+ n: c.n
2317
+ });
2318
+ }
2319
+ candles.sort((a, b) => a.t - b.t);
2320
+ return {
2321
+ ...this.envelope(config),
2322
+ coin: canonical2,
2323
+ interval: window.interval,
2324
+ candles: candles.slice(-window.count)
2325
+ };
2326
+ }
2327
+ }
2328
+
2329
+ // src/hyperliquid-risk.ts
2330
+ var HYPERLIQUID_RISK_INFO_URLS = {
2331
+ mainnet: "https://api.hyperliquid.xyz/info",
2332
+ testnet: "https://api.hyperliquid-testnet.xyz/info"
2333
+ };
2334
+ var PERPS_RISK_FIELDS = [
2335
+ "warnDistanceBps",
2336
+ "criticalDistanceBps",
2337
+ "warnMarginRatioBps",
2338
+ "criticalMarginRatioBps",
2339
+ "clearBandBps"
2340
+ ];
2341
+ var HYPERLIQUID_RISK_DEFAULTS = {
2342
+ warnDistanceBps: 1500,
2343
+ criticalDistanceBps: 700,
2344
+ warnMarginRatioBps: 6000,
2345
+ criticalMarginRatioBps: 8000,
2346
+ clearBandBps: 200
2347
+ };
2348
+ function effectivePerpsRisk(stored) {
2349
+ const out = { ...HYPERLIQUID_RISK_DEFAULTS };
2350
+ for (const field of PERPS_RISK_FIELDS) {
2351
+ const value = stored?.[field];
2352
+ if (typeof value === "number")
2353
+ out[field] = value;
2354
+ }
2355
+ return out;
2356
+ }
2357
+ function perpsRiskProblem(t) {
2358
+ const ranges = {
2359
+ warnDistanceBps: [1, 9900],
2360
+ criticalDistanceBps: [1, 9900],
2361
+ warnMarginRatioBps: [1, 9999],
2362
+ criticalMarginRatioBps: [1, 9999],
2363
+ clearBandBps: [0, 1000]
2364
+ };
2365
+ for (const field of PERPS_RISK_FIELDS) {
2366
+ const value = t[field];
2367
+ const [min, max] = ranges[field];
2368
+ if (!Number.isInteger(value) || value < min || value > max)
2369
+ return `${field} must be a whole number of bps from ${min} to ${max}`;
2370
+ }
2371
+ if (t.criticalDistanceBps >= t.warnDistanceBps)
2372
+ return "criticalDistanceBps must be below warnDistanceBps";
2373
+ if (t.warnMarginRatioBps >= t.criticalMarginRatioBps)
2374
+ return "warnMarginRatioBps must be below criticalMarginRatioBps";
2375
+ return null;
2376
+ }
2377
+ function perpsRiskFromLimits(body) {
2378
+ const record = isRecord(body) ? body : {};
2379
+ const reported = "perpsRisk" in record || "perpsRiskEffective" in record;
2380
+ const stored = isRecord(record.perpsRisk) ? pickSettings(record.perpsRisk) : null;
2381
+ if (!reported)
2382
+ return { thresholds: { ...HYPERLIQUID_RISK_DEFAULTS, source: "defaults (server did not report)" }, stored };
2383
+ const effective = isRecord(record.perpsRiskEffective) ? effectivePerpsRisk(pickSettings(record.perpsRiskEffective)) : effectivePerpsRisk(stored);
2384
+ if (perpsRiskProblem(effective) !== null)
2385
+ return { thresholds: { ...HYPERLIQUID_RISK_DEFAULTS, source: "defaults (server did not report)" }, stored };
2386
+ const source = stored && Object.keys(stored).length > 0 ? "key" : "defaults";
2387
+ return { thresholds: { ...effective, source }, stored };
2388
+ }
2389
+ function pickSettings(raw) {
2390
+ const out = {};
2391
+ for (const field of PERPS_RISK_FIELDS)
2392
+ if (typeof raw[field] === "number")
2393
+ out[field] = raw[field];
2394
+ return out;
2395
+ }
2396
+
2397
+ class HyperliquidReadError extends Error {
2398
+ rateLimited;
2399
+ code = "PERPS_READ_FAILED";
2400
+ constructor(message, rateLimited = false) {
2401
+ super(message);
2402
+ this.rateLimited = rateLimited;
2403
+ this.name = "HyperliquidReadError";
2404
+ }
2405
+ }
2406
+ var INFO_TIMEOUT_MS = 5000;
2407
+ var RATE_LIMIT_RETRY_MS = 1000;
2408
+ async function hyperliquidInfo(fetcher, network, body, opts = {}) {
2409
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
2410
+ for (let attempt = 0;; attempt++) {
2411
+ const controller = new AbortController;
2412
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INFO_TIMEOUT_MS);
2413
+ let res;
2414
+ let text;
2415
+ try {
2416
+ res = await fetcher(HYPERLIQUID_RISK_INFO_URLS[network], {
2417
+ method: "POST",
2418
+ headers: { "content-type": "application/json" },
2419
+ body: JSON.stringify(body),
2420
+ signal: controller.signal
2421
+ });
2422
+ text = res.ok ? await res.text() : "";
2423
+ } catch {
2424
+ throw new HyperliquidReadError(`Hyperliquid ${String(body.type)} read failed or timed out`);
2425
+ } finally {
2426
+ clearTimeout(timer);
2427
+ }
2428
+ if (res.status === 429) {
2429
+ if (attempt === 0) {
2430
+ await sleep2(RATE_LIMIT_RETRY_MS);
2431
+ continue;
2432
+ }
2433
+ throw new HyperliquidReadError(`Hyperliquid rate-limited the ${String(body.type)} read (HTTP 429)`, true);
2434
+ }
2435
+ if (!res.ok)
2436
+ throw new HyperliquidReadError(`Hyperliquid ${String(body.type)} answered HTTP ${res.status ?? "?"}`);
2437
+ try {
2438
+ return JSON.parse(text);
2439
+ } catch {
2440
+ throw new HyperliquidReadError(`Hyperliquid ${String(body.type)} answered with something other than JSON`);
2441
+ }
2442
+ }
2443
+ }
2444
+ async function readHyperliquidMarketSnapshot(fetcher, network, opts = {}) {
2445
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
2446
+ let limited = false;
2447
+ const watched = async (url, init) => {
2448
+ const res = await fetcher(url, init);
2449
+ limited = res.status === 429;
2450
+ return res;
2451
+ };
2452
+ try {
2453
+ return await readHyperliquidMarkets(watched, network);
2454
+ } catch {
2455
+ await sleep2(RATE_LIMIT_RETRY_MS);
2456
+ }
2457
+ try {
2458
+ return await readHyperliquidMarkets(watched, network);
2459
+ } catch (error) {
2460
+ throw new HyperliquidReadError(`Hyperliquid metaAndAssetCtxs read failed${error instanceof PerpsMarketError ? ` (${error.code})` : ""}`, limited);
2461
+ }
2462
+ }
2463
+ function hyperliquidMark(markets, coin) {
2464
+ const index = markets.universe.findIndex((m) => m.name === coin);
2465
+ const mark = index < 0 ? undefined : markets.ctxs[index]?.markPx;
2466
+ if (typeof mark !== "string")
2467
+ return null;
2468
+ const parsed = dec(mark);
2469
+ return parsed !== null && parsed > 0n ? mark : null;
2470
+ }
2471
+ async function readHyperliquidClearinghouse(fetcher, network, address, opts = {}) {
2472
+ return parseClearinghouse(await hyperliquidInfo(fetcher, network, { type: "clearinghouseState", user: address.toLowerCase() }, opts));
2473
+ }
2474
+ async function readHyperliquidAccountMode(fetcher, network, address, opts = {}) {
2475
+ const body = await hyperliquidInfo(fetcher, network, { type: "userAbstraction", user: address.toLowerCase() }, opts);
2476
+ if (typeof body !== "string" || body === "")
2477
+ throw new HyperliquidReadError("Unexpected userAbstraction shape");
2478
+ return body;
2479
+ }
2480
+ var HYPERLIQUID_STANDARD_MODES = ["default", "disabled"];
2481
+ function parseClearinghouse(body) {
2482
+ const bad = (what) => new HyperliquidReadError(`Unexpected clearinghouseState shape (${what})`);
2483
+ if (!isRecord(body))
2484
+ throw bad("not an object");
2485
+ const summary = (value, name) => {
2486
+ if (!isRecord(value) || !isDecString(value.accountValue) || !isDecString(value.totalMarginUsed))
2487
+ throw bad(name);
2488
+ return { accountValue: value.accountValue, totalMarginUsed: value.totalMarginUsed };
2489
+ };
2490
+ const marginSummary = summary(body.marginSummary, "marginSummary");
2491
+ const crossMarginSummary = summary(body.crossMarginSummary, "crossMarginSummary");
2492
+ if (!isDecString(body.crossMaintenanceMarginUsed))
2493
+ throw bad("crossMaintenanceMarginUsed");
2494
+ if (!isDecString(body.withdrawable))
2495
+ throw bad("withdrawable");
2496
+ if (!Array.isArray(body.assetPositions))
2497
+ throw bad("assetPositions");
2498
+ const positions = body.assetPositions.map((entry) => {
2499
+ const p = isRecord(entry) ? entry.position : undefined;
2500
+ if (!isRecord(p) || typeof p.coin !== "string" || p.coin === "")
2501
+ throw bad("position");
2502
+ for (const field of ["szi", "entryPx", "positionValue", "unrealizedPnl", "returnOnEquity", "marginUsed"])
2503
+ if (!isDecString(p[field]))
2504
+ throw bad(`position ${field}`);
2505
+ const leverage = p.leverage;
2506
+ if (!isRecord(leverage) || typeof leverage.type !== "string" || typeof leverage.value !== "number")
2507
+ throw bad("position leverage");
2508
+ if (p.liquidationPx !== null && p.liquidationPx !== undefined && !isDecString(p.liquidationPx))
2509
+ throw bad("position liquidationPx");
2510
+ const funding = p.cumFunding;
2511
+ if (!isRecord(funding) || !isDecString(funding.sinceOpen))
2512
+ throw bad("position cumFunding");
2513
+ return {
2514
+ coin: p.coin,
2515
+ szi: p.szi,
2516
+ entryPx: p.entryPx,
2517
+ positionValue: p.positionValue,
2518
+ unrealizedPnl: p.unrealizedPnl,
2519
+ returnOnEquity: p.returnOnEquity,
2520
+ leverage: { type: leverage.type, value: leverage.value },
2521
+ liquidationPx: p.liquidationPx ?? null,
2522
+ marginUsed: p.marginUsed,
2523
+ cumFunding: { sinceOpen: funding.sinceOpen }
2524
+ };
2525
+ });
2526
+ return {
2527
+ marginSummary,
2528
+ crossMarginSummary,
2529
+ crossMaintenanceMarginUsed: body.crossMaintenanceMarginUsed,
2530
+ withdrawable: body.withdrawable,
2531
+ positions
2532
+ };
2533
+ }
2534
+ var PERPS_RISK_NOTE = "Phase 1 has no server-side monitor: nothing warns you unless you poll this or run candle perps watch.";
2535
+ var RANK = { ok: 0, warn: 1, critical: 2 };
2536
+ function worse(a, b) {
2537
+ return RANK[a] >= RANK[b] ? a : b;
2538
+ }
2539
+ function hyperliquidRiskReport(state, market, mode, thresholds, now, identity = { walletId: "", address: "", network: "mainnet" }) {
2540
+ const base = {
2541
+ schema: "candle.perps.risk/1",
2542
+ walletId: identity.walletId,
2543
+ address: identity.address,
2544
+ network: identity.network,
2545
+ mode,
2546
+ readAt: new Date(now).toISOString(),
2547
+ thresholds,
2548
+ note: PERPS_RISK_NOTE
2549
+ };
2550
+ if (!HYPERLIQUID_STANDARD_MODES.includes(mode)) {
2551
+ return {
2552
+ ...base,
2553
+ level: "unknown",
2554
+ reason: "PERPS_ACCOUNT_MODE",
2555
+ drivers: [],
2556
+ closest: null,
2557
+ account: null,
2558
+ positions: []
2559
+ };
2560
+ }
2561
+ const ch = isParsedClearinghouse(state) ? state : parseClearinghouse(state);
2562
+ if (!market || !Array.isArray(market.universe) || !Array.isArray(market.ctxs))
2563
+ throw new HyperliquidReadError("Unexpected metaAndAssetCtxs shape");
2564
+ const markets = market;
2565
+ const t = thresholds;
2566
+ const drivers = [];
2567
+ const nearest = [];
2568
+ let level = "ok";
2569
+ let unrealized = 0n;
2570
+ const positions = ch.positions.map((p) => {
2571
+ const szi = dec(p.szi);
2572
+ unrealized += dec(p.unrealizedPnl);
2573
+ const markPx = hyperliquidMark(markets, p.coin);
2574
+ const distance = markPx !== null && p.liquidationPx !== null ? distanceParts(markPx, p.liquidationPx) : null;
2575
+ let rowLevel = "none";
2576
+ if (distance) {
2577
+ rowLevel = fracLess(distance.num, distance.den, t.criticalDistanceBps) ? "critical" : fracLess(distance.num, distance.den, t.warnDistanceBps) ? "warn" : "ok";
2578
+ }
2579
+ const liquidationDistance = distance ? ratioNumber(distance.num, distance.den) : null;
2580
+ if (markPx === null) {
2581
+ drivers.push({
2582
+ kind: "position",
2583
+ coin: p.coin,
2584
+ level: "warn",
2585
+ liquidationDistance: null,
2586
+ threshold: null,
2587
+ reason: "unmarked"
2588
+ });
2589
+ level = worse(level, "warn");
2590
+ } else if (rowLevel === "warn" || rowLevel === "critical") {
2591
+ drivers.push({
2592
+ kind: "position",
2593
+ coin: p.coin,
2594
+ level: rowLevel,
2595
+ liquidationDistance,
2596
+ threshold: (rowLevel === "critical" ? t.criticalDistanceBps : t.warnDistanceBps) / 1e4
2597
+ });
2598
+ level = worse(level, rowLevel);
2599
+ } else if (rowLevel === "ok" && liquidationDistance !== null) {
2600
+ nearest.push({
2601
+ driver: {
2602
+ kind: "position",
2603
+ coin: p.coin,
2604
+ level: "ok",
2605
+ liquidationDistance,
2606
+ threshold: t.warnDistanceBps / 1e4
2607
+ },
2608
+ headroom: liquidationDistance / (t.warnDistanceBps / 1e4) - 1
2609
+ });
2610
+ }
2611
+ return {
2612
+ coin: p.coin,
2613
+ side: szi < 0n ? "short" : "long",
2614
+ size: p.szi.replace(/^-/, ""),
2615
+ entryPx: p.entryPx,
2616
+ markPx,
2617
+ unrealizedPnlUsd: p.unrealizedPnl,
2618
+ returnOnEquity: p.returnOnEquity,
2619
+ leverage: { value: p.leverage.value, type: p.leverage.type },
2620
+ liquidationPx: p.liquidationPx,
2621
+ liquidationDistance,
2622
+ fundingSinceOpenUsd: p.cumFunding.sinceOpen,
2623
+ marginUsedUsd: p.marginUsed,
2624
+ positionValueUsd: p.positionValue,
2625
+ level: rowLevel
2626
+ };
2627
+ });
2628
+ const maint = dec(ch.crossMaintenanceMarginUsed);
2629
+ const crossValue = dec(ch.crossMarginSummary.accountValue);
2630
+ let marginRatio = 0;
2631
+ if (positions.length > 0 && maint > 0n) {
2632
+ if (crossValue <= 0n) {
2633
+ marginRatio = null;
2634
+ drivers.push({
2635
+ kind: "margin",
2636
+ level: "critical",
2637
+ marginRatio: null,
2638
+ threshold: t.criticalMarginRatioBps / 1e4,
2639
+ reason: "no equity"
2640
+ });
2641
+ level = "critical";
2642
+ } else {
2643
+ marginRatio = ratioNumber(maint, crossValue);
2644
+ const marginLevel = fracGreater(maint, crossValue, t.criticalMarginRatioBps) ? "critical" : fracGreater(maint, crossValue, t.warnMarginRatioBps) ? "warn" : "ok";
2645
+ if (marginLevel === "ok") {
2646
+ nearest.push({
2647
+ driver: { kind: "margin", level: "ok", marginRatio, threshold: t.warnMarginRatioBps / 1e4 },
2648
+ headroom: 1 - marginRatio / (t.warnMarginRatioBps / 1e4)
2649
+ });
2650
+ } else {
2651
+ drivers.push({
2652
+ kind: "margin",
2653
+ level: marginLevel,
2654
+ marginRatio,
2655
+ threshold: (marginLevel === "critical" ? t.criticalMarginRatioBps : t.warnMarginRatioBps) / 1e4
2656
+ });
2657
+ level = worse(level, marginLevel);
2658
+ }
2659
+ }
2660
+ }
2661
+ drivers.sort((a, b) => RANK[b.level] - RANK[a.level]);
2662
+ nearest.sort((a, b) => a.headroom - b.headroom);
2663
+ const accountValue = dec(ch.marginSummary.accountValue);
2664
+ const marginUsed = dec(ch.marginSummary.totalMarginUsed);
2665
+ const free = accountValue - marginUsed;
2666
+ const account = {
2667
+ accountValueUsd: ch.marginSummary.accountValue,
2668
+ marginUsedUsd: ch.marginSummary.totalMarginUsed,
2669
+ crossAccountValueUsd: ch.crossMarginSummary.accountValue,
2670
+ freeMarginUsd: formatDec(free > 0n ? free : 0n),
2671
+ withdrawableUsd: ch.withdrawable,
2672
+ crossMaintenanceMarginUsedUsd: ch.crossMaintenanceMarginUsed,
2673
+ marginRatio,
2674
+ unrealizedPnlUsd: formatDec(unrealized),
2675
+ positionCount: positions.length,
2676
+ mode,
2677
+ level
2678
+ };
2679
+ return {
2680
+ ...base,
2681
+ level,
2682
+ reason: null,
2683
+ drivers,
2684
+ closest: level === "ok" ? nearest[0]?.driver ?? null : null,
2685
+ account,
2686
+ positions
2687
+ };
2688
+ }
2689
+ function unknownRiskReport(identity, thresholds, reason, now, mode = null) {
2690
+ return {
2691
+ schema: "candle.perps.risk/1",
2692
+ ...identity,
2693
+ mode,
2694
+ readAt: new Date(now).toISOString(),
2695
+ level: "unknown",
2696
+ reason,
2697
+ drivers: [],
2698
+ closest: null,
2699
+ thresholds,
2700
+ account: null,
2701
+ positions: [],
2702
+ note: PERPS_RISK_NOTE
2703
+ };
2704
+ }
2705
+ var PERPS_RISK_STALE_MS = 30000;
2706
+ async function readPerpsRisk(opts) {
2707
+ const identity = { walletId: opts.walletId, address: opts.address, network: opts.network };
2708
+ const timed = async (read) => {
2709
+ const value = await read;
2710
+ return { value, at: opts.now() };
2711
+ };
2712
+ const [state, market, mode] = await Promise.all([
2713
+ timed(readHyperliquidClearinghouse(opts.fetcher, opts.network, opts.address, opts)),
2714
+ timed(readHyperliquidMarketSnapshot(opts.fetcher, opts.network, opts)),
2715
+ opts.mode !== undefined ? Promise.resolve({ value: opts.mode, at: Number.NaN }) : timed(readHyperliquidAccountMode(opts.fetcher, opts.network, opts.address, opts))
2716
+ ]);
2717
+ const times = [state.at, market.at, mode.at].filter((at2) => !Number.isNaN(at2));
2718
+ const earliest = Math.min(...times);
2719
+ if (Math.max(...times) - earliest > PERPS_RISK_STALE_MS)
2720
+ return unknownRiskReport(identity, opts.thresholds, "PERPS_READ_STALE", earliest, mode.value);
2721
+ return hyperliquidRiskReport(state.value, market.value, mode.value, opts.thresholds, earliest, identity);
2722
+ }
2723
+ function perpsRiskExitCode(level) {
2724
+ return level === "ok" ? 0 : level === "warn" ? 10 : level === "critical" ? 11 : 1;
2725
+ }
2726
+ var PERPS_FILLS_RESPONSE_CAP = 2000;
2727
+ var PERPS_FUNDING_RESPONSE_CAP = 500;
2728
+ var PERPS_RECENT_MAX_MS = 30 * 24 * 60 * 60 * 1000;
2729
+ function isMainPerpCoin(coin) {
2730
+ return typeof coin === "string" && coin !== "" && !coin.includes(":") && !coin.includes("/") && !coin.startsWith("@");
2731
+ }
2732
+ async function readPerpsRecent(fetcher, network, address, window, opts = {}) {
2733
+ const user = address.toLowerCase();
2734
+ const [fills, funding] = await Promise.all([
2735
+ hyperliquidInfo(fetcher, network, { type: "userFillsByTime", user, startTime: window.startTime, endTime: window.endTime }, opts),
2736
+ hyperliquidInfo(fetcher, network, { type: "userFunding", user, startTime: window.startTime, endTime: window.endTime }, opts)
2737
+ ]);
2738
+ return summarizePerpsRecent(fills, funding, window);
2739
+ }
2740
+ function summarizePerpsRecent(fills, funding, window) {
2741
+ if (!Array.isArray(fills))
2742
+ throw new HyperliquidReadError("Unexpected userFillsByTime shape");
2743
+ if (!Array.isArray(funding))
2744
+ throw new HyperliquidReadError("Unexpected userFunding shape");
2745
+ let realized = 0n;
2746
+ let fees = 0n;
2747
+ let paid = 0n;
2748
+ let fillCount = 0;
2749
+ let fundingCount = 0;
2750
+ for (const row of fills) {
2751
+ if (!isRecord(row))
2752
+ throw new HyperliquidReadError("Unexpected fill row");
2753
+ if (!isMainPerpCoin(row.coin))
2754
+ continue;
2755
+ const closed = dec(row.closedPnl);
2756
+ const fee = dec(row.fee);
2757
+ if (closed === null || fee === null)
2758
+ throw new HyperliquidReadError("Unexpected fill amount");
2759
+ realized += closed;
2760
+ fees += fee;
2761
+ fillCount++;
2762
+ }
2763
+ for (const row of funding) {
2764
+ const delta = isRecord(row) ? row.delta : undefined;
2765
+ if (!isRecord(delta))
2766
+ throw new HyperliquidReadError("Unexpected funding row");
2767
+ if (delta.type !== "funding" || !isMainPerpCoin(delta.coin))
2768
+ continue;
2769
+ const usdc = dec(delta.usdc);
2770
+ if (usdc === null)
2771
+ throw new HyperliquidReadError("Unexpected funding amount");
2772
+ paid += usdc;
2773
+ fundingCount++;
2774
+ }
2775
+ return {
2776
+ startTime: window.startTime,
2777
+ endTime: window.endTime,
2778
+ realizedPnlUsd: formatDec(realized),
2779
+ feesUsd: formatDec(fees),
2780
+ fundingUsd: formatDec(paid),
2781
+ fills: fillCount,
2782
+ fundingPayments: fundingCount,
2783
+ truncated: fills.length >= PERPS_FILLS_RESPONSE_CAP || funding.length >= PERPS_FUNDING_RESPONSE_CAP
2784
+ };
2785
+ }
2786
+ function perpsAccountReport(report, recent) {
2787
+ const { mode: _mode, level: _level, ...line } = report.account ?? {};
2788
+ return {
2789
+ schema: "candle.perps.account/1",
2790
+ walletId: report.walletId,
2791
+ address: report.address,
2792
+ network: report.network,
2793
+ mode: report.mode,
2794
+ readAt: report.readAt,
2795
+ ...line,
2796
+ recent,
2797
+ risk: { level: report.level, reason: report.reason, drivers: report.drivers }
2798
+ };
2799
+ }
2800
+ function perpsRiskClears(report, leaving) {
2801
+ const t = report.thresholds;
2802
+ const account = report.account;
2803
+ if (!account)
2804
+ return false;
2805
+ const distanceBps = (leaving === "warn" ? t.warnDistanceBps : t.criticalDistanceBps) + t.clearBandBps;
2806
+ const ratioBps = (leaving === "warn" ? t.warnMarginRatioBps : t.criticalMarginRatioBps) - t.clearBandBps;
2807
+ for (const p of report.positions) {
2808
+ if (p.markPx === null) {
2809
+ if (leaving === "warn")
2810
+ return false;
2811
+ continue;
2812
+ }
2813
+ if (p.liquidationPx === null)
2814
+ continue;
2815
+ const d = distanceParts(p.markPx, p.liquidationPx);
2816
+ if (d && fracLess(d.num, d.den, distanceBps))
2817
+ return false;
2818
+ }
2819
+ const maint = dec(account.crossMaintenanceMarginUsedUsd);
2820
+ if (report.positions.length > 0 && maint > 0n) {
2821
+ const crossValue = dec(account.crossAccountValueUsd);
2822
+ if (crossValue <= 0n)
2823
+ return false;
2824
+ if (fracGreater(maint, crossValue, ratioBps))
2825
+ return false;
2826
+ }
2827
+ return true;
2828
+ }
2829
+ function perpsRiskStep(previous, report) {
2830
+ const rule = report.level;
2831
+ if (rule === "unknown" || previous === "unknown")
2832
+ return rule;
2833
+ if (RANK[rule] >= RANK[previous])
2834
+ return rule;
2835
+ if (rule === "ok" && perpsRiskClears(report, "warn"))
2836
+ return "ok";
2837
+ if (previous === "critical" && perpsRiskClears(report, "critical"))
2838
+ return "warn";
2839
+ return previous;
2840
+ }
2841
+ var PERPS_WATCH_DEFAULT_INTERVAL_MS = 1e4;
2842
+ var PERPS_WATCH_MAX_INTERVAL_MS = 300000;
2843
+ var PERPS_WATCH_DEFAULT_HEARTBEAT_MS = 60000;
2844
+ var PERPS_WATCH_REFRESH_MS = 300000;
2845
+ var HYPERLIQUID_INFO_WEIGHTS = { clearinghouseState: 2, metaAndAssetCtxs: 20, userAbstraction: 20 };
2846
+ function perpsWatchWeightPerMinute(intervalMs) {
2847
+ const polls = 60000 / intervalMs;
2848
+ const modeReads = 60000 / PERPS_WATCH_REFRESH_MS;
2849
+ return Math.round(polls * (HYPERLIQUID_INFO_WEIGHTS.clearinghouseState + HYPERLIQUID_INFO_WEIGHTS.metaAndAssetCtxs) + modeReads * HYPERLIQUID_INFO_WEIGHTS.userAbstraction);
2850
+ }
2851
+ async function watchPerpsRisk(opts) {
2852
+ const baseInterval = opts.intervalMs ?? PERPS_WATCH_DEFAULT_INTERVAL_MS;
2853
+ const heartbeatMs = opts.heartbeatMs ?? PERPS_WATCH_DEFAULT_HEARTBEAT_MS;
2854
+ const identity = { walletId: opts.walletId, address: opts.address, network: opts.network };
2855
+ const sleep2 = (ms) => abortableSleep(opts.sleep, ms, opts.signal);
2856
+ const info2 = { sleep: opts.sleep };
2857
+ let thresholds;
2858
+ try {
2859
+ thresholds = await opts.thresholds();
2860
+ } catch {
2861
+ thresholds = { ...HYPERLIQUID_RISK_DEFAULTS, source: "defaults (server did not report)" };
2862
+ }
2863
+ let thresholdsAt = opts.now();
2864
+ let mode;
2865
+ let modeAt = Number.NEGATIVE_INFINITY;
2866
+ let level = "unknown";
2867
+ let lastGoodAt = null;
2868
+ let staleReported = false;
2869
+ let interval = baseInterval;
2870
+ let lastHeartbeat = opts.now();
2871
+ let established = false;
2872
+ await opts.onEvent({
2873
+ event: "start",
2874
+ ...identity,
2875
+ at: new Date(opts.now()).toISOString(),
2876
+ intervalSeconds: baseInterval / 1000,
2877
+ heartbeatSeconds: heartbeatMs / 1000,
2878
+ thresholds,
2879
+ weightPerMinute: perpsWatchWeightPerMinute(baseInterval)
2880
+ });
2881
+ while (!opts.signal?.aborted) {
2882
+ if (opts.now() - thresholdsAt >= PERPS_WATCH_REFRESH_MS) {
2883
+ thresholdsAt = opts.now();
2884
+ try {
2885
+ thresholds = await opts.thresholds();
2886
+ } catch {}
2887
+ }
2888
+ let report = null;
2889
+ try {
2890
+ if (mode === undefined || opts.now() - modeAt >= PERPS_WATCH_REFRESH_MS) {
2891
+ mode = await readHyperliquidAccountMode(opts.fetcher, opts.network, opts.address, info2);
2892
+ modeAt = opts.now();
2893
+ }
2894
+ report = await readPerpsRisk({ ...identity, fetcher: opts.fetcher, thresholds, now: opts.now, mode, ...info2 });
2895
+ interval = baseInterval;
2896
+ } catch (error) {
2897
+ const rateLimited = error instanceof HyperliquidReadError && error.rateLimited;
2898
+ if (rateLimited)
2899
+ interval = Math.min(interval * 2, PERPS_WATCH_MAX_INTERVAL_MS);
2900
+ await opts.onEvent({
2901
+ event: "error",
2902
+ at: new Date(opts.now()).toISOString(),
2903
+ code: error instanceof HyperliquidReadError ? error.code : "PERPS_READ_FAILED",
2904
+ message: error instanceof Error ? error.message : "Hyperliquid read failed",
2905
+ nextPollSeconds: interval / 1000
2906
+ });
2907
+ if (level !== "unknown" && !staleReported && lastGoodAt !== null && opts.now() - lastGoodAt > PERPS_RISK_STALE_MS) {
2908
+ const previous = level;
2909
+ level = "unknown";
2910
+ staleReported = true;
2911
+ const stale = unknownRiskReport(identity, thresholds, "PERPS_READ_STALE", opts.now(), mode ?? null);
2912
+ await opts.onEvent({ ...stale, event: "level", level, ruleLevel: "unknown", previous });
2913
+ if (opts.onceLevelChange && established)
2914
+ return { level };
2915
+ }
2916
+ }
2917
+ if (report) {
2918
+ const previous = level;
2919
+ const next = perpsRiskStep(previous, report);
2920
+ if (report.level !== "unknown") {
2921
+ lastGoodAt = opts.now();
2922
+ staleReported = false;
2923
+ }
2924
+ if (next !== previous) {
2925
+ level = next;
2926
+ const cleared = next === "ok" && (previous === "warn" || previous === "critical");
2927
+ await opts.onEvent({
2928
+ ...report,
2929
+ event: "level",
2930
+ level: next,
2931
+ ruleLevel: report.level,
2932
+ previous,
2933
+ ...cleared ? { cleared: true } : {}
2934
+ });
2935
+ if (opts.onceLevelChange && established)
2936
+ return { level };
2937
+ if (next !== "unknown")
2938
+ established = true;
2939
+ } else if (next !== "unknown") {
2940
+ established = true;
2941
+ }
2942
+ if (heartbeatMs > 0 && opts.now() - lastHeartbeat >= heartbeatMs) {
2943
+ lastHeartbeat = opts.now();
2944
+ await opts.onEvent({ ...report, event: "heartbeat", level, ruleLevel: report.level });
2945
+ }
2946
+ }
2947
+ if (opts.signal?.aborted)
2948
+ break;
2949
+ await sleep2(interval);
2950
+ }
2951
+ return { level };
2952
+ }
2953
+ function abortableSleep(sleep2, ms, signal) {
2954
+ if (!signal)
2955
+ return sleep2(ms);
2956
+ if (signal.aborted)
2957
+ return Promise.resolve();
2958
+ return new Promise((resolve2) => {
2959
+ const done = () => {
2960
+ signal.removeEventListener("abort", done);
2961
+ resolve2();
2962
+ };
2963
+ signal.addEventListener("abort", done);
2964
+ sleep2(ms).then(done, done);
2965
+ });
2966
+ }
2967
+ var PERPS_PORTFOLIO_MAX_ACCOUNTS = 100;
2968
+ var PORTFOLIO_READS_IN_FLIGHT = 8;
2969
+ async function perpsPortfolioSection(fetcher, network, wallets, opts = {}) {
2970
+ const now = opts.now ?? Date.now;
2971
+ const considered = wallets.slice(0, PERPS_PORTFOLIO_MAX_ACCOUNTS);
2972
+ const unread = wallets.slice(PERPS_PORTFOLIO_MAX_ACCOUNTS).map((w) => ({ ...w, reason: `past the ${PERPS_PORTFOLIO_MAX_ACCOUNTS}-address cap` }));
2973
+ let markets = { universe: [], ctxs: [], marginTables: new Map };
2974
+ if (considered.length > 0) {
2975
+ try {
2976
+ markets = await readHyperliquidMarketSnapshot(fetcher, network, opts);
2977
+ } catch (error) {
2978
+ return { read: false, reason: error instanceof Error ? error.message : "Hyperliquid market read failed" };
2979
+ }
2980
+ }
2981
+ const thresholds = { ...HYPERLIQUID_RISK_DEFAULTS, source: "defaults" };
2982
+ const results = new Array(considered.length).fill(null);
2983
+ const failures = new Array(considered.length).fill(null);
2984
+ let next = 0;
2985
+ const worker = async () => {
2986
+ while (next < considered.length) {
2987
+ const at2 = next++;
2988
+ const wallet = considered[at2];
2989
+ try {
2990
+ const state = await readHyperliquidClearinghouse(fetcher, network, wallet.address, opts);
2991
+ const report = hyperliquidRiskReport(state, markets, "default", thresholds, now());
2992
+ results[at2] = {
2993
+ address: wallet.address,
2994
+ ...wallet.label !== undefined ? { label: wallet.label } : {},
2995
+ ...wallet.walletId !== undefined ? { walletId: wallet.walletId } : {},
2996
+ accountValueUsd: report.account?.accountValueUsd ?? "0",
2997
+ unrealizedPnlUsd: report.account?.unrealizedPnlUsd ?? "0",
2998
+ positionCount: report.positions.length,
2999
+ level: report.level
3000
+ };
3001
+ } catch (error) {
3002
+ failures[at2] = error instanceof Error ? error.message : "read failed";
3003
+ }
3004
+ }
3005
+ };
3006
+ await Promise.all(Array.from({ length: Math.min(PORTFOLIO_READS_IN_FLIGHT, considered.length) }, worker));
3007
+ const accounts = [];
3008
+ let empty = 0;
3009
+ let value = 0n;
3010
+ let pnl = 0n;
3011
+ considered.forEach((wallet, at2) => {
3012
+ const result = results[at2];
3013
+ if (!result) {
3014
+ unread.push({ ...wallet, reason: failures[at2] ?? "read failed" });
3015
+ return;
3016
+ }
3017
+ if (result.positionCount === 0 && dec(result.accountValueUsd) === 0n) {
3018
+ empty++;
3019
+ return;
3020
+ }
3021
+ accounts.push(result);
3022
+ value += dec(result.accountValueUsd);
3023
+ pnl += dec(result.unrealizedPnlUsd);
3024
+ });
3025
+ return {
3026
+ read: true,
3027
+ network,
3028
+ accounts,
3029
+ subtotal: { accountValueUsd: formatDec(value), unrealizedPnlUsd: formatDec(pnl) },
3030
+ empty,
3031
+ unread,
3032
+ thresholds: "defaults"
3033
+ };
3034
+ }
3035
+ var SCALE = 18;
3036
+ var ONE2 = 10n ** BigInt(SCALE);
3037
+ var DEC_RE = /^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/;
3038
+ var LOOSE_DEC_RE = /^-?[0-9]+(\.[0-9]{1,18})?$/;
3039
+ function isDecString(value) {
3040
+ return typeof value === "string" && (DEC_RE.test(value) || LOOSE_DEC_RE.test(value));
3041
+ }
3042
+ function dec(value) {
3043
+ if (!isDecString(value))
3044
+ return null;
3045
+ const negative = value.startsWith("-");
3046
+ const [whole = "0", fraction = ""] = value.replace(/^-/, "").split(".");
3047
+ const scaled2 = BigInt(whole) * ONE2 + BigInt(fraction.padEnd(SCALE, "0"));
3048
+ return negative ? -scaled2 : scaled2;
912
3049
  }
913
- function hyperliquidExchangeBody(action, nonce, signature) {
914
- return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
3050
+ function formatDec(value) {
3051
+ const negative = value < 0n;
3052
+ const abs = negative ? -value : value;
3053
+ const whole = abs / ONE2;
3054
+ const fraction = (abs % ONE2).toString().padStart(SCALE, "0").replace(/0+$/, "");
3055
+ const text = fraction ? `${whole}.${fraction}` : `${whole}`;
3056
+ return negative && text !== "0" ? `-${text}` : text;
915
3057
  }
916
- function hyperliquidRelayBody(typedData) {
917
- return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
3058
+ function distanceParts(markPx, liquidationPx) {
3059
+ const mark = dec(markPx);
3060
+ const liq = dec(liquidationPx);
3061
+ if (mark === null || liq === null || mark <= 0n)
3062
+ return null;
3063
+ const diff = mark - liq;
3064
+ return { num: diff < 0n ? -diff : diff, den: mark };
3065
+ }
3066
+ function fracLess(num, den, bps) {
3067
+ return num * 10000n < BigInt(bps) * den;
3068
+ }
3069
+ function fracGreater(num, den, bps) {
3070
+ return num * 10000n > BigInt(bps) * den;
3071
+ }
3072
+ function ratioNumber(num, den) {
3073
+ const micro = num * 1000000000000n / den;
3074
+ return Number(micro) / 1000000000000;
3075
+ }
3076
+ function isRecord(value) {
3077
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3078
+ }
3079
+ function isParsedClearinghouse(value) {
3080
+ return isRecord(value) && Array.isArray(value.positions) && !("assetPositions" in value);
3081
+ }
3082
+
3083
+ // src/hyperliquid-withdraw.ts
3084
+ var CANDLE_PERPS_WITHDRAW_PINS = {
3085
+ token: "USDC:0x6d1e7cde53ba9467b783cb7c530ce054",
3086
+ destinationDex: "",
3087
+ depositories: ["0x66cf0aace1b4e562593bec10ec7868fba9932224"],
3088
+ amountForm: "fixed6"
3089
+ };
3090
+ var ZERO = "0x0000000000000000000000000000000000000000";
3091
+ var EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
3092
+ var BYTES32_RE = /^0x[0-9a-fA-F]{64}$/;
3093
+ var RELAY_NONCE_MAPPING_TYPES = {
3094
+ NonceMapping: [
3095
+ { name: "chainId", type: "string" },
3096
+ { name: "wallet", type: "address" },
3097
+ { name: "depositor", type: "address" },
3098
+ { name: "id", type: "bytes32" },
3099
+ { name: "nonce", type: "uint256" }
3100
+ ]
3101
+ };
3102
+ var HYPERLIQUID_SEND_ASSET_TYPES = {
3103
+ "HyperliquidTransaction:SendAsset": [
3104
+ { name: "hyperliquidChain", type: "string" },
3105
+ { name: "destination", type: "string" },
3106
+ { name: "sourceDex", type: "string" },
3107
+ { name: "destinationDex", type: "string" },
3108
+ { name: "token", type: "string" },
3109
+ { name: "amount", type: "string" },
3110
+ { name: "fromSubAccount", type: "string" },
3111
+ { name: "nonce", type: "uint64" }
3112
+ ]
3113
+ };
3114
+ var SEND_ASSET_KEYS = HYPERLIQUID_SEND_ASSET_TYPES["HyperliquidTransaction:SendAsset"].map((field) => field.name);
3115
+ var REQUEST_AMOUNT_RE = /^(0|[1-9][0-9]{0,11})(\.[0-9]{1,6})?$/;
3116
+ var CANONICAL_RE = /^(0|[1-9][0-9]*)(\.[0-9]{0,5}[1-9])?$/;
3117
+ var FIXED6_RE = /^(0|[1-9][0-9]*)\.[0-9]{6}$/;
3118
+ function parseWithdrawAmount(value) {
3119
+ if (!REQUEST_AMOUNT_RE.test(value))
3120
+ return null;
3121
+ const [whole = "0", frac = ""] = value.split(".");
3122
+ const micros = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
3123
+ return micros > 0n ? micros : null;
3124
+ }
3125
+ function parseSendAssetAmount(value, form) {
3126
+ if (typeof value !== "string" || !(form === "canonical" ? CANONICAL_RE : FIXED6_RE).test(value))
3127
+ return null;
3128
+ const [whole = "0", frac = ""] = value.split(".");
3129
+ if (frac.length > 6)
3130
+ return null;
3131
+ return BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
3132
+ }
3133
+ function isObject(value) {
3134
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3135
+ }
3136
+ function exactKeys2(value, keys) {
3137
+ const own = Object.keys(value);
3138
+ return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
3139
+ }
3140
+ function sameEvm(a, b) {
3141
+ return typeof a === "string" && EVM_ADDRESS_RE2.test(a) && a.toLowerCase() === b.toLowerCase();
3142
+ }
3143
+ function typesMatch(types, expected) {
3144
+ if (!isObject(types) || !exactKeys2(types, Object.keys(expected)))
3145
+ return false;
3146
+ return Object.entries(expected).every(([key, want]) => {
3147
+ const fields = types[key];
3148
+ return Array.isArray(fields) && fields.length === want.length && fields.every((field, i) => isObject(field) && exactKeys2(field, ["name", "type"]) && field.name === want[i]?.name && field.type === want[i]?.type);
3149
+ });
3150
+ }
3151
+ function domainMatches(domain, expected) {
3152
+ return isObject(domain) && exactKeys2(domain, ["name", "version", "chainId", "verifyingContract"]) && domain.name === expected.name && domain.version === expected.version && domain.chainId === expected.chainId && typeof domain.verifyingContract === "string" && domain.verifyingContract.toLowerCase() === ZERO;
3153
+ }
3154
+ function verifyWithdrawMapping(build, expect) {
3155
+ const refuse = (reason) => ({ ok: false, reason });
3156
+ if (expect.pins === null)
3157
+ return refuse("no withdrawal pins are set");
3158
+ if (build.id !== expect.id)
3159
+ return refuse("the build names another withdrawal id");
3160
+ if (!sameEvm(build.from?.address, expect.account))
3161
+ return refuse("the build withdraws from another account");
3162
+ if (build.to?.chain !== expect.to)
3163
+ return refuse(`the build lands on ${String(build.to?.chain)}, not ${expect.to}`);
3164
+ const destination = build.to.address;
3165
+ const own = expect.ownDestinations.some((address) => address.startsWith("0x") ? sameEvm(destination, address) : destination === address);
3166
+ if (!own)
3167
+ return refuse("the build lands on a wallet that is not this key's own");
3168
+ if (parseWithdrawAmount(build.amountUsdc) !== expect.amountMicros)
3169
+ return refuse("the build is for another amount");
3170
+ const typed = build.typedData;
3171
+ if (!isObject(typed) || typed.primary_type !== "NonceMapping")
3172
+ return refuse("the typed data is not a NonceMapping");
3173
+ if (!domainMatches(typed.domain, { name: "RelayNonceMapping", version: "2", chainId: 1 }))
3174
+ return refuse("the NonceMapping domain is not Relay's");
3175
+ if (!typesMatch(typed.types, RELAY_NONCE_MAPPING_TYPES))
3176
+ return refuse("the NonceMapping type list differs");
3177
+ const message = typed.message;
3178
+ if (!isObject(message) || !exactKeys2(message, ["chainId", "wallet", "depositor", "id", "nonce"]))
3179
+ return refuse("the NonceMapping message has other fields");
3180
+ if (message.chainId !== "hyperliquid")
3181
+ return refuse("the NonceMapping names another chain");
3182
+ if (!sameEvm(message.wallet, expect.account))
3183
+ return refuse("the NonceMapping wallet is not this key's account");
3184
+ if (!sameEvm(message.depositor, expect.account))
3185
+ return refuse("the NonceMapping depositor is not this key's account");
3186
+ if (typeof build.relayOrderId !== "string" || !BYTES32_RE.test(build.relayOrderId))
3187
+ return refuse("the build has no Relay order id");
3188
+ if (typeof message.id !== "string" || !BYTES32_RE.test(message.id) || message.id !== build.relayOrderId)
3189
+ return refuse("the NonceMapping names another Relay order");
3190
+ if (message.nonce !== build.nonce || !Number.isSafeInteger(build.nonce) || build.nonce <= 0)
3191
+ return refuse("the NonceMapping nonce is not the build's");
3192
+ return { ok: true };
3193
+ }
3194
+ function verifyWithdrawSendAsset(authorization, expect) {
3195
+ const refuse = (reason) => ({ ok: false, reason });
3196
+ const pins = expect.pins;
3197
+ if (pins === null)
3198
+ return refuse("no withdrawal pins are set");
3199
+ const typed = authorization.typedData;
3200
+ if (!isObject(typed) || typed.primary_type !== "HyperliquidTransaction:SendAsset")
3201
+ return refuse("the typed data is not a SendAsset");
3202
+ if (!domainMatches(typed.domain, { name: "HyperliquidSignTransaction", version: "1", chainId: 42161 }))
3203
+ return refuse("the SendAsset domain is not Hyperliquid's user-signed domain");
3204
+ if (!typesMatch(typed.types, HYPERLIQUID_SEND_ASSET_TYPES))
3205
+ return refuse("the SendAsset type list differs");
3206
+ const m = typed.message;
3207
+ if (!isObject(m) || !exactKeys2(m, SEND_ASSET_KEYS))
3208
+ return refuse("the SendAsset message has other fields");
3209
+ if (m.hyperliquidChain !== "Mainnet")
3210
+ return refuse("the SendAsset is not on Mainnet");
3211
+ if (m.nonce !== expect.mappingNonce || authorization.nonce !== expect.mappingNonce)
3212
+ return refuse("the SendAsset nonce is not the NonceMapping's");
3213
+ if (parseSendAssetAmount(m.amount, pins.amountForm) !== expect.amountMicros)
3214
+ return refuse("the SendAsset amount is not the amount asked for");
3215
+ if (m.fromSubAccount !== "")
3216
+ return refuse("the SendAsset moves from a sub-account");
3217
+ if (m.sourceDex !== "")
3218
+ return refuse("the SendAsset moves from another dex");
3219
+ if (m.token !== pins.token)
3220
+ return refuse("the SendAsset moves another token");
3221
+ if (m.destinationDex !== pins.destinationDex)
3222
+ return refuse("the SendAsset names another destination dex");
3223
+ if (typeof m.destination !== "string" || !pins.depositories.includes(m.destination.toLowerCase()))
3224
+ return refuse("the SendAsset destination is not a Relay depository on the allow-list");
3225
+ const action = authorization.action;
3226
+ if (!isObject(action) || !exactKeys2(action, ["type", "signatureChainId", ...SEND_ASSET_KEYS]))
3227
+ return refuse("the action has other fields");
3228
+ if (action.type !== "sendAsset" || action.signatureChainId !== "0xa4b1")
3229
+ return refuse("the action is not a sendAsset");
3230
+ for (const key of SEND_ASSET_KEYS) {
3231
+ if (action[key] !== m[key])
3232
+ return refuse(`the action's ${key} differs from what is signed`);
3233
+ }
3234
+ return { ok: true };
918
3235
  }
919
3236
 
920
3237
  // src/internal/rpc-endpoint.ts
@@ -926,6 +3243,288 @@ function describeRpcEndpoint(url) {
926
3243
  }
927
3244
  }
928
3245
 
3246
+ // src/perps-history.ts
3247
+ var PERPS_HISTORY_INFO_URLS = {
3248
+ mainnet: "https://api.hyperliquid.xyz/info",
3249
+ testnet: "https://api.hyperliquid-testnet.xyz/info"
3250
+ };
3251
+ var PERPS_HISTORY_DEFAULT_LIMIT = 100;
3252
+ var PERPS_HISTORY_MAX_LIMIT = { fills: 2000, funding: 500 };
3253
+ var PERPS_HISTORY_DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
3254
+ var PERPS_HISTORY_REQUEST_BUDGET = 10;
3255
+ var PERPS_HISTORY_FILL_RETENTION = 1e4;
3256
+ var PERPS_HISTORY_MAX_CURSOR_LENGTH = 200000;
3257
+ var PERPS_HISTORY_TIMEOUT_MS = 15000;
3258
+
3259
+ class PerpsHistoryError extends Error {
3260
+ code;
3261
+ extra;
3262
+ constructor(code, message, extra = {}) {
3263
+ super(message);
3264
+ this.code = code;
3265
+ this.extra = extra;
3266
+ this.name = "PerpsHistoryError";
3267
+ }
3268
+ }
3269
+ var RETENTION_TEXT = "Hyperliquid serves only the 10,000 most recent fills for an account; older fills cannot be read here, and this history is not an all-time total.";
3270
+ var EVM_ADDRESS_RE3 = /^0x[0-9a-f]{40}$/;
3271
+ function isTime(value) {
3272
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3273
+ }
3274
+ function invalid(field, message) {
3275
+ return new PerpsHistoryError("VALIDATION_FAILED", message, { field });
3276
+ }
3277
+ function unavailable2(message, retryAfterMs) {
3278
+ return new PerpsHistoryError("PERPS_UNAVAILABLE", message, {
3279
+ retryable: true,
3280
+ ...retryAfterMs !== undefined ? { retryAfterMs } : {}
3281
+ });
3282
+ }
3283
+ function base64UrlEncode(text) {
3284
+ const bytes = new TextEncoder().encode(text);
3285
+ let binary = "";
3286
+ for (const b of bytes)
3287
+ binary += String.fromCharCode(b);
3288
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3289
+ }
3290
+ function base64UrlDecode(text) {
3291
+ if (!/^[A-Za-z0-9_-]*$/.test(text))
3292
+ throw new Error("not base64url");
3293
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text.length % 4) % 4);
3294
+ const binary = atob(padded);
3295
+ const bytes = new Uint8Array(binary.length);
3296
+ for (let i = 0;i < binary.length; i++)
3297
+ bytes[i] = binary.charCodeAt(i);
3298
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3299
+ }
3300
+ function encodeCursor(state) {
3301
+ return `ph1.${base64UrlEncode(JSON.stringify(state))}`;
3302
+ }
3303
+ function decodeCursor(cursor) {
3304
+ if (cursor.length > PERPS_HISTORY_MAX_CURSOR_LENGTH || !cursor.startsWith("ph1."))
3305
+ throw invalid("cursor", "cursor is not one this client issued");
3306
+ let state;
3307
+ try {
3308
+ state = JSON.parse(base64UrlDecode(cursor.slice(4)));
3309
+ } catch {
3310
+ throw invalid("cursor", "cursor is not one this client issued");
3311
+ }
3312
+ const ok = state !== null && typeof state === "object" && state.v === 1 && (state.k === "fills" || state.k === "funding") && (state.n === "mainnet" || state.n === "testnet") && typeof state.w === "string" && EVM_ADDRESS_RE3.test(state.w) && isTime(state.s) && isTime(state.e) && state.s <= state.e && Number.isSafeInteger(state.l) && state.l >= 1 && state.l <= PERPS_HISTORY_MAX_LIMIT[state.k] && isTime(state.t) && state.t >= state.s && state.t <= state.e && Array.isArray(state.b) && state.b.every((id) => typeof id === "string" && id.length <= 512) && Number.isSafeInteger(state.c) && state.c >= 0;
3313
+ if (!ok)
3314
+ throw invalid("cursor", "cursor is not one this client issued");
3315
+ return state;
3316
+ }
3317
+ function canonical2(value) {
3318
+ if (value === null || typeof value !== "object")
3319
+ return JSON.stringify(value);
3320
+ if (Array.isArray(value))
3321
+ return `[${value.map(canonical2).join(",")}]`;
3322
+ const record = value;
3323
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical2(record[key])}`).join(",")}}`;
3324
+ }
3325
+ function isDecimalString(value) {
3326
+ return typeof value === "string" && /^-?[0-9]+(\.[0-9]+)?$/.test(value);
3327
+ }
3328
+ function asFill(raw) {
3329
+ const row = raw;
3330
+ if (!row || typeof row !== "object" || typeof row.coin !== "string" || !isTime(row.time) || !Number.isSafeInteger(row.tid) || !Number.isSafeInteger(row.oid) || !isDecimalString(row.px) || !isDecimalString(row.sz) || typeof row.side !== "string" || !isDecimalString(row.closedPnl) || !isDecimalString(row.fee) || row.builderFee !== undefined && !isDecimalString(row.builderFee))
3331
+ throw unavailable2("Hyperliquid returned a fill this client cannot read");
3332
+ return row;
3333
+ }
3334
+ function asFunding(raw) {
3335
+ const row = raw;
3336
+ const delta = row?.delta;
3337
+ if (!row || typeof row !== "object" || !isTime(row.time) || typeof row.hash !== "string" || !delta || typeof delta !== "object" || typeof delta.coin !== "string" || !isDecimalString(delta.usdc) || !isDecimalString(delta.fundingRate) || !isDecimalString(delta.szi))
3338
+ throw unavailable2("Hyperliquid returned a funding payment this client cannot read");
3339
+ return row;
3340
+ }
3341
+ function identity(kind, row) {
3342
+ if (kind === "fills") {
3343
+ const fill = row;
3344
+ return `${fill.coin}|${fill.tid}|${fill.oid}`;
3345
+ }
3346
+ const funding = row;
3347
+ return `${funding.hash}|${funding.time}|${funding.delta.coin}|${canonical2(funding.delta)}`;
3348
+ }
3349
+ function rowCoin(kind, row) {
3350
+ return kind === "fills" ? row.coin : row.delta.coin;
3351
+ }
3352
+ async function postInfo(req, body, what) {
3353
+ const controller = typeof AbortController === "function" ? new AbortController : null;
3354
+ const timer = controller ? setTimeout(() => controller.abort(), PERPS_HISTORY_TIMEOUT_MS) : null;
3355
+ let res;
3356
+ try {
3357
+ res = await req.fetch(PERPS_HISTORY_INFO_URLS[req.network], {
3358
+ method: "POST",
3359
+ headers: { "content-type": "application/json" },
3360
+ body: JSON.stringify(body),
3361
+ ...controller ? { signal: controller.signal } : {}
3362
+ });
3363
+ } catch (err) {
3364
+ const aborted = err instanceof Error && err.name === "AbortError";
3365
+ throw unavailable2(aborted ? `Hyperliquid did not answer the ${what} read in time; retry shortly` : `Hyperliquid could not be reached for the ${what} read; retry shortly`);
3366
+ } finally {
3367
+ if (timer)
3368
+ clearTimeout(timer);
3369
+ }
3370
+ if (!res.ok) {
3371
+ if (res.status === 429) {
3372
+ const header = res.headers?.get("retry-after") ?? null;
3373
+ const seconds = header !== null && /^[0-9]{1,6}$/.test(header.trim()) ? Number(header.trim()) : undefined;
3374
+ throw unavailable2(`Hyperliquid rate-limited the ${what} read${seconds !== undefined ? `; retry after ${seconds}s` : "; retry shortly"}`, seconds !== undefined ? seconds * 1000 : undefined);
3375
+ }
3376
+ throw unavailable2(`Hyperliquid answered the ${what} read with HTTP ${res.status}; retry shortly`);
3377
+ }
3378
+ try {
3379
+ return JSON.parse(await res.text());
3380
+ } catch {
3381
+ throw unavailable2(`Hyperliquid returned an unreadable ${what} response`);
3382
+ }
3383
+ }
3384
+ async function mainPerpCoins(req) {
3385
+ let universe;
3386
+ try {
3387
+ universe = (await readHyperliquidMarkets(req.fetch, req.network)).universe;
3388
+ } catch {
3389
+ throw unavailable2("Hyperliquid's market list could not be read; history is not reported without it");
3390
+ }
3391
+ if (universe.length === 0)
3392
+ throw unavailable2("Hyperliquid's market list could not be read; history is not reported without it");
3393
+ return new Set(universe.map((market) => market.name));
3394
+ }
3395
+ function resolveWindow(req) {
3396
+ const options = req.options ?? {};
3397
+ const kind = req.kind;
3398
+ const address = req.address.toLowerCase();
3399
+ if (!EVM_ADDRESS_RE3.test(address))
3400
+ throw invalid("wallet", "The wallet's Hyperliquid account is not an EVM address");
3401
+ for (const field of ["startTime", "endTime"]) {
3402
+ const value = options[field];
3403
+ if (value !== undefined && !isTime(value))
3404
+ throw invalid(field, `${field} must be a whole, non-negative number of epoch milliseconds`);
3405
+ }
3406
+ const max = PERPS_HISTORY_MAX_LIMIT[kind];
3407
+ if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > max))
3408
+ throw invalid("limit", `limit must be a whole number from 1 to ${max} for ${kind}`);
3409
+ if (options.cursor !== undefined) {
3410
+ if (typeof options.cursor !== "string" || options.cursor.length === 0)
3411
+ throw invalid("cursor", "cursor is not one this client issued");
3412
+ const state = decodeCursor(options.cursor);
3413
+ if (state.k !== kind)
3414
+ throw invalid("cursor", `This cursor pages ${state.k}, not ${kind}`);
3415
+ if (state.n !== req.network)
3416
+ throw invalid("cursor", `This cursor pages ${state.n}, not ${req.network}`);
3417
+ if (state.w !== address)
3418
+ throw invalid("cursor", "This cursor pages another wallet");
3419
+ if (options.startTime !== undefined && options.startTime !== state.s || options.endTime !== undefined && options.endTime !== state.e || options.limit !== undefined && options.limit !== state.l)
3420
+ throw invalid("cursor", "The window or page size changed; start again without a cursor");
3421
+ return { state };
3422
+ }
3423
+ const endTime = options.endTime ?? req.now();
3424
+ const startTime = options.startTime ?? Math.max(0, endTime - PERPS_HISTORY_DEFAULT_WINDOW_MS);
3425
+ if (!isTime(endTime))
3426
+ throw invalid("endTime", "endTime must be epoch milliseconds");
3427
+ if (startTime > endTime)
3428
+ throw invalid("startTime", "startTime must not be after endTime");
3429
+ return {
3430
+ state: {
3431
+ v: 1,
3432
+ k: kind,
3433
+ n: req.network,
3434
+ w: address,
3435
+ s: startTime,
3436
+ e: endTime,
3437
+ l: options.limit ?? PERPS_HISTORY_DEFAULT_LIMIT,
3438
+ t: startTime,
3439
+ b: [],
3440
+ c: 0
3441
+ }
3442
+ };
3443
+ }
3444
+ async function readPerpsHistoryPage(req) {
3445
+ const { state } = resolveWindow(req);
3446
+ const kind = req.kind;
3447
+ const max = PERPS_HISTORY_MAX_LIMIT[kind];
3448
+ const readAt = req.now();
3449
+ const coins = await mainPerpCoins(req);
3450
+ const page = [];
3451
+ const warnings = [];
3452
+ let t = state.t;
3453
+ let boundary = new Set(state.b);
3454
+ let consumed = state.c;
3455
+ let done = false;
3456
+ let truncated = false;
3457
+ let requests = 0;
3458
+ const stop = (code, message, ends) => {
3459
+ warnings.push({ code, message });
3460
+ if (ends) {
3461
+ done = true;
3462
+ truncated = true;
3463
+ }
3464
+ };
3465
+ walk:
3466
+ while (page.length < state.l && !done) {
3467
+ if (requests >= PERPS_HISTORY_REQUEST_BUDGET) {
3468
+ stop("PERPS_HISTORY_PAGE_BUDGET", `Stopped after ${PERPS_HISTORY_REQUEST_BUDGET} Hyperliquid reads; pass nextCursor to continue`, false);
3469
+ break;
3470
+ }
3471
+ const body = kind === "fills" ? { type: "userFillsByTime", user: state.w, startTime: t, endTime: state.e, aggregateByTime: false } : { type: "userFunding", user: state.w, startTime: t, endTime: state.e };
3472
+ const raw = await postInfo(req, body, kind === "fills" ? "fills" : "funding");
3473
+ requests++;
3474
+ if (!Array.isArray(raw))
3475
+ throw unavailable2(`Hyperliquid returned an unreadable ${kind} response`);
3476
+ const rows = raw.map((r) => kind === "fills" ? asFill(r) : asFunding(r));
3477
+ const keyed = rows.map((row) => ({ row, id: identity(kind, row) })).sort((a, b) => a.row.time - b.row.time || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
3478
+ const full = rows.length >= max;
3479
+ const startedAt = t;
3480
+ let previousId = null;
3481
+ for (const { row, id } of keyed) {
3482
+ if (id === previousId)
3483
+ continue;
3484
+ previousId = id;
3485
+ if (row.time < t || row.time > state.e || row.time === t && boundary.has(id))
3486
+ continue;
3487
+ if (page.length >= state.l)
3488
+ break walk;
3489
+ if (row.time > t) {
3490
+ t = row.time;
3491
+ boundary = new Set;
3492
+ }
3493
+ boundary.add(id);
3494
+ consumed++;
3495
+ if (coins.has(rowCoin(kind, row)))
3496
+ page.push(row);
3497
+ if (kind === "fills" && consumed >= PERPS_HISTORY_FILL_RETENTION) {
3498
+ stop("PERPS_HISTORY_RETENTION_LIMIT", "Reached Hyperliquid's 10,000-fill retention across this cursor chain; older fills are not available", true);
3499
+ break walk;
3500
+ }
3501
+ }
3502
+ if (!full) {
3503
+ done = true;
3504
+ } else if (t === startedAt) {
3505
+ stop("PERPS_HISTORY_BOUNDARY_LIMIT", `More than ${max} ${kind === "fills" ? "fills" : "funding rows"} share one millisecond (${t}); coverage stops there`, true);
3506
+ }
3507
+ }
3508
+ if (kind === "fills" && !warnings.some((w) => w.code === "PERPS_HISTORY_RETENTION_LIMIT"))
3509
+ warnings.unshift({ code: "PERPS_HISTORY_RETENTION_LIMIT", message: RETENTION_TEXT });
3510
+ const nextCursor = done ? null : encodeCursor({ ...state, t, b: [...boundary], c: consumed });
3511
+ const envelope = {
3512
+ success: true,
3513
+ walletId: req.walletId,
3514
+ address: state.w,
3515
+ network: state.n,
3516
+ source: "hyperliquid",
3517
+ readAt,
3518
+ startTime: state.s,
3519
+ endTime: state.e,
3520
+ nextCursor,
3521
+ hasMore: nextCursor !== null,
3522
+ truncated,
3523
+ warnings
3524
+ };
3525
+ return kind === "fills" ? { ...envelope, fills: page } : { ...envelope, funding: page };
3526
+ }
3527
+
929
3528
  // src/wallet-import.ts
930
3529
  import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
931
3530
  import { CipherSuite, DhkemP256HkdfSha256, HkdfSha256 } from "@hpke/core";
@@ -1012,6 +3611,32 @@ function historyTime(value) {
1012
3611
  }
1013
3612
  var HYPERLIQUID_RELAY_CHAIN_ID = 1337;
1014
3613
  var HYPERLIQUID_RELAY_USDC = "0x00000000000000000000000000000000";
3614
+ function perpsClientId(clientId) {
3615
+ if (clientId === undefined)
3616
+ return;
3617
+ const lower = typeof clientId === "string" ? clientId.toLowerCase() : "";
3618
+ if (!HYPERLIQUID_CLOID_RE.test(lower))
3619
+ throw new Error("perps: clientId must be 0x followed by 32 hex digits");
3620
+ return lower;
3621
+ }
3622
+ function perpsReplayOf(build, walletId, clientId) {
3623
+ const answer = build;
3624
+ if (answer.replayed === true) {
3625
+ if (clientId === undefined || answer.clientId !== clientId || answer.walletId !== walletId)
3626
+ throw new Error("perps: refused a replay that does not name this request's client id and wallet");
3627
+ return {
3628
+ replayed: true,
3629
+ replay: build,
3630
+ build: null,
3631
+ signature: null,
3632
+ submitted: false,
3633
+ exchange: null
3634
+ };
3635
+ }
3636
+ if (clientId !== undefined && (answer.clientId !== clientId || answer.cloid !== clientId))
3637
+ throw new Error("perps: PERPS_CLIENT_ID_UNSUPPORTED: this Candle API did not echo the client order id, so its order could not be deduplicated. Nothing was signed.");
3638
+ return null;
3639
+ }
1015
3640
  function perpsDepositProblem(build, params) {
1016
3641
  const same = (a, b) => typeof a === "string" && a.toLowerCase() === b.toLowerCase();
1017
3642
  if (build.walletId !== params.walletId)
@@ -1053,7 +3678,7 @@ function isRetryableLaunchFailure(error) {
1053
3678
  return error.status >= 500 || error.status === 409;
1054
3679
  }
1055
3680
  function sleep2(ms) {
1056
- return new Promise((resolve) => setTimeout(resolve, ms));
3681
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1057
3682
  }
1058
3683
  var MAX_BLOCKHASH_REBUILDS = 2;
1059
3684
  function isBlockhashExpiry(error) {
@@ -1203,7 +3828,29 @@ class CandleClient {
1203
3828
  solanaRpcUrl;
1204
3829
  evmRpcUrl;
1205
3830
  hyperliquidNetwork;
3831
+ perpsMarketReader;
1206
3832
  hyperliquidBuilder;
3833
+ perpsWithdrawPins;
3834
+ perps = {
3835
+ withdraw: {
3836
+ build: async (request) => {
3837
+ this.requireKey("perps.withdraw.build()");
3838
+ return this.requestJson("POST", "/api/v1/agent/perps/withdraw", request);
3839
+ },
3840
+ authorize: async (request) => {
3841
+ this.requireKey("perps.withdraw.authorize()");
3842
+ return this.requestJson("POST", "/api/v1/agent/perps/withdraw/authorize", request);
3843
+ },
3844
+ reportSubmitted: async (request) => {
3845
+ this.requireKey("perps.withdraw.reportSubmitted()");
3846
+ return this.requestJson("POST", "/api/v1/agent/perps/withdraw/submitted", request);
3847
+ },
3848
+ get: async (clientWithdrawalId) => {
3849
+ this.requireKey("perps.withdraw.get()");
3850
+ return this.requestJson("GET", `/api/v1/agent/perps/withdraw/${encodeURIComponent(clientWithdrawalId)}`);
3851
+ }
3852
+ }
3853
+ };
1207
3854
  wallets = {
1208
3855
  swapReceipt: async (hash) => {
1209
3856
  this.requireKey("wallets.swapReceipt()");
@@ -1277,9 +3924,9 @@ class CandleClient {
1277
3924
  if (opts.evmRpcUrl !== undefined)
1278
3925
  this.evmRpcUrl = opts.evmRpcUrl;
1279
3926
  this.hyperliquidNetwork = opts.hyperliquidNetwork ?? "mainnet";
1280
- const builder = opts.hyperliquidBuilder ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
1281
- if (builder)
1282
- this.hyperliquidBuilder = builder.toLowerCase();
3927
+ this.perpsWithdrawPins = opts.perpsWithdrawPins ?? CANDLE_PERPS_WITHDRAW_PINS;
3928
+ if (opts.hyperliquidBuilder !== undefined)
3929
+ this.hyperliquidBuilder = opts.hyperliquidBuilder;
1283
3930
  }
1284
3931
  async getQuotePairs(chain) {
1285
3932
  const query = chain ? `?chain=${chain}` : "";
@@ -1503,15 +4150,59 @@ class CandleClient {
1503
4150
  }
1504
4151
  async previewCloseEmptyAccounts(req = {}) {
1505
4152
  this.requireKey("previewCloseEmptyAccounts()");
1506
- return this.requestJson("POST", "/api/v1/agent/wallets/embedded/close-empty/preview", req);
4153
+ const { wallet, ...body } = req;
4154
+ return this.requestJson("POST", wallet === undefined ? "/api/v1/agent/wallets/embedded/close-empty/preview" : `/api/v1/agent/wallets/${encodeURIComponent(wallet)}/close-empty/preview`, body);
1507
4155
  }
1508
4156
  async closeEmptyAccounts(req) {
1509
4157
  this.requireKey("closeEmptyAccounts()");
1510
- return this.requestJson("POST", "/api/v1/agent/wallets/embedded/close-empty", req);
4158
+ const { wallet, privyWalletId, ...body } = req;
4159
+ if (wallet === undefined)
4160
+ return this.requestJson("POST", "/api/v1/agent/wallets/embedded/close-empty", body);
4161
+ if (!privyWalletId)
4162
+ throw new Error("closeEmptyAccounts() with a TEE wallet requires privyWalletId: the same wallet's Privy wallet id, which signLinkedTransaction() signs for");
4163
+ const base = `/api/v1/agent/wallets/${encodeURIComponent(wallet)}/close-empty`;
4164
+ const build = await this.requestJson("POST", `${base}/build`, body);
4165
+ if (build.status === "nothing_to_close")
4166
+ return {
4167
+ closed: [],
4168
+ signatures: [],
4169
+ lamportsRecovered: "0",
4170
+ solRecovered: "0",
4171
+ feeLamports: "0",
4172
+ netLamports: "0",
4173
+ netSol: "0",
4174
+ failed: [],
4175
+ unattempted: [],
4176
+ notClosed: [],
4177
+ skipped: [],
4178
+ remaining: 0,
4179
+ ...build
4180
+ };
4181
+ if (build.submitState !== undefined) {
4182
+ const job = await this.getCloseEmptyAccountsJob(req.clientTradeId, { wallet });
4183
+ const status = job.httpStatus ?? 200;
4184
+ if (status >= 400)
4185
+ throw candleApiErrorFromResponse(status, JSON.stringify(job.result ?? {}));
4186
+ return job.result;
4187
+ }
4188
+ const signed = [];
4189
+ for (const transaction of build.transactions) {
4190
+ const result = await this.signLinkedTransaction({
4191
+ chain: "solana",
4192
+ linkedWalletId: wallet,
4193
+ privyWalletId,
4194
+ unsignedTransactionBase64: transaction.unsignedTransaction
4195
+ });
4196
+ signed.push({ index: transaction.index, signedTransaction: result.signedTransaction });
4197
+ }
4198
+ return this.requestJson("POST", `${base}/submit`, {
4199
+ clientTradeId: req.clientTradeId,
4200
+ signed
4201
+ });
1511
4202
  }
1512
- async getCloseEmptyAccountsJob(clientTradeId) {
4203
+ async getCloseEmptyAccountsJob(clientTradeId, opts = {}) {
1513
4204
  this.requireKey("getCloseEmptyAccountsJob()");
1514
- const body = await this.requestJson("GET", `/api/v1/agent/wallets/embedded/close-empty/jobs/${encodeURIComponent(clientTradeId)}`);
4205
+ const body = await this.requestJson("GET", opts.wallet === undefined ? `/api/v1/agent/wallets/embedded/close-empty/jobs/${encodeURIComponent(clientTradeId)}` : `/api/v1/agent/wallets/${encodeURIComponent(opts.wallet)}/close-empty/jobs/${encodeURIComponent(clientTradeId)}`);
1515
4206
  return body.job;
1516
4207
  }
1517
4208
  async importWallet(params) {
@@ -1994,13 +4685,47 @@ class CandleClient {
1994
4685
  }
1995
4686
  return txHash;
1996
4687
  }
4688
+ marketReader() {
4689
+ this.perpsMarketReader ??= new PerpsMarketReader({
4690
+ apiKey: this.apiKey,
4691
+ network: this.hyperliquidNetwork,
4692
+ fetcher: this.fetchImpl,
4693
+ readConfig: () => this.perpsConfig()
4694
+ });
4695
+ return this.perpsMarketReader;
4696
+ }
4697
+ async perpsMarkets(opts = {}) {
4698
+ return this.marketReader().markets(opts);
4699
+ }
4700
+ async perpsMarket(coin) {
4701
+ return this.marketReader().market(coin);
4702
+ }
4703
+ async perpsBook(coin, opts = {}) {
4704
+ return this.marketReader().book(coin, opts);
4705
+ }
4706
+ async perpsCandles(coin, opts = {}) {
4707
+ return this.marketReader().candles(coin, opts);
4708
+ }
1997
4709
  async perpsConfig() {
1998
4710
  this.requireKey("perpsConfig()");
1999
4711
  return this.requestJson("GET", "/api/v1/agent/perps/config");
2000
4712
  }
2001
4713
  async perpsSetup(params) {
2002
4714
  this.requireKey("perpsSetup()");
4715
+ const builder = this.perpsBuilder();
4716
+ const address = await this.perpsWalletAddress(params.walletId);
4717
+ const mode = await hyperliquidAccountMode(this.fetchImpl, this.hyperliquidNetwork, address);
4718
+ if (!HYPERLIQUID_STANDARD_ACCOUNT_MODES.includes(mode))
4719
+ throw new Error(`perps: PERPS_ACCOUNT_MODE: the account is in ${mode} mode on Hyperliquid; Candle trades only a standard account. Nothing was requested or signed.`);
2003
4720
  const res = await this.requestJson("POST", "/api/v1/agent/perps/setup", { walletId: params.walletId });
4721
+ const problem = perpsSetupStatusProblem(res, {
4722
+ network: this.hyperliquidNetwork,
4723
+ address,
4724
+ builder,
4725
+ mode
4726
+ });
4727
+ if (problem)
4728
+ throw new Error(`perps: refused this setup answer: ${problem}. Nothing was signed.`);
2004
4729
  const status = {
2005
4730
  walletId: res.walletId,
2006
4731
  address: res.address,
@@ -2011,7 +4736,8 @@ class CandleClient {
2011
4736
  withdrawable: res.withdrawable,
2012
4737
  builder: res.builder,
2013
4738
  approvedFeeTenthsBps: res.approvedFeeTenthsBps,
2014
- ready: res.ready
4739
+ ready: res.ready,
4740
+ preview: { builderTrust: this.perpsBuilderTrust() }
2015
4741
  };
2016
4742
  if (res.ready)
2017
4743
  return status;
@@ -2019,18 +4745,39 @@ class CandleClient {
2019
4745
  }
2020
4746
  async perpsOpen(params) {
2021
4747
  this.requireKey("perpsOpen()");
4748
+ this.perpsBuilder();
2022
4749
  const { walletId, privyWalletId: _p, submit: _s, ...order } = params;
2023
- const build = await this.requestJson("POST", "/api/v1/agent/perps/open", { walletId, ...order });
2024
- return this.perpsComplete(build, params, "open");
4750
+ const clientId = perpsClientId(params.clientId);
4751
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/open", {
4752
+ walletId,
4753
+ ...order,
4754
+ ...clientId !== undefined ? { clientId } : {}
4755
+ });
4756
+ const replay = perpsReplayOf(build, walletId, clientId);
4757
+ if (replay)
4758
+ return replay;
4759
+ return this.perpsComplete(build, { ...params, ...clientId ? { clientId } : {} }, "open");
2025
4760
  }
2026
4761
  async perpsClose(params) {
2027
4762
  this.requireKey("perpsClose()");
4763
+ if (params.pct !== undefined && params.size !== undefined)
4764
+ throw new Error("perpsClose(): give a size or a pct, not both");
4765
+ this.perpsBuilder();
2028
4766
  const { walletId, privyWalletId: _p, submit: _s, ...close } = params;
2029
- const build = await this.requestJson("POST", "/api/v1/agent/perps/close", { walletId, ...close });
2030
- return this.perpsComplete(build, params, "close");
4767
+ const clientId = perpsClientId(params.clientId);
4768
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/close", {
4769
+ walletId,
4770
+ ...close,
4771
+ ...clientId !== undefined ? { clientId } : {}
4772
+ });
4773
+ const replay = perpsReplayOf(build, walletId, clientId);
4774
+ if (replay)
4775
+ return replay;
4776
+ return this.perpsComplete(build, { ...params, ...clientId ? { clientId } : {} }, "close");
2031
4777
  }
2032
4778
  async perpsCancel(params) {
2033
4779
  this.requireKey("perpsCancel()");
4780
+ this.perpsBuilder();
2034
4781
  const build = await this.requestJson("POST", "/api/v1/agent/perps/cancel", {
2035
4782
  walletId: params.walletId,
2036
4783
  cloid: params.cloid
@@ -2039,18 +4786,42 @@ class CandleClient {
2039
4786
  }
2040
4787
  async perpsModify(params) {
2041
4788
  this.requireKey("perpsModify()");
4789
+ this.perpsBuilder();
2042
4790
  const { walletId, privyWalletId: _p, submit: _s, ...modify } = params;
2043
4791
  const build = await this.requestJson("POST", "/api/v1/agent/perps/modify", { walletId, ...modify });
2044
4792
  return this.perpsComplete(build, params, "modify");
2045
4793
  }
2046
4794
  async perpsLeverage(params) {
2047
4795
  this.requireKey("perpsLeverage()");
4796
+ if (params.leverage === undefined && params.mode === undefined)
4797
+ throw new Error("perpsLeverage(): leverage is required unless mode names the margin mode");
4798
+ this.perpsBuilder();
2048
4799
  const { walletId, privyWalletId: _p, submit: _s, ...leverage } = params;
2049
4800
  const build = await this.requestJson("POST", "/api/v1/agent/perps/leverage", { walletId, ...leverage });
2050
- return this.perpsComplete(build, params, "leverage");
4801
+ const result = await this.perpsComplete(build, params, "leverage");
4802
+ if (params.mode === undefined || result.outcome?.status !== "confirmed")
4803
+ return result;
4804
+ let confirmedMode = null;
4805
+ try {
4806
+ const res = await this.fetchImpl(HYPERLIQUID_EXCHANGE_URLS[this.hyperliquidNetwork].replace("/exchange", "/info"), {
4807
+ method: "POST",
4808
+ headers: { "content-type": "application/json" },
4809
+ body: JSON.stringify({
4810
+ type: "activeAssetData",
4811
+ user: build.address,
4812
+ coin: String(build.preview.coin ?? params.coin)
4813
+ })
4814
+ });
4815
+ const type = JSON.parse(await res.text())?.leverage?.type;
4816
+ confirmedMode = type === "cross" || type === "isolated" ? type : null;
4817
+ } catch {
4818
+ confirmedMode = null;
4819
+ }
4820
+ return { ...result, requestedMode: params.mode, confirmedMode };
2051
4821
  }
2052
4822
  async perpsMargin(params) {
2053
4823
  this.requireKey("perpsMargin()");
4824
+ this.perpsBuilder();
2054
4825
  const build = await this.requestJson("POST", "/api/v1/agent/perps/margin", {
2055
4826
  walletId: params.walletId,
2056
4827
  coin: params.coin,
@@ -2060,7 +4831,22 @@ class CandleClient {
2060
4831
  }
2061
4832
  async perpsPositions(walletId) {
2062
4833
  this.requireKey("perpsPositions()");
2063
- return this.requestJson("GET", `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(walletId)}`);
4834
+ const server = await this.requestJson("GET", `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(walletId)}`);
4835
+ const { effective } = await this.perpsThresholdsOrDefaults();
4836
+ const identity2 = { walletId, address: server.address, network: this.hyperliquidNetwork };
4837
+ let report;
4838
+ try {
4839
+ report = await readPerpsRisk({
4840
+ ...identity2,
4841
+ fetcher: this.fetchImpl,
4842
+ thresholds: effective,
4843
+ now: Date.now
4844
+ });
4845
+ } catch {
4846
+ report = unknownRiskReport(identity2, effective, "PERPS_READ_FAILED", Date.now());
4847
+ }
4848
+ const { positions: rows, ...risk } = report;
4849
+ return { ...server, rows, accountLine: report.account, risk };
2064
4850
  }
2065
4851
  async perpsOrders(walletId, opts = {}) {
2066
4852
  this.requireKey("perpsOrders()");
@@ -2069,6 +4855,43 @@ class CandleClient {
2069
4855
  params.set("limit", String(opts.limit));
2070
4856
  return this.requestJson("GET", `/api/v1/agent/perps/orders?${params}`);
2071
4857
  }
4858
+ async perpsFillsPage(walletId, options = {}) {
4859
+ this.requireKey("perpsFillsPage()");
4860
+ return await this.perpsHistoryPage("fills", walletId, options);
4861
+ }
4862
+ async perpsFundingPage(walletId, options = {}) {
4863
+ this.requireKey("perpsFundingPage()");
4864
+ return await this.perpsHistoryPage("funding", walletId, options);
4865
+ }
4866
+ async perpsHistoryPage(kind, walletId, options) {
4867
+ const address = await this.perpsWalletAddress(walletId);
4868
+ return readPerpsHistoryPage({
4869
+ kind,
4870
+ network: this.hyperliquidNetwork,
4871
+ walletId,
4872
+ address,
4873
+ options,
4874
+ fetch: this.fetchImpl,
4875
+ now: Date.now
4876
+ });
4877
+ }
4878
+ async perpsWalletAddress(walletId) {
4879
+ let cursor = null;
4880
+ for (let page = 0;page < 10; page++) {
4881
+ const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
4882
+ const listed = await this.requestJson("GET", `/api/v1/agent/wallets/trading${query}`);
4883
+ const row = (listed.page ?? []).find((r) => r.id === walletId);
4884
+ if (row) {
4885
+ if (row.chain !== "evm" || !/^0x[0-9a-fA-F]{40}$/.test(row.address))
4886
+ throw new Error(`perps: wallet ${walletId} is not an EVM TEE wallet`);
4887
+ return row.address.toLowerCase();
4888
+ }
4889
+ if (listed.isDone !== false || !listed.continueCursor)
4890
+ break;
4891
+ cursor = listed.continueCursor;
4892
+ }
4893
+ throw new Error(`perps: wallet ${walletId} is not an EVM TEE wallet bound to this key`);
4894
+ }
2072
4895
  async perpsFills(walletId) {
2073
4896
  this.requireKey("perpsFills()");
2074
4897
  return this.requestJson("GET", `/api/v1/agent/perps/fills?walletId=${encodeURIComponent(walletId)}`);
@@ -2080,6 +4903,87 @@ class CandleClient {
2080
4903
  params.set("startTime", String(startTime));
2081
4904
  return this.requestJson("GET", `/api/v1/agent/perps/funding?${params}`);
2082
4905
  }
4906
+ async getPerpsRisk() {
4907
+ this.requireKey("getPerpsRisk()");
4908
+ const { thresholds, stored } = perpsRiskFromLimits(await this.requestJson("GET", "/api/v1/agent/keys/self/limits"));
4909
+ return { perpsRisk: stored, effective: thresholds };
4910
+ }
4911
+ async setPerpsRisk(fields) {
4912
+ this.requireKey("setPerpsRisk()");
4913
+ return this.requestJson("PUT", "/api/v1/agent/keys/self/perps-risk", { perpsRisk: fields });
4914
+ }
4915
+ async setKeyPerpsRisk(keyPrefix, fields, owner) {
4916
+ const res = await this.fetchImpl(`${this.apiUrl}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/perps-risk`, {
4917
+ method: "PUT",
4918
+ headers: { "content-type": "application/json", authorization: `Bearer ${owner.bearerToken}` },
4919
+ body: JSON.stringify({ perpsRisk: fields })
4920
+ });
4921
+ return this.parseResponse(res);
4922
+ }
4923
+ async perpsRisk(params) {
4924
+ this.requireKey("perpsRisk()");
4925
+ const address = params.address ?? await this.perpsWalletAddress(params.walletId);
4926
+ const { effective } = await this.perpsThresholdsOrDefaults();
4927
+ return readPerpsRisk({
4928
+ fetcher: this.fetchImpl,
4929
+ network: this.hyperliquidNetwork,
4930
+ walletId: params.walletId,
4931
+ address,
4932
+ thresholds: effective,
4933
+ now: Date.now
4934
+ });
4935
+ }
4936
+ async perpsAccount(params) {
4937
+ this.requireKey("perpsAccount()");
4938
+ const now = Date.now();
4939
+ const startTime = params.since ?? now - 24 * 60 * 60 * 1000;
4940
+ if (!Number.isSafeInteger(startTime) || startTime >= now || now - startTime > PERPS_RECENT_MAX_MS)
4941
+ throw new Error("perpsAccount(): since must be an epoch-ms time within the last 30 days");
4942
+ const address = params.address ?? await this.perpsWalletAddress(params.walletId);
4943
+ const { effective } = await this.perpsThresholdsOrDefaults();
4944
+ const fetcher = this.fetchImpl;
4945
+ const [report, recent] = await Promise.all([
4946
+ readPerpsRisk({
4947
+ fetcher,
4948
+ network: this.hyperliquidNetwork,
4949
+ walletId: params.walletId,
4950
+ address,
4951
+ thresholds: effective,
4952
+ now: Date.now
4953
+ }),
4954
+ readPerpsRecent(fetcher, this.hyperliquidNetwork, address, { startTime, endTime: now })
4955
+ ]);
4956
+ return perpsAccountReport(report, recent);
4957
+ }
4958
+ async watchPerpsRisk(params) {
4959
+ this.requireKey("watchPerpsRisk()");
4960
+ const address = params.address ?? await this.perpsWalletAddress(params.walletId);
4961
+ return watchPerpsRisk({
4962
+ fetcher: this.fetchImpl,
4963
+ network: this.hyperliquidNetwork,
4964
+ walletId: params.walletId,
4965
+ address,
4966
+ thresholds: async () => (await this.getPerpsRisk()).effective,
4967
+ ...params.intervalMs !== undefined ? { intervalMs: params.intervalMs } : {},
4968
+ ...params.heartbeatMs !== undefined ? { heartbeatMs: params.heartbeatMs } : {},
4969
+ ...params.signal ? { signal: params.signal } : {},
4970
+ ...params.onceLevelChange ? { onceLevelChange: true } : {},
4971
+ now: Date.now,
4972
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
4973
+ onEvent: params.onEvent
4974
+ });
4975
+ }
4976
+ async perpsPortfolioSection(wallets) {
4977
+ return perpsPortfolioSection(this.fetchImpl, this.hyperliquidNetwork, wallets);
4978
+ }
4979
+ async perpsThresholdsOrDefaults() {
4980
+ try {
4981
+ return await this.getPerpsRisk();
4982
+ } catch {
4983
+ const { thresholds, stored } = perpsRiskFromLimits(null);
4984
+ return { perpsRisk: stored, effective: thresholds };
4985
+ }
4986
+ }
2083
4987
  async perpsDeposit(params) {
2084
4988
  this.requireKey("perpsDeposit()");
2085
4989
  const build = await this.requestJson("POST", "/api/v1/agent/perps/deposit", {
@@ -2088,7 +4992,8 @@ class CandleClient {
2088
4992
  asset: params.asset,
2089
4993
  amountRaw: params.amountRaw,
2090
4994
  ...params.perpsWalletId !== undefined ? { perpsWalletId: params.perpsWalletId } : {},
2091
- ...params.maxSlippageBps !== undefined ? { maxSlippageBps: params.maxSlippageBps } : {}
4995
+ ...params.maxSlippageBps !== undefined ? { maxSlippageBps: params.maxSlippageBps } : {},
4996
+ ...params.submit === false ? { preview: true } : {}
2092
4997
  });
2093
4998
  if (build.job !== undefined)
2094
4999
  throw new Error(`perpsDeposit(): ${params.clientDepositId} already names a deposit; read it with perpsDepositStatus()`);
@@ -2097,6 +5002,8 @@ class CandleClient {
2097
5002
  throw new Error(`perps: refused to sign this deposit: ${problem}`);
2098
5003
  if (params.submit === false)
2099
5004
  return { build, submitted: false, result: null };
5005
+ if (build.preview === true || build.depositId === undefined)
5006
+ throw new Error("perps: refused to sign this deposit: the API answered a preview, not a build");
2100
5007
  const submitPath = "/api/v1/agent/perps/deposit/submit";
2101
5008
  const ids = { clientDepositId: params.clientDepositId, depositId: build.depositId };
2102
5009
  if (build.chain === "solana") {
@@ -2153,45 +5060,32 @@ class CandleClient {
2153
5060
  this.requireKey("perpsDepositStatus()");
2154
5061
  return this.requestJson("GET", `/api/v1/agent/perps/deposit/${encodeURIComponent(clientDepositId)}`);
2155
5062
  }
2156
- async perpsBuilder() {
2157
- if (this.hyperliquidBuilder)
2158
- return this.hyperliquidBuilder;
2159
- const config = await this.perpsConfig();
2160
- if (!config.builder)
2161
- throw new Error("perps: the server reports no Hyperliquid builder address");
2162
- this.hyperliquidBuilder = config.builder.toLowerCase();
2163
- return this.hyperliquidBuilder;
5063
+ perpsBuilder() {
5064
+ return resolveHyperliquidBuilder(this.hyperliquidBuilder);
5065
+ }
5066
+ perpsBuilderTrust() {
5067
+ return {
5068
+ address: this.perpsBuilder(),
5069
+ source: this.hyperliquidBuilder === undefined ? "release" : "override"
5070
+ };
2164
5071
  }
2165
5072
  async perpsComplete(build, ref, method) {
2166
- const check = verifyPerpsBuild(build, {
2167
- builder: await this.perpsBuilder(),
5073
+ build = { ...build, preview: { ...build.preview, builderTrust: this.perpsBuilderTrust() } };
5074
+ const params = ref;
5075
+ const opts = {
5076
+ builder: this.perpsBuilder(),
2168
5077
  network: this.hyperliquidNetwork,
2169
- intent: { method, params: ref }
5078
+ intent: { method, params },
5079
+ keyReduceOnly: build.perpsReduceOnly === true
5080
+ };
5081
+ const check = method === "setup" ? verifyPerpsBuild(build, opts) : await verifyPerpsBuildAgainstVenue(build, {
5082
+ ...opts,
5083
+ address: build.address,
5084
+ fetcher: this.fetchImpl,
5085
+ now: Date.now
2170
5086
  });
2171
5087
  if (!check.ok)
2172
5088
  throw new Error(`perps: refused to sign this build: ${check.reason}`);
2173
- if (method === "close") {
2174
- const closeOrder = await hyperliquidCloseOrder(this.fetchImpl, this.hyperliquidNetwork, build.address, ref);
2175
- const closeCheck = verifyPerpsBuild(build, {
2176
- builder: await this.perpsBuilder(),
2177
- network: this.hyperliquidNetwork,
2178
- intent: { method, params: ref },
2179
- closeOrder
2180
- });
2181
- if (!closeCheck.ok)
2182
- throw new Error(`perps: refused to sign this build: ${closeCheck.reason}`);
2183
- }
2184
- if (method === "open" || method === "modify") {
2185
- const market = await hyperliquidOrderMarket(this.fetchImpl, this.hyperliquidNetwork, build.address, method, ref);
2186
- const marketCheck = verifyPerpsBuild(build, {
2187
- builder: await this.perpsBuilder(),
2188
- network: this.hyperliquidNetwork,
2189
- intent: { method, params: ref },
2190
- market
2191
- });
2192
- if (!marketCheck.ok)
2193
- throw new Error(`perps: refused to sign this build: ${marketCheck.reason}`);
2194
- }
2195
5089
  if (ref.submit === false)
2196
5090
  return { build, signature: null, submitted: false, exchange: null };
2197
5091
  const signature = await this.signLinkedTypedData({
@@ -2209,7 +5103,8 @@ class CandleClient {
2209
5103
  if (!res.ok) {
2210
5104
  return { build, signature, submitted: false, exchange: null, submitError: `HTTP ${res.status}: ${text}` };
2211
5105
  }
2212
- return { build, signature, submitted: true, exchange: JSON.parse(text) };
5106
+ const exchange = JSON.parse(text);
5107
+ return { build, signature, submitted: true, exchange, outcome: hyperliquidExchangeOutcome(exchange) };
2213
5108
  } catch (err) {
2214
5109
  return {
2215
5110
  build,
@@ -2220,6 +5115,159 @@ class CandleClient {
2220
5115
  };
2221
5116
  }
2222
5117
  }
5118
+ async perpsScheduleCancel(params) {
5119
+ this.requireKey("perpsScheduleCancel()");
5120
+ const clear = params.clear === true;
5121
+ if (clear === (params.seconds !== undefined))
5122
+ throw new Error("perpsScheduleCancel(): give seconds, or clear: true");
5123
+ const build = await this.requestJson("POST", "/api/v1/agent/perps/schedule-cancel", {
5124
+ walletId: params.walletId,
5125
+ ...clear ? { clear: true } : { seconds: params.seconds }
5126
+ });
5127
+ const result = await this.perpsComplete(build, params, "scheduleCancel");
5128
+ if (result.outcome?.status === "rejected")
5129
+ return { ...result, submitError: `PERPS_SCHEDULE_CANCEL_REJECTED: ${result.outcome.error}` };
5130
+ if (result.outcome?.status !== "confirmed")
5131
+ return result;
5132
+ let acknowledged = false;
5133
+ try {
5134
+ const ack = await this.requestJson("POST", "/api/v1/agent/perps/schedule-cancel/ack", {
5135
+ walletId: params.walletId,
5136
+ perpOrderId: build.perpOrderId,
5137
+ exchange: result.exchange
5138
+ });
5139
+ acknowledged = ack.acknowledged === true;
5140
+ } catch {
5141
+ acknowledged = false;
5142
+ }
5143
+ return { ...result, acknowledged };
5144
+ }
5145
+ async perpsTpsl(params) {
5146
+ this.requireKey("perpsTpsl()");
5147
+ const { walletId, privyWalletId: _p, submit: _s, ...tpsl } = params;
5148
+ return runPerpsTpsl(await this.perpsWorkflowIo(params), tpsl);
5149
+ }
5150
+ async perpsFlatten(params) {
5151
+ this.requireKey("perpsFlatten()");
5152
+ return runPerpsFlatten(await this.perpsWorkflowIo(params));
5153
+ }
5154
+ async perpsWorkflowIo(ref) {
5155
+ const address = await this.perpsWalletAddress(ref.walletId);
5156
+ const builder = this.perpsBuilder();
5157
+ const reduceOnly = await this.getPerpsReduceOnly().catch(() => null);
5158
+ return {
5159
+ network: this.hyperliquidNetwork,
5160
+ walletId: ref.walletId,
5161
+ address,
5162
+ builder,
5163
+ keyReduceOnly: reduceOnly === null ? true : reduceOnly.perpsReduceOnly,
5164
+ fetcher: this.fetchImpl,
5165
+ now: Date.now,
5166
+ build: (path, body) => this.requestJson("POST", path, body),
5167
+ read: (path) => this.requestJson("GET", path),
5168
+ sign: (typedData) => this.signLinkedTypedData({ linkedWalletId: ref.walletId, privyWalletId: ref.privyWalletId, typedData }),
5169
+ mode: ref.submit === false ? "no-submit" : "submit",
5170
+ cloid: () => {
5171
+ const bytes = new Uint8Array(16);
5172
+ globalThis.crypto.getRandomValues(bytes);
5173
+ return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
5174
+ }
5175
+ };
5176
+ }
5177
+ async getPerpsReduceOnly() {
5178
+ this.requireKey("getPerpsReduceOnly()");
5179
+ return this.requestJson("GET", "/api/v1/agent/keys/self/perps-reduce-only");
5180
+ }
5181
+ async setPerpsReduceOnly(perpsReduceOnly) {
5182
+ this.requireKey("setPerpsReduceOnly()");
5183
+ return this.requestJson("PUT", "/api/v1/agent/keys/self/perps-reduce-only", {
5184
+ perpsReduceOnly
5185
+ });
5186
+ }
5187
+ async setKeyPerpsReduceOnly(keyPrefix, perpsReduceOnly, owner) {
5188
+ const res = await this.fetchImpl(`${this.apiUrl}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/perps-reduce-only`, {
5189
+ method: "PUT",
5190
+ headers: { "content-type": "application/json", authorization: `Bearer ${owner.bearerToken}` },
5191
+ body: JSON.stringify({ perpsReduceOnly })
5192
+ });
5193
+ return this.parseResponse(res);
5194
+ }
5195
+ async perpsWithdraw(params) {
5196
+ this.requireKey("perpsWithdraw()");
5197
+ if (this.hyperliquidNetwork !== "mainnet")
5198
+ throw new Error("perpsWithdraw: withdrawals are mainnet only");
5199
+ if (this.perpsWithdrawPins === null) {
5200
+ throw new Error("perpsWithdraw: no withdrawal pins are set; nothing was built");
5201
+ }
5202
+ const amountMicros = parseWithdrawAmount(params.amountUsdc);
5203
+ if (amountMicros === null)
5204
+ throw new Error("perpsWithdraw: amountUsdc must be a positive decimal, at most 6 dp");
5205
+ const id = params.clientWithdrawalId ?? `withdraw-${crypto.randomUUID()}`;
5206
+ const build = await this.perps.withdraw.build({
5207
+ clientWithdrawalId: id,
5208
+ amountUsdc: params.amountUsdc,
5209
+ to: params.to,
5210
+ wallet: params.walletId
5211
+ });
5212
+ if (build.status !== "quoted") {
5213
+ throw new Error(`perpsWithdraw: withdrawal ${id} is already ${build.status}; read it with perps.withdraw.get()`);
5214
+ }
5215
+ const mapping = verifyWithdrawMapping(build, {
5216
+ id,
5217
+ account: params.account,
5218
+ to: params.to,
5219
+ amountMicros,
5220
+ ownDestinations: params.ownDestinations,
5221
+ pins: this.perpsWithdrawPins
5222
+ });
5223
+ if (!mapping.ok)
5224
+ throw new Error(`perpsWithdraw: refused to sign this withdrawal: ${mapping.reason}`);
5225
+ const mappingSignature = await this.signLinkedTypedData({
5226
+ linkedWalletId: params.walletId,
5227
+ privyWalletId: params.privyWalletId,
5228
+ typedData: build.typedData
5229
+ });
5230
+ const authorization = await this.perps.withdraw.authorize({ clientWithdrawalId: id, signature: mappingSignature });
5231
+ const send = verifyWithdrawSendAsset(authorization, {
5232
+ mappingNonce: build.nonce,
5233
+ amountMicros,
5234
+ pins: this.perpsWithdrawPins
5235
+ });
5236
+ if (!send.ok)
5237
+ throw new Error(`perpsWithdraw: refused to sign the transfer: ${send.reason}`);
5238
+ const signature = await this.signLinkedTypedData({
5239
+ linkedWalletId: params.walletId,
5240
+ privyWalletId: params.privyWalletId,
5241
+ typedData: authorization.typedData
5242
+ });
5243
+ let exchange = null;
5244
+ let submitError;
5245
+ try {
5246
+ const res = await this.fetchImpl(HYPERLIQUID_EXCHANGE_URLS.mainnet, {
5247
+ method: "POST",
5248
+ headers: { "content-type": "application/json" },
5249
+ body: JSON.stringify(hyperliquidExchangeBody(authorization.action, authorization.nonce, signature))
5250
+ });
5251
+ const text = await res.text();
5252
+ if (res.ok)
5253
+ exchange = JSON.parse(text);
5254
+ else
5255
+ submitError = `HTTP ${res.status}: ${text}`;
5256
+ } catch (err) {
5257
+ submitError = err instanceof Error ? err.message : String(err);
5258
+ }
5259
+ if (exchange !== null) {
5260
+ await this.perps.withdraw.reportSubmitted({ clientWithdrawalId: id, exchangeResponse: exchange }).catch(() => {});
5261
+ }
5262
+ return {
5263
+ build,
5264
+ authorization,
5265
+ signature,
5266
+ submitted: exchange !== null,
5267
+ exchange,
5268
+ ...submitError !== undefined ? { submitError } : {}
5269
+ };
5270
+ }
2223
5271
  async signLinkedTypedData(params) {
2224
5272
  if (!this.privyAppId) {
2225
5273
  throw new Error("perps signing requires privyAppId in CandleClientOptions (the relay's Privy app id)");
@@ -2431,7 +5479,7 @@ function generateClientLaunchId() {
2431
5479
  function generateClientTradeId() {
2432
5480
  return generateSdkId();
2433
5481
  }
2434
- var SDK_VERSION = "0.6.2";
5482
+ var SDK_VERSION = "0.6.4";
2435
5483
  var sdkUpdateWarned = false;
2436
5484
  function noteLatestSdkVersion(value) {
2437
5485
  if (sdkUpdateWarned || !value || !/^\d+\.\d+\.\d+$/.test(value))
@@ -2629,6 +5677,188 @@ function planTableMarkdown(table) {
2629
5677
  return [line(headers), line(headers.map(() => "---")), ...rows.map(line)].join(`
2630
5678
  `);
2631
5679
  }
5680
+ // src/quant.ts
5681
+ var QUANT_AGENT_PATH = "/api/v1/agent/quant";
5682
+ var QUANT_CALL_TIMEOUT_MS = 5000;
5683
+ var QUANT_POLL_TIMEOUT_MS = 35000;
5684
+ var QUANT_DEFAULT_MAX_TEXT_CHARS = 4000;
5685
+ function quantCodePoints(text) {
5686
+ let n = 0;
5687
+ for (const _ of text)
5688
+ n++;
5689
+ return n;
5690
+ }
5691
+ function newClientTurnId() {
5692
+ return crypto.randomUUID();
5693
+ }
5694
+ function quantRefusalCode(body) {
5695
+ const record = asRecord(body);
5696
+ if (typeof record?.code === "string")
5697
+ return record.code;
5698
+ const inner = asRecord(record?.error);
5699
+ return typeof inner?.code === "string" ? inner.code : undefined;
5700
+ }
5701
+ function asRecord(value) {
5702
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
5703
+ }
5704
+ function refusalOf(status, body) {
5705
+ const record = asRecord(body) ?? {};
5706
+ const relayCode = typeof record.code === "string" ? record.code : undefined;
5707
+ const code = quantRefusalCode(body) ?? `HTTP_${status}`;
5708
+ const refusal = { code, status, shape: relayCode !== undefined ? "relay" : "envelope" };
5709
+ if (typeof record.retryable === "boolean")
5710
+ refusal.retryable = record.retryable;
5711
+ if (typeof record.resetsAt === "string" || typeof record.resetsAt === "number")
5712
+ refusal.resetsAt = record.resetsAt;
5713
+ if (typeof record.turnId === "string")
5714
+ refusal.turnId = record.turnId;
5715
+ if (typeof record.limit === "number")
5716
+ refusal.limit = record.limit;
5717
+ return refusal;
5718
+ }
5719
+ function parseStatus(body) {
5720
+ const b = asRecord(body);
5721
+ if (typeof b?.enabled !== "boolean")
5722
+ return;
5723
+ const status = { enabled: b.enabled };
5724
+ if (typeof b.allowed === "boolean")
5725
+ status.allowed = b.allowed;
5726
+ if (typeof b.tier === "string" || b.tier === null)
5727
+ status.tier = b.tier;
5728
+ if (typeof b.marketAccess === "boolean")
5729
+ status.marketAccess = b.marketAccess;
5730
+ if (typeof b.linked === "boolean")
5731
+ status.linked = b.linked;
5732
+ if (typeof b.remaining === "number" || b.remaining === null)
5733
+ status.remaining = b.remaining;
5734
+ if (typeof b.maxTextChars === "number" || b.maxTextChars === null)
5735
+ status.maxTextChars = b.maxTextChars;
5736
+ if (typeof b.activeTurnId === "string" || b.activeTurnId === null)
5737
+ status.activeTurnId = b.activeTurnId;
5738
+ return status;
5739
+ }
5740
+ function parseTurn(body) {
5741
+ const b = asRecord(body);
5742
+ if (typeof b?.turnId !== "string" || b.turnId.length === 0)
5743
+ return;
5744
+ return { turnId: b.turnId, ...typeof b.cursor === "number" ? { cursor: b.cursor } : {} };
5745
+ }
5746
+ function parseEvents(body) {
5747
+ const b = asRecord(body);
5748
+ if (!b || !Array.isArray(b.events) || typeof b.next !== "number" || typeof b.status !== "string")
5749
+ return;
5750
+ const events = [];
5751
+ for (const raw of b.events) {
5752
+ const e = asRecord(raw);
5753
+ if (typeof e?.seq !== "number" || typeof e.kind !== "string")
5754
+ continue;
5755
+ events.push({
5756
+ seq: e.seq,
5757
+ kind: e.kind,
5758
+ data: asRecord(e.data) ?? {},
5759
+ ...typeof e.createdAt === "string" ? { createdAt: e.createdAt } : {}
5760
+ });
5761
+ }
5762
+ return { events, next: b.next, status: b.status };
5763
+ }
5764
+ function parseHistory(body) {
5765
+ const b = asRecord(body);
5766
+ if (!b || !Array.isArray(b.items))
5767
+ return;
5768
+ const items = [];
5769
+ for (const raw of b.items) {
5770
+ const item = asRecord(raw);
5771
+ if (typeof item?.id !== "string" || typeof item.text !== "string")
5772
+ continue;
5773
+ if (item.role !== "user" && item.role !== "assistant")
5774
+ continue;
5775
+ items.push({
5776
+ id: item.id,
5777
+ role: item.role,
5778
+ text: item.text,
5779
+ createdAt: typeof item.createdAt === "string" ? item.createdAt : ""
5780
+ });
5781
+ }
5782
+ return { items, next: typeof b.next === "string" ? b.next : null };
5783
+ }
5784
+ function createQuantKeyClient(options) {
5785
+ const base = new URL(options.apiUrl);
5786
+ const local = base.hostname === "localhost" || base.hostname === "127.0.0.1" || base.hostname === "[::1]";
5787
+ if (base.protocol !== "https:" && !(base.protocol === "http:" && local)) {
5788
+ throw new Error("Quant needs an https API URL: the key would otherwise travel in clear.");
5789
+ }
5790
+ const root = base.toString().replace(/\/+$/, "");
5791
+ const doFetch = options.fetch ?? globalThis.fetch;
5792
+ const callMs = options.callTimeoutMs ?? QUANT_CALL_TIMEOUT_MS;
5793
+ const pollMs = options.pollTimeoutMs ?? QUANT_POLL_TIMEOUT_MS;
5794
+ async function call(method, path, parse, opts) {
5795
+ const controller = new AbortController;
5796
+ let timedOut = false;
5797
+ const timer = setTimeout(() => {
5798
+ timedOut = true;
5799
+ controller.abort();
5800
+ }, opts.timeoutMs);
5801
+ const onAbort = () => controller.abort();
5802
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
5803
+ try {
5804
+ const response = await doFetch(`${root}${QUANT_AGENT_PATH}${path}`, {
5805
+ method,
5806
+ redirect: "error",
5807
+ signal: controller.signal,
5808
+ headers: {
5809
+ "x-api-key": options.apiKey,
5810
+ accept: "application/json",
5811
+ ...opts.body !== undefined ? { "content-type": "application/json" } : {}
5812
+ },
5813
+ ...opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}
5814
+ });
5815
+ let body;
5816
+ try {
5817
+ body = await response.json();
5818
+ } catch {
5819
+ body = undefined;
5820
+ }
5821
+ if (!response.ok)
5822
+ return { ok: false, refusal: refusalOf(response.status, body) };
5823
+ const parsed = parse(body);
5824
+ if (parsed === undefined) {
5825
+ return { ok: false, refusal: { code: "BAD_RESPONSE", status: response.status, shape: "transport" } };
5826
+ }
5827
+ return { ok: true, status: response.status, body: parsed };
5828
+ } catch {
5829
+ return {
5830
+ ok: false,
5831
+ refusal: { code: timedOut ? "TIMEOUT" : "NETWORK_ERROR", status: 0, shape: "transport", retryable: true }
5832
+ };
5833
+ } finally {
5834
+ clearTimeout(timer);
5835
+ opts.signal?.removeEventListener("abort", onAbort);
5836
+ }
5837
+ }
5838
+ const turnPath = (turnId) => `/turns/${encodeURIComponent(turnId)}`;
5839
+ return {
5840
+ status: () => call("GET", "/status", parseStatus, { timeoutMs: callMs }),
5841
+ sendTurn: (input) => call("POST", "/turns", parseTurn, {
5842
+ body: { text: input.text, clientTurnId: input.clientTurnId },
5843
+ timeoutMs: callMs
5844
+ }),
5845
+ events: (turnId, after, opts) => call("GET", `${turnPath(turnId)}/events?after=${Math.max(0, Math.floor(after))}`, parseEvents, {
5846
+ timeoutMs: pollMs,
5847
+ ...opts?.signal ? { signal: opts.signal } : {}
5848
+ }),
5849
+ cancel: (turnId) => call("POST", `${turnPath(turnId)}/cancel`, (body) => asRecord(body) ?? {}, { timeoutMs: callMs }),
5850
+ history: (input = {}) => {
5851
+ const query = new URLSearchParams;
5852
+ if (input.before !== undefined)
5853
+ query.set("before", input.before);
5854
+ if (input.limit !== undefined)
5855
+ query.set("limit", String(input.limit));
5856
+ const encoded = query.toString();
5857
+ const suffix = encoded ? `?${encoded}` : "";
5858
+ return call("GET", `/history${suffix}`, parseHistory, { timeoutMs: callMs });
5859
+ }
5860
+ };
5861
+ }
2632
5862
  // src/file-lock.ts
2633
5863
  var STALE_MS = 30000;
2634
5864
  var RETRY_MS = 25;
@@ -2652,7 +5882,7 @@ async function withFileLock(target, fn) {
2652
5882
  if (Date.now() > deadline) {
2653
5883
  throw new Error(`Timed out waiting for ${lockPath}. Another candle process is writing; if none is running, delete that file.`);
2654
5884
  }
2655
- await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
5885
+ await new Promise((resolve2) => setTimeout(resolve2, RETRY_MS));
2656
5886
  }
2657
5887
  }
2658
5888
  try {
@@ -2788,29 +6018,68 @@ function verifyWebhookSignature(secret, header, body, nowSec, toleranceSec = 300
2788
6018
  return timingSafeEqual(provided, expected);
2789
6019
  }
2790
6020
  export {
6021
+ watchPerpsRisk,
6022
+ verifyWithdrawSendAsset,
6023
+ verifyWithdrawMapping,
2791
6024
  verifyWebhookSignature,
6025
+ verifyPerpsBuildAgainstVenue,
2792
6026
  verifyPerpsBuild,
6027
+ readPerpsRisk,
6028
+ readPerpsHistoryPage,
6029
+ readHyperliquidMarkets,
6030
+ quantRefusalCode,
6031
+ quantCodePoints,
2793
6032
  planTableRows,
2794
6033
  planTableMarkdown,
2795
6034
  planPromotionLine,
2796
6035
  planLabel,
6036
+ perpsSetupStatusProblem,
6037
+ perpsRiskStep,
6038
+ perpsRiskExitCode,
6039
+ perpsPortfolioSection,
6040
+ perpsMarketGate,
2797
6041
  perpsDepositProblem,
6042
+ perpsCandleWindow,
6043
+ parseWithdrawAmount,
6044
+ parseSendAssetAmount,
2798
6045
  operationIds,
6046
+ newClientTurnId,
2799
6047
  isSolanaRpcErrorData,
6048
+ hyperliquidWalletSnapshot,
6049
+ hyperliquidRiskReport,
2800
6050
  hyperliquidOrderMarket,
6051
+ hyperliquidExchangeOutcome,
2801
6052
  hyperliquidActionHash,
6053
+ hyperliquidAccountMode,
2802
6054
  generateSignerKeypair,
2803
6055
  formatPlanBps,
2804
6056
  encryptWalletKeyForImport,
6057
+ effectivePerpsRisk,
6058
+ createQuantKeyClient,
2805
6059
  TradeLegOutcomeError,
2806
6060
  TradeLandedUnconfirmedError,
2807
6061
  TradeLandedFeeLegError,
2808
6062
  SelfLaunchOutcomeError,
6063
+ QUANT_POLL_TIMEOUT_MS,
6064
+ QUANT_DEFAULT_MAX_TEXT_CHARS,
6065
+ QUANT_CALL_TIMEOUT_MS,
6066
+ QUANT_AGENT_PATH,
6067
+ PerpsWorkflowError,
6068
+ PerpsMarketReader,
6069
+ PerpsMarketError,
6070
+ PerpsHistoryError,
2809
6071
  PLAN_CAPABILITY_NOTE,
2810
6072
  PLAN_CAPABILITY_LABELS,
6073
+ PERPS_HISTORY_MAX_LIMIT,
6074
+ PERPS_HISTORY_DEFAULT_LIMIT,
6075
+ PERPS_FLATTEN_MAX_STEPS,
6076
+ PERPS_CANDLE_INTERVALS,
2811
6077
  KeychainSecretStore,
2812
6078
  JsonRpcError,
2813
6079
  InMemorySecretStore,
6080
+ HyperliquidReadError,
6081
+ HYPERLIQUID_STANDARD_ACCOUNT_MODES,
6082
+ HYPERLIQUID_RISK_DEFAULTS,
2814
6083
  HYPERLIQUID_EXCHANGE_URLS,
2815
6084
  HYPERLIQUID_ALLOWED_ACTION_TYPES,
2816
6085
  EncryptedFileSecretStore,
@@ -2818,6 +6087,7 @@ export {
2818
6087
  CandleOperationError,
2819
6088
  CandleClient,
2820
6089
  CandleApiError,
6090
+ CANDLE_PERPS_WITHDRAW_PINS,
2821
6091
  CANDLE_HYPERLIQUID_BUILDER_ADDRESS,
2822
6092
  BRIDGE_ERROR_CODES
2823
6093
  };