@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/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.20' });
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: MASTER signs+POST approveAgent + ApproveBuilderFee once → next hl_balances → then hl_place_order sizeUsd:12.',
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). Prefer sizeUsd:12. Errors: approve missing / no margin / wrong net / retry place.',
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 — auto mark + size round (e.g. 12)'),
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('Limit px; omit with sizeUsd to use mark'),
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',
@@ -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: 'Gtc' } } });
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,
@@ -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
- X3.signer = { ready, lastError, selfTest, signL1, userTypedData, assertDeterministicHash, addressFromPrivateKey: (pk) => sdk().addressFromPrivateKey(pk) };
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) {}