@candledottv/agent-sdk 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +394 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/hyperliquid.d.ts +116 -0
- package/dist/hyperliquid.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +581 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -260,6 +260,406 @@ async function waitForReceipt(rpc, txHash, opts = {}) {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// src/hyperliquid.ts
|
|
264
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
265
|
+
var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
|
|
266
|
+
"order",
|
|
267
|
+
"cancel",
|
|
268
|
+
"modify",
|
|
269
|
+
"updateLeverage",
|
|
270
|
+
"updateIsolatedMargin"
|
|
271
|
+
];
|
|
272
|
+
var HYPERLIQUID_EXCHANGE_URLS = {
|
|
273
|
+
mainnet: "https://api.hyperliquid.xyz/exchange",
|
|
274
|
+
testnet: "https://api.hyperliquid-testnet.xyz/exchange"
|
|
275
|
+
};
|
|
276
|
+
var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
|
|
277
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
|
|
278
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
|
|
279
|
+
var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
|
|
280
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
281
|
+
var UINT64_MAX = (1n << 64n) - 1n;
|
|
282
|
+
var INT64_MIN = -(1n << 63n);
|
|
283
|
+
function pushUint(out, value, width) {
|
|
284
|
+
for (let i = width - 1;i >= 0; i--)
|
|
285
|
+
out.push(Number(value >> BigInt(i * 8) & 0xffn));
|
|
286
|
+
}
|
|
287
|
+
function packInteger(out, value) {
|
|
288
|
+
if (value >= 0n) {
|
|
289
|
+
if (value > UINT64_MAX)
|
|
290
|
+
throw new Error("msgpack: integer above uint64");
|
|
291
|
+
if (value < 0x80n)
|
|
292
|
+
out.push(Number(value));
|
|
293
|
+
else if (value <= 0xffn) {
|
|
294
|
+
out.push(204);
|
|
295
|
+
pushUint(out, value, 1);
|
|
296
|
+
} else if (value <= 0xffffn) {
|
|
297
|
+
out.push(205);
|
|
298
|
+
pushUint(out, value, 2);
|
|
299
|
+
} else if (value <= 0xffffffffn) {
|
|
300
|
+
out.push(206);
|
|
301
|
+
pushUint(out, value, 4);
|
|
302
|
+
} else {
|
|
303
|
+
out.push(207);
|
|
304
|
+
pushUint(out, value, 8);
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (value < INT64_MIN)
|
|
309
|
+
throw new Error("msgpack: integer below int64");
|
|
310
|
+
if (value >= -32n)
|
|
311
|
+
out.push(Number(value & 0xffn));
|
|
312
|
+
else if (value >= -128n) {
|
|
313
|
+
out.push(208);
|
|
314
|
+
pushUint(out, value & 0xffn, 1);
|
|
315
|
+
} else if (value >= -32768n) {
|
|
316
|
+
out.push(209);
|
|
317
|
+
pushUint(out, value & 0xffffn, 2);
|
|
318
|
+
} else if (value >= -2147483648n) {
|
|
319
|
+
out.push(210);
|
|
320
|
+
pushUint(out, value & 0xffffffffn, 4);
|
|
321
|
+
} else {
|
|
322
|
+
out.push(211);
|
|
323
|
+
pushUint(out, value & UINT64_MAX, 8);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function packHeader(out, length, fixBase, fixMax, codes) {
|
|
327
|
+
if (length <= fixMax) {
|
|
328
|
+
out.push(fixBase | length);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const [c8, c16, c32] = codes;
|
|
332
|
+
if (c8 !== undefined && c8 !== 0 && length <= 255) {
|
|
333
|
+
out.push(c8);
|
|
334
|
+
pushUint(out, BigInt(length), 1);
|
|
335
|
+
} else if (length <= 65535) {
|
|
336
|
+
out.push(c16);
|
|
337
|
+
pushUint(out, BigInt(length), 2);
|
|
338
|
+
} else {
|
|
339
|
+
out.push(c32);
|
|
340
|
+
pushUint(out, BigInt(length), 4);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function packValue(out, value) {
|
|
344
|
+
if (value === null)
|
|
345
|
+
out.push(192);
|
|
346
|
+
else if (value === false)
|
|
347
|
+
out.push(194);
|
|
348
|
+
else if (value === true)
|
|
349
|
+
out.push(195);
|
|
350
|
+
else if (typeof value === "bigint")
|
|
351
|
+
packInteger(out, value);
|
|
352
|
+
else if (typeof value === "number") {
|
|
353
|
+
if (!Number.isSafeInteger(value))
|
|
354
|
+
throw new Error(`msgpack: ${value} is not a safe integer`);
|
|
355
|
+
packInteger(out, BigInt(value));
|
|
356
|
+
} else if (typeof value === "string") {
|
|
357
|
+
const bytes = new TextEncoder().encode(value);
|
|
358
|
+
packHeader(out, bytes.length, 160, 31, [217, 218, 219]);
|
|
359
|
+
for (const b of bytes)
|
|
360
|
+
out.push(b);
|
|
361
|
+
} else if (Array.isArray(value)) {
|
|
362
|
+
packHeader(out, value.length, 144, 15, [0, 220, 221]);
|
|
363
|
+
for (const item of value)
|
|
364
|
+
packValue(out, item);
|
|
365
|
+
} else if (typeof value === "object") {
|
|
366
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
367
|
+
for (const [key] of entries) {
|
|
368
|
+
if (/^(0|[1-9][0-9]*)$/.test(key))
|
|
369
|
+
throw new Error(`msgpack: integer-like key "${key}"`);
|
|
370
|
+
}
|
|
371
|
+
packHeader(out, entries.length, 128, 15, [0, 222, 223]);
|
|
372
|
+
for (const [key, item] of entries) {
|
|
373
|
+
packValue(out, key);
|
|
374
|
+
packValue(out, item);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
throw new Error(`msgpack: cannot encode a ${typeof value}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function msgpackEncode(value) {
|
|
381
|
+
const out = [];
|
|
382
|
+
packValue(out, value);
|
|
383
|
+
return Uint8Array.from(out);
|
|
384
|
+
}
|
|
385
|
+
function toHex(bytes) {
|
|
386
|
+
let out = "";
|
|
387
|
+
for (const b of bytes)
|
|
388
|
+
out += b.toString(16).padStart(2, "0");
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
function hyperliquidActionHash(action, nonce) {
|
|
392
|
+
const n = BigInt(nonce);
|
|
393
|
+
if (n < 0n || n > UINT64_MAX)
|
|
394
|
+
throw new Error("hyperliquidActionHash: nonce out of range");
|
|
395
|
+
const out = Array.from(msgpackEncode(action));
|
|
396
|
+
pushUint(out, n, 8);
|
|
397
|
+
out.push(0);
|
|
398
|
+
return `0x${toHex(keccak_256(Uint8Array.from(out)))}`;
|
|
399
|
+
}
|
|
400
|
+
function hyperliquidL1TypedData(connectionId, network) {
|
|
401
|
+
return {
|
|
402
|
+
domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS },
|
|
403
|
+
types: {
|
|
404
|
+
Agent: [
|
|
405
|
+
{ name: "source", type: "string" },
|
|
406
|
+
{ name: "connectionId", type: "bytes32" }
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
primary_type: "Agent",
|
|
410
|
+
message: { source: network === "mainnet" ? "a" : "b", connectionId }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function hyperliquidApproveBuilderFeeTypedData(builder, nonce) {
|
|
414
|
+
return {
|
|
415
|
+
domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS },
|
|
416
|
+
types: {
|
|
417
|
+
"HyperliquidTransaction:ApproveBuilderFee": [
|
|
418
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
419
|
+
{ name: "maxFeeRate", type: "string" },
|
|
420
|
+
{ name: "builder", type: "address" },
|
|
421
|
+
{ name: "nonce", type: "uint64" }
|
|
422
|
+
]
|
|
423
|
+
},
|
|
424
|
+
primary_type: "HyperliquidTransaction:ApproveBuilderFee",
|
|
425
|
+
message: {
|
|
426
|
+
hyperliquidChain: "Mainnet",
|
|
427
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
428
|
+
builder: builder.toLowerCase(),
|
|
429
|
+
nonce
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function canonical(value) {
|
|
434
|
+
if (value === null || typeof value !== "object")
|
|
435
|
+
return JSON.stringify(value);
|
|
436
|
+
if (Array.isArray(value))
|
|
437
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
438
|
+
const record = value;
|
|
439
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
|
|
440
|
+
}
|
|
441
|
+
function sameValue(a, b) {
|
|
442
|
+
return canonical(a) === canonical(b);
|
|
443
|
+
}
|
|
444
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
445
|
+
function checkBuilderField(action, builder) {
|
|
446
|
+
if (!("builder" in action) || action.builder === undefined)
|
|
447
|
+
return null;
|
|
448
|
+
const field = action.builder;
|
|
449
|
+
if (typeof field !== "object" || field === null)
|
|
450
|
+
return "builder field is not an object";
|
|
451
|
+
if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
|
|
452
|
+
return "the order names a builder that is not Candle's";
|
|
453
|
+
}
|
|
454
|
+
if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS) {
|
|
455
|
+
return "the builder fee is outside Candle's 0.1% maximum";
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
function decimal(value) {
|
|
460
|
+
if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
|
|
461
|
+
return null;
|
|
462
|
+
const negative = value.startsWith("-");
|
|
463
|
+
const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
|
|
464
|
+
const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
|
|
465
|
+
return negative ? -n : n;
|
|
466
|
+
}
|
|
467
|
+
function checkActionIntent(action, intent) {
|
|
468
|
+
const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
|
|
469
|
+
const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
|
|
470
|
+
if (!Array.isArray(orderList))
|
|
471
|
+
return "invalid orders";
|
|
472
|
+
for (const raw of orderList) {
|
|
473
|
+
if (!raw || typeof raw !== "object")
|
|
474
|
+
return "invalid order";
|
|
475
|
+
const o = raw;
|
|
476
|
+
if (!mainAsset(o.a))
|
|
477
|
+
return "order asset is outside the main perp universe";
|
|
478
|
+
if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
|
|
479
|
+
return "invalid order side, size, price or reduce-only flag";
|
|
480
|
+
}
|
|
481
|
+
if (action.type === "cancel") {
|
|
482
|
+
if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
|
|
483
|
+
return "invalid cancel asset";
|
|
484
|
+
}
|
|
485
|
+
if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
|
|
486
|
+
return "invalid action asset";
|
|
487
|
+
if (!intent)
|
|
488
|
+
return null;
|
|
489
|
+
const { method, params: p } = intent;
|
|
490
|
+
const types = {
|
|
491
|
+
setup: "approveBuilderFee",
|
|
492
|
+
open: "order",
|
|
493
|
+
close: "order",
|
|
494
|
+
cancel: "cancel",
|
|
495
|
+
modify: "modify",
|
|
496
|
+
leverage: "updateLeverage",
|
|
497
|
+
margin: "updateIsolatedMargin"
|
|
498
|
+
};
|
|
499
|
+
if (action.type !== types[method])
|
|
500
|
+
return "action type does not match the requested method";
|
|
501
|
+
if (method === "leverage") {
|
|
502
|
+
if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
|
|
503
|
+
return "leverage or mode does not match the request";
|
|
504
|
+
}
|
|
505
|
+
if (method === "margin") {
|
|
506
|
+
const amount = decimal(p.amount);
|
|
507
|
+
if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
|
|
508
|
+
return "isolated margin amount does not match the request";
|
|
509
|
+
}
|
|
510
|
+
if (method === "open" || method === "close" || method === "modify") {
|
|
511
|
+
const list = orderList;
|
|
512
|
+
const first = list[0];
|
|
513
|
+
if (!first)
|
|
514
|
+
return "missing requested order";
|
|
515
|
+
const typ = first.t;
|
|
516
|
+
if (!typ?.limit)
|
|
517
|
+
return "requested order is not a limit/IOC order";
|
|
518
|
+
if (method === "open") {
|
|
519
|
+
const buy = p.side === "long" || p.side === "buy";
|
|
520
|
+
if (first.b !== buy || first.r !== false || decimal(first.s) !== decimal(p.size))
|
|
521
|
+
return "order side, size or reduce-only flag does not match the request";
|
|
522
|
+
const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
|
|
523
|
+
if (typ.limit.tif !== tif)
|
|
524
|
+
return "order time in force does not match the request";
|
|
525
|
+
const triggers = [
|
|
526
|
+
["takeProfit", "tp"],
|
|
527
|
+
["stopLoss", "sl"]
|
|
528
|
+
].filter(([field]) => p[field] != null);
|
|
529
|
+
if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
|
|
530
|
+
return "unexpected extra orders or grouping";
|
|
531
|
+
for (let i = 0;i < triggers.length; i++) {
|
|
532
|
+
const [field, kind] = triggers[i];
|
|
533
|
+
const leg = list[i + 1];
|
|
534
|
+
if (!leg)
|
|
535
|
+
return "missing trigger order";
|
|
536
|
+
const trigger = leg.t?.trigger;
|
|
537
|
+
if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal(leg.s) !== decimal(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal(trigger.triggerPx) !== decimal(p[field]))
|
|
538
|
+
return "trigger side, size, reduce-only flag or price does not match the request";
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
if (list.length !== 1)
|
|
542
|
+
return "unexpected extra orders";
|
|
543
|
+
if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
|
|
544
|
+
return "close size or reduce-only flag does not match the request";
|
|
545
|
+
if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
|
|
546
|
+
return "replacement size does not match the request";
|
|
547
|
+
}
|
|
548
|
+
if (p.price != null && decimal(first.p) !== decimal(p.price))
|
|
549
|
+
return "order price does not match the request";
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
async function hyperliquidCloseOrder(fetcher, network, address, params) {
|
|
554
|
+
const read = async (body) => {
|
|
555
|
+
const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: { "content-type": "application/json" },
|
|
558
|
+
body: JSON.stringify(body)
|
|
559
|
+
});
|
|
560
|
+
if (!res.ok)
|
|
561
|
+
throw new Error("Cannot read the close position from Hyperliquid");
|
|
562
|
+
return JSON.parse(await res.text());
|
|
563
|
+
};
|
|
564
|
+
const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
|
|
565
|
+
const coin = String(params.coin).toUpperCase();
|
|
566
|
+
const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
|
|
567
|
+
const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
|
|
568
|
+
const signed = decimal(szi);
|
|
569
|
+
if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
|
|
570
|
+
throw new Error("No main-universe position to close");
|
|
571
|
+
const held = signed < 0n ? -signed : signed;
|
|
572
|
+
const asked = params.size == null ? held : decimal(params.size);
|
|
573
|
+
if (asked === null || asked <= 0n)
|
|
574
|
+
throw new Error("Invalid close size");
|
|
575
|
+
const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
|
|
576
|
+
return { asset, isBuy: signed < 0n, size };
|
|
577
|
+
}
|
|
578
|
+
function verifyPerpsBuild(build, opts) {
|
|
579
|
+
try {
|
|
580
|
+
const network = opts.network ?? "mainnet";
|
|
581
|
+
if (!EVM_ADDRESS_RE.test(opts.builder))
|
|
582
|
+
return { ok: false, reason: "no valid Candle builder address to check against" };
|
|
583
|
+
if (build.network !== undefined && build.network !== network) {
|
|
584
|
+
return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
|
|
585
|
+
}
|
|
586
|
+
if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
|
|
587
|
+
return { ok: false, reason: "the build is for a different wallet" };
|
|
588
|
+
}
|
|
589
|
+
if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
|
|
590
|
+
return { ok: false, reason: "invalid nonce" };
|
|
591
|
+
const typed = build.typedData;
|
|
592
|
+
if (!typed || typeof typed !== "object")
|
|
593
|
+
return { ok: false, reason: "no typed data" };
|
|
594
|
+
const action = build.action;
|
|
595
|
+
if (!action || typeof action !== "object")
|
|
596
|
+
return { ok: false, reason: "no action" };
|
|
597
|
+
const intentRefusal = checkActionIntent(action, opts.intent);
|
|
598
|
+
if (intentRefusal)
|
|
599
|
+
return { ok: false, reason: intentRefusal };
|
|
600
|
+
if (opts.closeOrder) {
|
|
601
|
+
const order = action.orders[0];
|
|
602
|
+
if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
|
|
603
|
+
return { ok: false, reason: "close side, asset or size does not match the requested position" };
|
|
604
|
+
}
|
|
605
|
+
if (typed.primary_type === "Agent") {
|
|
606
|
+
const type = action.type;
|
|
607
|
+
if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
|
|
608
|
+
return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
|
|
609
|
+
}
|
|
610
|
+
if (type === "order") {
|
|
611
|
+
const refusal = checkBuilderField(action, opts.builder);
|
|
612
|
+
if (refusal)
|
|
613
|
+
return { ok: false, reason: refusal };
|
|
614
|
+
} else if ("builder" in action) {
|
|
615
|
+
return { ok: false, reason: `a ${type} action carries no builder` };
|
|
616
|
+
}
|
|
617
|
+
if ("vaultAddress" in action)
|
|
618
|
+
return { ok: false, reason: "the action names a vault" };
|
|
619
|
+
const expected = hyperliquidL1TypedData(hyperliquidActionHash(action, build.nonce), network);
|
|
620
|
+
if (!sameValue(typed, expected)) {
|
|
621
|
+
return { ok: false, reason: "the typed data does not commit to this action and nonce" };
|
|
622
|
+
}
|
|
623
|
+
return { ok: true, kind: "l1" };
|
|
624
|
+
}
|
|
625
|
+
if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
|
|
626
|
+
const expected = hyperliquidApproveBuilderFeeTypedData(opts.builder, build.nonce);
|
|
627
|
+
if (!sameValue(typed, expected)) {
|
|
628
|
+
return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
|
|
629
|
+
}
|
|
630
|
+
const expectedAction = {
|
|
631
|
+
type: "approveBuilderFee",
|
|
632
|
+
hyperliquidChain: "Mainnet",
|
|
633
|
+
signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID,
|
|
634
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
635
|
+
builder: opts.builder.toLowerCase(),
|
|
636
|
+
nonce: build.nonce
|
|
637
|
+
};
|
|
638
|
+
if (!sameValue(action, expectedAction)) {
|
|
639
|
+
return { ok: false, reason: "the approval action does not match its typed data" };
|
|
640
|
+
}
|
|
641
|
+
return { ok: true, kind: "approveBuilderFee" };
|
|
642
|
+
}
|
|
643
|
+
return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
|
|
644
|
+
} catch (err) {
|
|
645
|
+
return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function hyperliquidSplitSignature(signature) {
|
|
649
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
650
|
+
throw new Error("Not a 65-byte signature");
|
|
651
|
+
let v = Number.parseInt(signature.slice(130, 132), 16);
|
|
652
|
+
if (v < 27)
|
|
653
|
+
v += 27;
|
|
654
|
+
return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
|
|
655
|
+
}
|
|
656
|
+
function hyperliquidExchangeBody(action, nonce, signature) {
|
|
657
|
+
return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
|
|
658
|
+
}
|
|
659
|
+
function hyperliquidRelayBody(typedData) {
|
|
660
|
+
return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
|
|
661
|
+
}
|
|
662
|
+
|
|
263
663
|
// src/internal/rpc-endpoint.ts
|
|
264
664
|
function describeRpcEndpoint(url) {
|
|
265
665
|
try {
|
|
@@ -474,6 +874,8 @@ class CandleClient {
|
|
|
474
874
|
secretStore;
|
|
475
875
|
solanaRpcUrl;
|
|
476
876
|
evmRpcUrl;
|
|
877
|
+
hyperliquidNetwork;
|
|
878
|
+
hyperliquidBuilder;
|
|
477
879
|
wallets = {
|
|
478
880
|
swapReceipt: async (hash) => {
|
|
479
881
|
this.requireKey("wallets.swapReceipt()");
|
|
@@ -509,6 +911,10 @@ class CandleClient {
|
|
|
509
911
|
this.solanaRpcUrl = opts.solanaRpcUrl;
|
|
510
912
|
if (opts.evmRpcUrl !== undefined)
|
|
511
913
|
this.evmRpcUrl = opts.evmRpcUrl;
|
|
914
|
+
this.hyperliquidNetwork = opts.hyperliquidNetwork ?? "mainnet";
|
|
915
|
+
const builder = opts.hyperliquidBuilder ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
|
|
916
|
+
if (builder)
|
|
917
|
+
this.hyperliquidBuilder = builder.toLowerCase();
|
|
512
918
|
}
|
|
513
919
|
async getQuotePairs(chain) {
|
|
514
920
|
const query = chain ? `?chain=${chain}` : "";
|
|
@@ -635,6 +1041,10 @@ class CandleClient {
|
|
|
635
1041
|
this.requireKey("getProfilePnl()");
|
|
636
1042
|
return this.requestJson("GET", `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl`);
|
|
637
1043
|
}
|
|
1044
|
+
async getPortfolio() {
|
|
1045
|
+
this.requireKey("getPortfolio()");
|
|
1046
|
+
return this.requestJson("GET", "/api/v1/agent/portfolio");
|
|
1047
|
+
}
|
|
638
1048
|
async setProfileWallets(keyPrefix, walletIds) {
|
|
639
1049
|
this.requireKey("setProfileWallets()");
|
|
640
1050
|
return this.requestJson("PUT", `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, { walletIds });
|
|
@@ -1026,6 +1436,171 @@ class CandleClient {
|
|
|
1026
1436
|
await waitForReceipt(params.rpc, txHash);
|
|
1027
1437
|
return txHash;
|
|
1028
1438
|
}
|
|
1439
|
+
async perpsConfig() {
|
|
1440
|
+
this.requireKey("perpsConfig()");
|
|
1441
|
+
return this.requestJson("GET", "/api/v1/agent/perps/config");
|
|
1442
|
+
}
|
|
1443
|
+
async perpsSetup(params) {
|
|
1444
|
+
this.requireKey("perpsSetup()");
|
|
1445
|
+
const res = await this.requestJson("POST", "/api/v1/agent/perps/setup", { walletId: params.walletId });
|
|
1446
|
+
const status = {
|
|
1447
|
+
walletId: res.walletId,
|
|
1448
|
+
address: res.address,
|
|
1449
|
+
network: res.network,
|
|
1450
|
+
mode: res.mode,
|
|
1451
|
+
standardMode: res.standardMode,
|
|
1452
|
+
accountValue: res.accountValue,
|
|
1453
|
+
withdrawable: res.withdrawable,
|
|
1454
|
+
builder: res.builder,
|
|
1455
|
+
approvedFeeTenthsBps: res.approvedFeeTenthsBps,
|
|
1456
|
+
ready: res.ready
|
|
1457
|
+
};
|
|
1458
|
+
if (res.ready)
|
|
1459
|
+
return status;
|
|
1460
|
+
return { ...status, action: await this.perpsComplete(res, params, "setup") };
|
|
1461
|
+
}
|
|
1462
|
+
async perpsOpen(params) {
|
|
1463
|
+
this.requireKey("perpsOpen()");
|
|
1464
|
+
const { walletId, privyWalletId: _p, submit: _s, ...order } = params;
|
|
1465
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/open", { walletId, ...order });
|
|
1466
|
+
return this.perpsComplete(build, params, "open");
|
|
1467
|
+
}
|
|
1468
|
+
async perpsClose(params) {
|
|
1469
|
+
this.requireKey("perpsClose()");
|
|
1470
|
+
const { walletId, privyWalletId: _p, submit: _s, ...close } = params;
|
|
1471
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/close", { walletId, ...close });
|
|
1472
|
+
return this.perpsComplete(build, params, "close");
|
|
1473
|
+
}
|
|
1474
|
+
async perpsCancel(params) {
|
|
1475
|
+
this.requireKey("perpsCancel()");
|
|
1476
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/cancel", {
|
|
1477
|
+
walletId: params.walletId,
|
|
1478
|
+
cloid: params.cloid
|
|
1479
|
+
});
|
|
1480
|
+
return this.perpsComplete(build, params, "cancel");
|
|
1481
|
+
}
|
|
1482
|
+
async perpsModify(params) {
|
|
1483
|
+
this.requireKey("perpsModify()");
|
|
1484
|
+
const { walletId, privyWalletId: _p, submit: _s, ...modify } = params;
|
|
1485
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/modify", { walletId, ...modify });
|
|
1486
|
+
return this.perpsComplete(build, params, "modify");
|
|
1487
|
+
}
|
|
1488
|
+
async perpsLeverage(params) {
|
|
1489
|
+
this.requireKey("perpsLeverage()");
|
|
1490
|
+
const { walletId, privyWalletId: _p, submit: _s, ...leverage } = params;
|
|
1491
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/leverage", { walletId, ...leverage });
|
|
1492
|
+
return this.perpsComplete(build, params, "leverage");
|
|
1493
|
+
}
|
|
1494
|
+
async perpsMargin(params) {
|
|
1495
|
+
this.requireKey("perpsMargin()");
|
|
1496
|
+
const build = await this.requestJson("POST", "/api/v1/agent/perps/margin", {
|
|
1497
|
+
walletId: params.walletId,
|
|
1498
|
+
coin: params.coin,
|
|
1499
|
+
amount: params.amount
|
|
1500
|
+
});
|
|
1501
|
+
return this.perpsComplete(build, params, "margin");
|
|
1502
|
+
}
|
|
1503
|
+
async perpsPositions(walletId) {
|
|
1504
|
+
this.requireKey("perpsPositions()");
|
|
1505
|
+
return this.requestJson("GET", `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(walletId)}`);
|
|
1506
|
+
}
|
|
1507
|
+
async perpsOrders(walletId, opts = {}) {
|
|
1508
|
+
this.requireKey("perpsOrders()");
|
|
1509
|
+
const params = new URLSearchParams({ walletId });
|
|
1510
|
+
if (opts.limit !== undefined)
|
|
1511
|
+
params.set("limit", String(opts.limit));
|
|
1512
|
+
return this.requestJson("GET", `/api/v1/agent/perps/orders?${params}`);
|
|
1513
|
+
}
|
|
1514
|
+
async perpsFills(walletId) {
|
|
1515
|
+
this.requireKey("perpsFills()");
|
|
1516
|
+
return this.requestJson("GET", `/api/v1/agent/perps/fills?walletId=${encodeURIComponent(walletId)}`);
|
|
1517
|
+
}
|
|
1518
|
+
async perpsFunding(walletId, startTime) {
|
|
1519
|
+
this.requireKey("perpsFunding()");
|
|
1520
|
+
const params = new URLSearchParams({ walletId });
|
|
1521
|
+
if (startTime !== undefined)
|
|
1522
|
+
params.set("startTime", String(startTime));
|
|
1523
|
+
return this.requestJson("GET", `/api/v1/agent/perps/funding?${params}`);
|
|
1524
|
+
}
|
|
1525
|
+
async perpsBuilder() {
|
|
1526
|
+
if (this.hyperliquidBuilder)
|
|
1527
|
+
return this.hyperliquidBuilder;
|
|
1528
|
+
const config = await this.perpsConfig();
|
|
1529
|
+
if (!config.builder)
|
|
1530
|
+
throw new Error("perps: the server reports no Hyperliquid builder address");
|
|
1531
|
+
this.hyperliquidBuilder = config.builder.toLowerCase();
|
|
1532
|
+
return this.hyperliquidBuilder;
|
|
1533
|
+
}
|
|
1534
|
+
async perpsComplete(build, ref, method) {
|
|
1535
|
+
const check = verifyPerpsBuild(build, {
|
|
1536
|
+
builder: await this.perpsBuilder(),
|
|
1537
|
+
network: this.hyperliquidNetwork,
|
|
1538
|
+
intent: { method, params: ref }
|
|
1539
|
+
});
|
|
1540
|
+
if (!check.ok)
|
|
1541
|
+
throw new Error(`perps: refused to sign this build: ${check.reason}`);
|
|
1542
|
+
if (method === "close") {
|
|
1543
|
+
const closeOrder = await hyperliquidCloseOrder(this.fetchImpl, this.hyperliquidNetwork, build.address, ref);
|
|
1544
|
+
const closeCheck = verifyPerpsBuild(build, {
|
|
1545
|
+
builder: await this.perpsBuilder(),
|
|
1546
|
+
network: this.hyperliquidNetwork,
|
|
1547
|
+
intent: { method, params: ref },
|
|
1548
|
+
closeOrder
|
|
1549
|
+
});
|
|
1550
|
+
if (!closeCheck.ok)
|
|
1551
|
+
throw new Error(`perps: refused to sign this build: ${closeCheck.reason}`);
|
|
1552
|
+
}
|
|
1553
|
+
if (ref.submit === false)
|
|
1554
|
+
return { build, signature: null, submitted: false, exchange: null };
|
|
1555
|
+
const signature = await this.signLinkedTypedData({
|
|
1556
|
+
linkedWalletId: ref.walletId,
|
|
1557
|
+
privyWalletId: ref.privyWalletId,
|
|
1558
|
+
typedData: build.typedData
|
|
1559
|
+
});
|
|
1560
|
+
try {
|
|
1561
|
+
const res = await this.fetchImpl(HYPERLIQUID_EXCHANGE_URLS[this.hyperliquidNetwork], {
|
|
1562
|
+
method: "POST",
|
|
1563
|
+
headers: { "content-type": "application/json" },
|
|
1564
|
+
body: JSON.stringify(hyperliquidExchangeBody(build.action, build.nonce, signature))
|
|
1565
|
+
});
|
|
1566
|
+
const text = await res.text();
|
|
1567
|
+
if (!res.ok) {
|
|
1568
|
+
return { build, signature, submitted: false, exchange: null, submitError: `HTTP ${res.status}: ${text}` };
|
|
1569
|
+
}
|
|
1570
|
+
return { build, signature, submitted: true, exchange: JSON.parse(text) };
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
return {
|
|
1573
|
+
build,
|
|
1574
|
+
signature,
|
|
1575
|
+
submitted: false,
|
|
1576
|
+
exchange: null,
|
|
1577
|
+
submitError: err instanceof Error ? err.message : String(err)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async signLinkedTypedData(params) {
|
|
1582
|
+
if (!this.privyAppId) {
|
|
1583
|
+
throw new Error("perps signing requires privyAppId in CandleClientOptions (the relay's Privy app id)");
|
|
1584
|
+
}
|
|
1585
|
+
if (!this.secretStore)
|
|
1586
|
+
throw new Error("perps signing requires a secretStore in CandleClientOptions");
|
|
1587
|
+
const privateKeyPem = await this.secretStore.get(params.linkedWalletId);
|
|
1588
|
+
if (!privateKeyPem) {
|
|
1589
|
+
throw new Error(`perps: no signer key stored for linked wallet "${params.linkedWalletId}"`);
|
|
1590
|
+
}
|
|
1591
|
+
const body = hyperliquidRelayBody(params.typedData);
|
|
1592
|
+
const authorizationSignature = await buildPrivyAuthorizationSignature({
|
|
1593
|
+
privateKeyPem,
|
|
1594
|
+
privyWalletId: params.privyWalletId,
|
|
1595
|
+
appId: this.privyAppId,
|
|
1596
|
+
body
|
|
1597
|
+
});
|
|
1598
|
+
const res = await this.requestJson("POST", `/api/v1/agent/wallets/${encodeURIComponent(params.linkedWalletId)}/sign`, { authorizationSignature, body });
|
|
1599
|
+
if (typeof res.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(res.signature)) {
|
|
1600
|
+
throw new Error("perps: the relay returned no signature");
|
|
1601
|
+
}
|
|
1602
|
+
return res.signature;
|
|
1603
|
+
}
|
|
1029
1604
|
requireKey(method) {
|
|
1030
1605
|
if (!this.apiKey) {
|
|
1031
1606
|
throw new Error(`${method} requires an apiKey: pass one in CandleClientOptions (new CandleClient({ apiKey }))`);
|
|
@@ -1109,7 +1684,7 @@ function generateClientLaunchId() {
|
|
|
1109
1684
|
function generateClientTradeId() {
|
|
1110
1685
|
return generateSdkId();
|
|
1111
1686
|
}
|
|
1112
|
-
var SDK_VERSION = "0.4.
|
|
1687
|
+
var SDK_VERSION = "0.4.6";
|
|
1113
1688
|
var sdkUpdateWarned = false;
|
|
1114
1689
|
function noteLatestSdkVersion(value) {
|
|
1115
1690
|
if (sdkUpdateWarned || !value || !/^\d+\.\d+\.\d+$/.test(value))
|
|
@@ -1394,14 +1969,19 @@ function verifyWebhookSignature(secret, header, body, nowSec, toleranceSec = 300
|
|
|
1394
1969
|
}
|
|
1395
1970
|
export {
|
|
1396
1971
|
verifyWebhookSignature,
|
|
1972
|
+
verifyPerpsBuild,
|
|
1397
1973
|
isSolanaRpcErrorData,
|
|
1974
|
+
hyperliquidActionHash,
|
|
1398
1975
|
generateSignerKeypair,
|
|
1399
1976
|
encryptWalletKeyForImport,
|
|
1400
1977
|
KeychainSecretStore,
|
|
1401
1978
|
JsonRpcError,
|
|
1402
1979
|
InMemorySecretStore,
|
|
1980
|
+
HYPERLIQUID_EXCHANGE_URLS,
|
|
1981
|
+
HYPERLIQUID_ALLOWED_ACTION_TYPES,
|
|
1403
1982
|
EncryptedFileSecretStore,
|
|
1404
1983
|
CandleClient,
|
|
1405
1984
|
CandleApiError,
|
|
1985
|
+
CANDLE_HYPERLIQUID_BUILDER_ADDRESS,
|
|
1406
1986
|
BRIDGE_ERROR_CODES
|
|
1407
1987
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@candledottv/agent-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "TypeScript SDK for the Candle agent rail: typed headless launches, market reads, and webhook verification",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@hpke/chacha20poly1305": "^1.6.2",
|
|
51
51
|
"@hpke/core": "^1.7.2",
|
|
52
|
+
"@noble/hashes": "1.8.0",
|
|
52
53
|
"@scure/base": "^1.2.6",
|
|
53
54
|
"canonicalize": "^2.1.0"
|
|
54
55
|
},
|