@hypelens/hypelens-agent-rail 0.1.20 → 0.1.22
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/README.md +27 -26
- package/package.json +2 -2
- package/skill/SKILL.md.bak.aside +70 -0
- package/skill/hypelens-agent-rail/SKILL.md +15 -53
- package/skill/hypelens-agent-rail/SKILL.md.bak-pre-conversion +59 -0
- package/src/core.js +52 -20
- package/src/exchange.js +261 -37
- package/src/exchange.js.bak-clearerr +211 -0
- package/src/index.js +1 -1
- package/src/mcp.js +17 -10
- package/vendor/hl-actions.js +5 -3
- package/vendor/hl-actions.js.bak +116 -0
- package/vendor/hl-sdk.js +13 -0
- package/vendor/hl-signer.js +11 -2
package/src/mcp.js
CHANGED
|
@@ -4,7 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { walls, cascade, pretradeCheck, whaleBook, quickstart } from './core.js';
|
|
7
|
-
import { status, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition } from './exchange.js';
|
|
7
|
+
import { status, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee } from './exchange.js';
|
|
8
8
|
|
|
9
9
|
const j = (v) => ({ content: [{ type: 'text', text: JSON.stringify(v, null, 1) }] });
|
|
10
10
|
const wrap = (fn) => async (args) => {
|
|
@@ -13,7 +13,7 @@ const wrap = (fn) => async (args) => {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const heatAfterPlace = (fn) => async (args) => {
|
|
16
|
-
if (!process.env.HYPELENS_AGENT_PK) {
|
|
16
|
+
if (!(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY)) {
|
|
17
17
|
return j({
|
|
18
18
|
deferred: true,
|
|
19
19
|
reason: 'setup_incomplete',
|
|
@@ -32,7 +32,7 @@ const heatAfterPlace = (fn) => async (args) => {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export async function main() {
|
|
35
|
-
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.
|
|
35
|
+
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.22' });
|
|
36
36
|
|
|
37
37
|
server.tool('hl_quickstart',
|
|
38
38
|
'CALL FIRST: checklist wallet→approve→hl_balances(equity>0 / fundHow if 0)→hl_place_order sizeUsd mainnet when user asks to trade. No heat-first.',
|
|
@@ -48,10 +48,15 @@ export async function main() {
|
|
|
48
48
|
{}, wrap(() => newAgentWallet()));
|
|
49
49
|
|
|
50
50
|
server.tool('hl_approve_payloads',
|
|
51
|
-
'Setup:
|
|
51
|
+
'Setup: OpenBroker — if HYPERLIQUID_PRIVATE_KEY/HYPELENS_MASTER_PK set, auto ApproveBuilderFee; else return clear MASTER payloads + maxBuilderFee. Next hl_balances → place.',
|
|
52
52
|
{ agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
53
53
|
wrap(({ agentAddress }) => approvePayloads(agentAddress)));
|
|
54
54
|
|
|
55
|
+
server.tool('hl_builder_fee_status',
|
|
56
|
+
'Check on-chain maxBuilderFee for builder 0x9548… (need >=10 tenths-bp). Place refuses until approved.',
|
|
57
|
+
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
58
|
+
wrap((a) => checkMaxBuilderFee(a)));
|
|
59
|
+
|
|
55
60
|
server.tool('hl_balances',
|
|
56
61
|
'Fund check before place. If equity=0 returns fundHow.depositAddress — ask user to deposit USDC, then recheck. If equity>0 nextTool=hl_place_order (only with user trading intent).',
|
|
57
62
|
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
@@ -63,16 +68,18 @@ export async function main() {
|
|
|
63
68
|
wrap((a) => getPositions(a)));
|
|
64
69
|
|
|
65
70
|
server.tool('hl_place_order',
|
|
66
|
-
'PLACE mainnet perps (paying path).
|
|
71
|
+
'PLACE mainnet perps (paying path). ALWAYS attaches builder 1bp. Requires confirmTrade:true (or intent trade/place/execute). Refuses if maxBuilderFee not approved (no silent $0). Prefer sizeUsd:12 IOC.',
|
|
67
72
|
{
|
|
68
73
|
coin: z.string().describe('e.g. BTC'),
|
|
69
74
|
isBuy: z.boolean(),
|
|
70
|
-
sizeUsd: z.number().positive().optional().describe('Preferred first-place notional USD —
|
|
75
|
+
sizeUsd: z.number().positive().optional().describe('Preferred first-place notional USD — IOC @ mark±0.5% slip + size round (e.g. 12)'),
|
|
71
76
|
size: z.number().positive().optional().describe('Coin units; optional if sizeUsd set'),
|
|
72
|
-
entryPx: z.number().positive().optional().describe('
|
|
77
|
+
entryPx: z.number().positive().optional().describe('GTC limit px; omit for IOC market (~0.5% slip from mark)'),
|
|
73
78
|
slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
|
|
74
79
|
leverage: z.number().positive().optional().describe('Optional; enables advisory pre-trade risk check (first place: 2)'),
|
|
75
|
-
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
|
|
80
|
+
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional(),
|
|
81
|
+
confirmTrade: z.boolean().optional().describe('Required true after explicit user trade/place/execute intent'),
|
|
82
|
+
intent: z.enum(['trade', 'place', 'execute', 'buy', 'sell']).optional()
|
|
76
83
|
},
|
|
77
84
|
wrap((a) => placeOrder(a)));
|
|
78
85
|
|
|
@@ -82,8 +89,8 @@ export async function main() {
|
|
|
82
89
|
wrap((a) => cancelOrder(a)));
|
|
83
90
|
|
|
84
91
|
server.tool('hl_close_position',
|
|
85
|
-
'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill.',
|
|
86
|
-
{ coin: z.string(), size: z.number().positive().optional(), px: z.number().positive().optional(), user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
92
|
+
'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill. Pass confirmTrade:true.',
|
|
93
|
+
{ coin: z.string(), size: z.number().positive().optional(), px: z.number().positive().optional(), user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(), confirmTrade: z.boolean().optional(), intent: z.enum(['trade', 'place', 'execute', 'close']).optional() },
|
|
87
94
|
wrap((a) => closePosition(a)));
|
|
88
95
|
|
|
89
96
|
server.tool('hl_walls',
|
package/vendor/hl-actions.js
CHANGED
|
@@ -82,15 +82,17 @@
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ==== L1 (agent-signed) ORDER action with normalTpsl grouping + builder ====
|
|
85
|
-
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx? }
|
|
85
|
+
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx?, tif? }
|
|
86
86
|
function buildOrderAction(plan) {
|
|
87
87
|
if (plan.assetIndex == null || plan.assetIndex < 0) throw new Error('bad assetIndex');
|
|
88
88
|
if (!(plan.size > 0)) throw new Error('bad size');
|
|
89
89
|
const szDec = plan.szDecimals | 0;
|
|
90
90
|
const s = sizeToWire(plan.size, szDec);
|
|
91
|
+
const rawTif = String(plan.tif || 'Gtc');
|
|
92
|
+
const tif = /^ioc$/i.test(rawTif) ? 'Ioc' : 'Gtc';
|
|
91
93
|
const orders = [];
|
|
92
|
-
// 1) entry — GTC limit
|
|
93
|
-
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif:
|
|
94
|
+
// 1) entry — IOC when plan.tif=Ioc (market/sizeUsd), else GTC limit
|
|
95
|
+
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif: tif } } });
|
|
94
96
|
// 2) SL — reduceOnly stop-market trigger (opposite side)
|
|
95
97
|
if (plan.slPx != null) {
|
|
96
98
|
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.slPx, szDec), s, r: true,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// HypeLens Module 3 — Hyperliquid EXCHANGE action builders (PURE, no signing).
|
|
2
|
+
// -----------------------------------------------------------------------------
|
|
3
|
+
// Testnet-first. The BUILDER address is PINNED here and must NEVER be read from
|
|
4
|
+
// the page. All numeric normalization (float_to_wire, szDecimals) lives here so
|
|
5
|
+
// it can be unit-tested; the vendored SDK's actionSorter still owns msgpack key
|
|
6
|
+
// order for the hash. Exposes window.HLX3.actions.
|
|
7
|
+
(function (g) {
|
|
8
|
+
'use strict';
|
|
9
|
+
const X3 = g.HLX3 = g.HLX3 || {};
|
|
10
|
+
|
|
11
|
+
// --- HARD BLOCK: mainnet placement is DISABLED in code until testnet proof +
|
|
12
|
+
// an explicit, separate operator sign-off. While false, the mainnet network
|
|
13
|
+
// option is hidden, setNet('mainnet') is refused, and any mainnet /exchange
|
|
14
|
+
// POST is rejected (defense-in-depth in the background too). ---
|
|
15
|
+
const MAINNET_PLACEMENT_ENABLED = false;
|
|
16
|
+
|
|
17
|
+
// --- PINNED constants (never sourced from the page) ---
|
|
18
|
+
const BUILDER = '0x9548B8E9554a1968843B3C380431b10996247c88'; // HypeLens builder
|
|
19
|
+
const BUILDER_F = 20; // f=20 tenths-of-a-bp = 2bp = 0.02% (f ≤ 100 perps)
|
|
20
|
+
const MAX_BUILDER_FEE_RATE = '0.02%'; // approveBuilderFee maxFeeRate
|
|
21
|
+
const AGENT_NAME = 'hypelens';
|
|
22
|
+
const SIGNATURE_CHAIN_ID = '0x66eee'; // 421614 (Arbitrum Sepolia) for user-signed actions
|
|
23
|
+
const EIP712_DOMAIN = { name: 'HyperliquidSignTransaction', version: '1', chainId: 421614, verifyingContract: '0x0000000000000000000000000000000000000000' };
|
|
24
|
+
|
|
25
|
+
const NET = {
|
|
26
|
+
testnet: { chain: 'Testnet', source: 'b', exchange: 'https://api.hyperliquid-testnet.xyz/exchange', info: 'https://api.hyperliquid-testnet.xyz/info' },
|
|
27
|
+
mainnet: { chain: 'Mainnet', source: 'a', exchange: 'https://api.hyperliquid.xyz/exchange', info: 'https://api.hyperliquid.xyz/info' }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// strictly-increasing millisecond nonce
|
|
31
|
+
let _lastNonce = 0;
|
|
32
|
+
function nonce() { let n = Date.now(); if (n <= _lastNonce) n = _lastNonce + 1; _lastNonce = n; return n; }
|
|
33
|
+
|
|
34
|
+
// ---- float_to_wire: no trailing zeros, ≤5 significant figures, integer-safe ----
|
|
35
|
+
// HL rule: prices ≤5 sig figs; perp price decimals ≤ (6 - szDecimals); size to szDecimals.
|
|
36
|
+
function floatToWire(x) {
|
|
37
|
+
if (x == null || typeof x !== 'number' || !isFinite(x)) throw new Error('floatToWire: not a finite number: ' + x);
|
|
38
|
+
if (x === 0) return '0';
|
|
39
|
+
// 5 significant figures, then trim to 8 decimals max, strip trailing zeros.
|
|
40
|
+
const rounded = parseFloat(x.toPrecision(5));
|
|
41
|
+
let s = rounded.toFixed(8);
|
|
42
|
+
s = s.replace(/0+$/, '').replace(/\.$/, '');
|
|
43
|
+
if (s === '-0') s = '0';
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
function roundToDecimals(x, decimals) { const f = Math.pow(10, decimals); return Math.round(x * f) / f; }
|
|
47
|
+
// size wire: round to szDecimals then float_to_wire. A positive size that
|
|
48
|
+
// rounds to '0' would be silently rejected (or worse) — throw instead.
|
|
49
|
+
function sizeToWire(sz, szDecimals) {
|
|
50
|
+
const d = Math.max(0, szDecimals | 0);
|
|
51
|
+
const wire = floatToWire(roundToDecimals(Number(sz), d));
|
|
52
|
+
if (Number(sz) > 0 && wire === '0') throw new Error('size rounds to zero at ' + d + ' decimals — increase size');
|
|
53
|
+
return wire;
|
|
54
|
+
}
|
|
55
|
+
// price wire: ≤5 sig figs AND ≤ (6 - szDecimals) decimals (perps), then float_to_wire
|
|
56
|
+
function priceToWire(px, szDecimals) {
|
|
57
|
+
const maxDec = Math.max(0, 6 - (szDecimals | 0));
|
|
58
|
+
const five = parseFloat(Number(px).toPrecision(5));
|
|
59
|
+
return floatToWire(roundToDecimals(five, maxDec));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isAddr(a) { return typeof a === 'string' && /^0x[0-9a-fA-F]{40}$/.test(a); }
|
|
63
|
+
|
|
64
|
+
// ==== USER-SIGNED actions (master wallet, EIP-712) ====
|
|
65
|
+
function buildApproveAgent(net, agentAddress) {
|
|
66
|
+
const N = NET[net]; if (!N) throw new Error('bad net'); if (!isAddr(agentAddress)) throw new Error('bad agentAddress');
|
|
67
|
+
const action = { type: 'approveAgent', hyperliquidChain: N.chain, signatureChainId: SIGNATURE_CHAIN_ID, agentAddress, agentName: AGENT_NAME, nonce: nonce() };
|
|
68
|
+
const types = { 'HyperliquidTransaction:ApproveAgent': [
|
|
69
|
+
{ name: 'hyperliquidChain', type: 'string' }, { name: 'agentAddress', type: 'address' },
|
|
70
|
+
{ name: 'agentName', type: 'string' }, { name: 'nonce', type: 'uint64' }
|
|
71
|
+
] };
|
|
72
|
+
return { action, types, primaryType: 'HyperliquidTransaction:ApproveAgent', domain: EIP712_DOMAIN };
|
|
73
|
+
}
|
|
74
|
+
function buildApproveBuilderFee(net) {
|
|
75
|
+
const N = NET[net]; if (!N) throw new Error('bad net');
|
|
76
|
+
const action = { type: 'approveBuilderFee', hyperliquidChain: N.chain, signatureChainId: SIGNATURE_CHAIN_ID, maxFeeRate: MAX_BUILDER_FEE_RATE, builder: BUILDER, nonce: nonce() };
|
|
77
|
+
const types = { 'HyperliquidTransaction:ApproveBuilderFee': [
|
|
78
|
+
{ name: 'hyperliquidChain', type: 'string' }, { name: 'maxFeeRate', type: 'string' },
|
|
79
|
+
{ name: 'builder', type: 'address' }, { name: 'nonce', type: 'uint64' }
|
|
80
|
+
] };
|
|
81
|
+
return { action, types, primaryType: 'HyperliquidTransaction:ApproveBuilderFee', domain: EIP712_DOMAIN };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ==== L1 (agent-signed) ORDER action with normalTpsl grouping + builder ====
|
|
85
|
+
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx? }
|
|
86
|
+
function buildOrderAction(plan) {
|
|
87
|
+
if (plan.assetIndex == null || plan.assetIndex < 0) throw new Error('bad assetIndex');
|
|
88
|
+
if (!(plan.size > 0)) throw new Error('bad size');
|
|
89
|
+
const szDec = plan.szDecimals | 0;
|
|
90
|
+
const s = sizeToWire(plan.size, szDec);
|
|
91
|
+
const orders = [];
|
|
92
|
+
// 1) entry — GTC limit
|
|
93
|
+
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif: 'Gtc' } } });
|
|
94
|
+
// 2) SL — reduceOnly stop-market trigger (opposite side)
|
|
95
|
+
if (plan.slPx != null) {
|
|
96
|
+
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.slPx, szDec), s, r: true,
|
|
97
|
+
t: { trigger: { isMarket: true, triggerPx: priceToWire(plan.slPx, szDec), tpsl: 'sl' } } });
|
|
98
|
+
}
|
|
99
|
+
// 3) TP — reduceOnly take-profit trigger (opposite side)
|
|
100
|
+
if (plan.tpPx != null) {
|
|
101
|
+
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.tpPx, szDec), s, r: true,
|
|
102
|
+
t: { trigger: { isMarket: true, triggerPx: priceToWire(plan.tpPx, szDec), tpsl: 'tp' } } });
|
|
103
|
+
}
|
|
104
|
+
const grouping = (plan.slPx != null || plan.tpPx != null) ? 'normalTpsl' : 'na';
|
|
105
|
+
return { type: 'order', orders, grouping, builder: { b: BUILDER.toLowerCase(), f: BUILDER_F } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
X3.actions = {
|
|
109
|
+
MAINNET_PLACEMENT_ENABLED,
|
|
110
|
+
BUILDER, BUILDER_F, MAX_BUILDER_FEE_RATE, AGENT_NAME, SIGNATURE_CHAIN_ID, NET, EIP712_DOMAIN,
|
|
111
|
+
nonce, floatToWire, sizeToWire, priceToWire, roundToDecimals, isAddr,
|
|
112
|
+
buildApproveAgent, buildApproveBuilderFee, buildOrderAction
|
|
113
|
+
};
|
|
114
|
+
// CommonJS export so the wire math can be unit-tested under node.
|
|
115
|
+
try { if (typeof module !== 'undefined' && module.exports) module.exports = X3.actions; } catch (e) {}
|
|
116
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
package/vendor/hl-sdk.js
CHANGED
|
@@ -9171,6 +9171,19 @@
|
|
|
9171
9171
|
message
|
|
9172
9172
|
};
|
|
9173
9173
|
},
|
|
9174
|
+
async signUserSigned(pk, action) {
|
|
9175
|
+
const wallet = privateKeyToAccount(pk);
|
|
9176
|
+
const typed = this.userSignedTypedData(action, action.signatureChainId);
|
|
9177
|
+
const types = { ...typed.types };
|
|
9178
|
+
delete types.EIP712Domain;
|
|
9179
|
+
return await signTypedData2({
|
|
9180
|
+
wallet,
|
|
9181
|
+
domain: typed.domain,
|
|
9182
|
+
types,
|
|
9183
|
+
primaryType: typed.primaryType,
|
|
9184
|
+
message: typed.message
|
|
9185
|
+
});
|
|
9186
|
+
},
|
|
9174
9187
|
orderToWire(order, szDecimals) {
|
|
9175
9188
|
const w = {
|
|
9176
9189
|
a: order.a,
|
package/vendor/hl-signer.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
const X3 = g.HLX3 = g.HLX3 || {};
|
|
11
11
|
|
|
12
|
-
const SDK_METHODS = ['randomPrivateKey', 'addressFromPrivateKey', 'hashL1Action', 'signL1Action', 'userSignedTypedData', 'orderToWire'];
|
|
12
|
+
const SDK_METHODS = ['randomPrivateKey', 'addressFromPrivateKey', 'hashL1Action', 'signL1Action', 'userSignedTypedData', 'signUserSigned', 'orderToWire'];
|
|
13
13
|
function sdk() { const s = g.HLSDK; if (!s) throw new Error('signing SDK not vendored — placement disabled'); return s; }
|
|
14
14
|
|
|
15
15
|
// SELF-TEST (runs at load): all 6 adapter methods present + hashL1Action is
|
|
@@ -61,7 +61,16 @@
|
|
|
61
61
|
return { domain: built.domain, types: built.types, primaryType: built.primaryType, message: built.action };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// MASTER-wallet EIP-712 sign for approveAgent / approveBuilderFee (OpenBroker).
|
|
65
|
+
async function signUserSigned(privateKey, action) {
|
|
66
|
+
const s = sdk();
|
|
67
|
+
if (typeof s.signUserSigned !== 'function') throw new Error('HLSDK.signUserSigned missing — rebuild vendor');
|
|
68
|
+
const signature = await s.signUserSigned(privateKey, action);
|
|
69
|
+
if (!signature || signature.r == null || signature.s == null || signature.v == null) throw new Error('SDK returned an invalid user signature');
|
|
70
|
+
return { signature, action, nonce: action.nonce };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
X3.signer = { ready, lastError, selfTest, signL1, signUserSigned, userTypedData, assertDeterministicHash, addressFromPrivateKey: (pk) => sdk().addressFromPrivateKey(pk) };
|
|
65
74
|
// Run the self-test once at load and surface the result in the console so a
|
|
66
75
|
// broken/absent SDK is obvious. Placement stays fail-closed on failure.
|
|
67
76
|
try { const r = selfTest(); if (r.ok) console.log('[HypeLens] signing SDK self-test PASSED (hash', r.hash.slice(0, 10) + '…)'); else console.warn('[HypeLens] signing SDK self-test FAILED —', r.error); } catch (e) {}
|