@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/exchange.js
CHANGED
|
@@ -8,9 +8,26 @@ import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
|
|
|
8
8
|
|
|
9
9
|
const net = () => process.env.HYPELENS_NET || 'mainnet';
|
|
10
10
|
|
|
11
|
+
/** Master / sole-wallet key (OpenBroker): auto ApproveBuilderFee. Never the builder sink. */
|
|
12
|
+
function masterPk() {
|
|
13
|
+
return process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY || '';
|
|
14
|
+
}
|
|
15
|
+
/** Agent place key; falls back to HYPERLIQUID_PRIVATE_KEY for fresh-wallet OpenBroker mode. */
|
|
16
|
+
function placePk() {
|
|
17
|
+
return process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY || '';
|
|
18
|
+
}
|
|
19
|
+
const FORBIDDEN_BUILDER = '0x9548B8E9554a1968843B3C380431b10996247c88';
|
|
20
|
+
function assertNotBuilderWallet(addr, label) {
|
|
21
|
+
if (addr && addr.toLowerCase() === FORBIDDEN_BUILDER.toLowerCase()) {
|
|
22
|
+
throw new Error('Refusing to trade/farm ' + label + ' ' + FORBIDDEN_BUILDER + ' — builder fee sink only, never a trading wallet');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
export function status() {
|
|
12
27
|
const { actions, signer } = loadShipped();
|
|
13
28
|
const st = signer.selfTest();
|
|
29
|
+
const hasPlace = Boolean(placePk());
|
|
30
|
+
const hasMaster = Boolean(masterPk());
|
|
14
31
|
return {
|
|
15
32
|
net: net(),
|
|
16
33
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
@@ -18,54 +35,185 @@ export function status() {
|
|
|
18
35
|
feeModel: '1bp on fills only — no subscription',
|
|
19
36
|
signerReady: st.ok, signerError: st.ok ? null : st.error,
|
|
20
37
|
hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
|
|
38
|
+
hasMasterKey: hasMaster,
|
|
39
|
+
hasPlaceKey: hasPlace,
|
|
21
40
|
fullFeedConfigured: fullFeedConfigured(),
|
|
22
41
|
first_success: 'mainnet place',
|
|
23
|
-
nextTool:
|
|
24
|
-
next:
|
|
25
|
-
?
|
|
42
|
+
nextTool: hasPlace ? 'hl_approve_payloads' : 'hl_new_agent_wallet',
|
|
43
|
+
next: hasPlace
|
|
44
|
+
? (hasMaster
|
|
45
|
+
? 'hl_approve_payloads(agentAddress) — master key present: auto ApproveBuilderFee then hl_balances'
|
|
46
|
+
: 'hl_approve_payloads(agentAddress) then hl_balances — do not skip ApproveBuilderFee')
|
|
26
47
|
: 'hl_new_agent_wallet',
|
|
27
|
-
note: 'Call exactly ONE nextTool. Heat blocked until after first place.'
|
|
48
|
+
note: 'Call exactly ONE nextTool. Heat blocked until after first place. Place only on explicit trade intent.'
|
|
28
49
|
};
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
const MISSING_PK = 'approve missing — hl_new_agent_wallet → export HYPELENS_AGENT_PK → hl_approve_payloads → hl_balances → hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) (mainnet)';
|
|
52
|
+
const MISSING_PK = 'approve missing — hl_new_agent_wallet → export HYPELENS_AGENT_PK (or HYPERLIQUID_PRIVATE_KEY) → hl_approve_payloads → hl_balances → hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) (mainnet)';
|
|
32
53
|
|
|
33
54
|
function assertPlacementAllowed(actions) {
|
|
34
55
|
if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
|
|
35
56
|
throw new Error('wrong net — MAINNET_PLACEMENT_ENABLED false; set HYPELENS_NET=mainnet with placement enabled, or HYPELENS_NET=testnet to practice');
|
|
36
57
|
}
|
|
37
|
-
if (!
|
|
58
|
+
if (!placePk()) throw new Error(MISSING_PK);
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
function agentAddress() {
|
|
41
62
|
const { signer } = loadShipped();
|
|
42
|
-
if (!
|
|
43
|
-
|
|
63
|
+
if (!placePk()) throw new Error(MISSING_PK);
|
|
64
|
+
const addr = signer.addressFromPrivateKey(placePk());
|
|
65
|
+
assertNotBuilderWallet(addr, 'agent');
|
|
66
|
+
return addr;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
/** Account that must have ApproveBuilderFee (master when set, else place wallet). */
|
|
70
|
+
function feeAccountAddress() {
|
|
71
|
+
const { signer } = loadShipped();
|
|
72
|
+
const explicit = process.env.HYPERLIQUID_ACCOUNT_ADDRESS || process.env.HYPELENS_MASTER_ADDRESS || '';
|
|
73
|
+
if (explicit && /^0x[0-9a-fA-F]{40}$/.test(explicit)) {
|
|
74
|
+
assertNotBuilderWallet(explicit, 'account');
|
|
75
|
+
return explicit;
|
|
76
|
+
}
|
|
77
|
+
if (masterPk()) {
|
|
78
|
+
const a = signer.addressFromPrivateKey(masterPk());
|
|
79
|
+
assertNotBuilderWallet(a, 'master');
|
|
80
|
+
return a;
|
|
81
|
+
}
|
|
82
|
+
return agentAddress();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** HL info maxBuilderFee — tenths of a bp. Need >= BUILDER_F (10) for 1bp attach. */
|
|
86
|
+
export async function checkMaxBuilderFee({ user } = {}) {
|
|
47
87
|
const { actions } = loadShipped();
|
|
88
|
+
const u = user || feeAccountAddress();
|
|
89
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
90
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ type: 'maxBuilderFee', user: u, builder: actions.BUILDER })
|
|
92
|
+
});
|
|
93
|
+
if (!r.ok) throw new Error('maxBuilderFee HTTP ' + r.status);
|
|
94
|
+
const raw = await r.json();
|
|
95
|
+
const maxTenthsBp = Number(raw);
|
|
96
|
+
const required = actions.BUILDER_F;
|
|
97
|
+
const approved = Number.isFinite(maxTenthsBp) && maxTenthsBp >= required;
|
|
98
|
+
return {
|
|
99
|
+
user: u,
|
|
100
|
+
builder: actions.BUILDER,
|
|
101
|
+
maxBuilderFeeTenthsBp: maxTenthsBp,
|
|
102
|
+
requiredTenthsBp: required,
|
|
103
|
+
maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
104
|
+
approved,
|
|
105
|
+
net: net()
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function postUserSigned(built, privateKey) {
|
|
110
|
+
const { actions, signer } = loadShipped();
|
|
111
|
+
assertNotBuilderWallet(signer.addressFromPrivateKey(privateKey), 'signer');
|
|
112
|
+
const signed = await signer.signUserSigned(privateKey, built.action);
|
|
113
|
+
const res = await fetch(actions.NET[net()].exchange, {
|
|
114
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
116
|
+
});
|
|
117
|
+
const body = await res.json().catch(() => ({}));
|
|
118
|
+
const ok = res.ok && body && body.status === 'ok';
|
|
119
|
+
return { ok, net: net(), response: body, error: ok ? null : (typeof body.response === 'string' ? body.response : JSON.stringify(body)) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* OpenBroker pattern: if master/sole key present, sign+POST ApproveBuilderFee (and approveAgent when agent differs).
|
|
124
|
+
* If only agent key: return clear unsigned payloads + maxBuilderFee status — never silent $0.
|
|
125
|
+
*/
|
|
126
|
+
export async function approvePayloads(agentAddressArg) {
|
|
127
|
+
const { actions, signer } = loadShipped();
|
|
128
|
+
if (!actions.isAddr(agentAddressArg)) throw new Error('bad agentAddress');
|
|
129
|
+
assertNotBuilderWallet(agentAddressArg, 'agent');
|
|
48
130
|
const exchange = actions.NET[net()].exchange;
|
|
49
131
|
const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
|
|
132
|
+
const approveAgent = actions.buildApproveAgent(net(), agentAddressArg);
|
|
133
|
+
const approveBuilderFee = actions.buildApproveBuilderFee(net());
|
|
134
|
+
const mk = masterPk();
|
|
135
|
+
let auto = null;
|
|
136
|
+
let maxCheck = null;
|
|
137
|
+
try {
|
|
138
|
+
maxCheck = await checkMaxBuilderFee({ user: feeAccountAddress() });
|
|
139
|
+
} catch (e) {
|
|
140
|
+
maxCheck = { error: String(e && e.message ? e.message : e), approved: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (mk) {
|
|
144
|
+
const masterAddr = signer.addressFromPrivateKey(mk);
|
|
145
|
+
assertNotBuilderWallet(masterAddr, 'master');
|
|
146
|
+
const results = { master: masterAddr, approveBuilderFee: null, approveAgent: null };
|
|
147
|
+
// Always (re)approve builder fee once when master key available
|
|
148
|
+
results.approveBuilderFee = await postUserSigned(approveBuilderFee, mk);
|
|
149
|
+
// approveAgent only when agent is a distinct API wallet
|
|
150
|
+
if (masterAddr.toLowerCase() !== agentAddressArg.toLowerCase()) {
|
|
151
|
+
results.approveAgent = await postUserSigned(approveAgent, mk);
|
|
152
|
+
} else {
|
|
153
|
+
results.approveAgent = { ok: true, skipped: true, reason: 'agent_is_master_fresh_wallet' };
|
|
154
|
+
}
|
|
155
|
+
auto = results;
|
|
156
|
+
try { maxCheck = await checkMaxBuilderFee({ user: masterAddr }); } catch (_) { /* keep prior */ }
|
|
157
|
+
const autoOk = Boolean(results.approveBuilderFee && results.approveBuilderFee.ok);
|
|
158
|
+
return {
|
|
159
|
+
net: net(),
|
|
160
|
+
mode: 'auto_master',
|
|
161
|
+
autoApproved: autoOk,
|
|
162
|
+
auto,
|
|
163
|
+
maxBuilderFee: maxCheck,
|
|
164
|
+
approveAgent,
|
|
165
|
+
approveBuilderFee,
|
|
166
|
+
builder: actions.BUILDER,
|
|
167
|
+
maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
168
|
+
fee: '1bp on fills (no sub)',
|
|
169
|
+
blocker: autoOk ? null : 'auto_approve_failed',
|
|
170
|
+
mustApproveBuilderFee: true,
|
|
171
|
+
funnel_step: autoOk ? 3 : 2,
|
|
172
|
+
steps: autoOk
|
|
173
|
+
? ['MASTER auto ApproveBuilderFee POSTed', 'Call ONLY hl_balances', 'If equity=0: deposit USDC', 'When equity>0 + user trade intent: hl_place_order sizeUsd:12']
|
|
174
|
+
: ['Auto ApproveBuilderFee failed — inspect auto.approveBuilderFee.error', 'Retry hl_approve_payloads or MASTER-sign payloads below', 'Then hl_balances → place on intent'],
|
|
175
|
+
nextTool: 'hl_balances',
|
|
176
|
+
next: autoOk
|
|
177
|
+
? 'ApproveBuilderFee auto-POSTed → hl_balances. If equity=0 show fundHow; when funded + user trade intent → hl_place_order.'
|
|
178
|
+
: 'Auto approve failed — fix master key / net, retry hl_approve_payloads. Skipping ApproveBuilderFee = $0 fees.',
|
|
179
|
+
nextCalls: ['hl_balances'],
|
|
180
|
+
nextPlace,
|
|
181
|
+
fundAfterApprove: {
|
|
182
|
+
depositAddress: agentAddressArg,
|
|
183
|
+
asset: 'USDC',
|
|
184
|
+
app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
|
|
185
|
+
minUsdSuggest: 15
|
|
186
|
+
},
|
|
187
|
+
note: 'OpenBroker: master key auto ApproveBuilderFee. Agent key places. Never trade ' + FORBIDDEN_BUILDER
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Agent-only: refuse silent $0 — clear payload + maxBuilderFee gate
|
|
50
192
|
return {
|
|
51
193
|
net: net(),
|
|
52
|
-
|
|
53
|
-
|
|
194
|
+
mode: 'agent_only_manual_master',
|
|
195
|
+
autoApproved: false,
|
|
196
|
+
maxBuilderFee: maxCheck,
|
|
197
|
+
approveAgent,
|
|
198
|
+
approveBuilderFee,
|
|
54
199
|
builder: actions.BUILDER,
|
|
55
200
|
maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
56
201
|
fee: '1bp on fills (no sub)',
|
|
57
|
-
blocker: '
|
|
202
|
+
blocker: (maxCheck && maxCheck.approved) ? null : 'master_must_sign_and_post_ApproveBuilderFee',
|
|
58
203
|
mustApproveBuilderFee: true,
|
|
59
|
-
|
|
204
|
+
refusePlaceUntilApproved: !(maxCheck && maxCheck.approved),
|
|
205
|
+
funnel_step: 2,
|
|
60
206
|
steps: [
|
|
61
|
-
'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk',
|
|
207
|
+
'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk (set HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK to auto)',
|
|
62
208
|
'POST both to ' + exchange,
|
|
209
|
+
'Verify maxBuilderFee >= 10 tenths-bp for builder ' + actions.BUILDER,
|
|
63
210
|
'Then call ONLY hl_balances',
|
|
64
|
-
'If equity=0: ask user to deposit USDC to agentAddress on Hyperliquid (app.hyperliquid.xyz), then hl_balances again',
|
|
65
211
|
'When equity>0 and user asked to trade: hl_place_order sizeUsd:12'
|
|
66
212
|
],
|
|
67
|
-
nextTool: 'hl_balances',
|
|
68
|
-
next:
|
|
213
|
+
nextTool: (maxCheck && maxCheck.approved) ? 'hl_balances' : 'hl_approve_payloads',
|
|
214
|
+
next: (maxCheck && maxCheck.approved)
|
|
215
|
+
? 'Builder fee already approved on-chain → hl_balances → place on user trading intent'
|
|
216
|
+
: 'MASTER must POST ApproveBuilderFee (or export HYPERLIQUID_PRIVATE_KEY and re-call). Place with builder is REFUSED until maxBuilderFee>=10 — no silent $0.',
|
|
69
217
|
nextCalls: ['hl_balances'],
|
|
70
218
|
nextPlace,
|
|
71
219
|
fundAfterApprove: {
|
|
@@ -74,20 +222,22 @@ export function approvePayloads(agentAddressArg) {
|
|
|
74
222
|
app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
|
|
75
223
|
minUsdSuggest: 15
|
|
76
224
|
},
|
|
77
|
-
note: 'Skipping ApproveBuilderFee = $0 fees. After POST: hl_balances → fund if equity=0 → place on ' + net() + ' only with user trading intent.'
|
|
225
|
+
note: 'Skipping ApproveBuilderFee = $0 fees. Agent-only mode cannot auto-approve. After POST: hl_balances → fund if equity=0 → place on ' + net() + ' only with user trading intent.'
|
|
78
226
|
};
|
|
79
227
|
}
|
|
80
228
|
|
|
81
229
|
export function newAgentWallet() {
|
|
82
230
|
const { signer, sdk } = loadShipped();
|
|
83
231
|
const pk = sdk.randomPrivateKey();
|
|
232
|
+
const address = signer.addressFromPrivateKey(pk);
|
|
233
|
+
assertNotBuilderWallet(address, 'agent');
|
|
84
234
|
return {
|
|
85
235
|
privateKey: pk,
|
|
86
|
-
address
|
|
236
|
+
address,
|
|
87
237
|
funnel_step: 2,
|
|
88
238
|
nextTool: 'hl_approve_payloads',
|
|
89
|
-
next: 'export HYPELENS_AGENT_PK=<privateKey>; call hl_approve_payloads(address)
|
|
90
|
-
note: 'Do not call heat tools. Next ONE tool = hl_approve_payloads.'
|
|
239
|
+
next: 'export HYPELENS_AGENT_PK=<privateKey>; optional export HYPERLIQUID_PRIVATE_KEY=<master> for auto ApproveBuilderFee; call hl_approve_payloads(address) — when user wants to trade',
|
|
240
|
+
note: 'Do not call heat tools. Next ONE tool = hl_approve_payloads. OpenBroker: master key auto-approves builder fee.'
|
|
91
241
|
};
|
|
92
242
|
}
|
|
93
243
|
|
|
@@ -123,23 +273,39 @@ async function assetMetaAndMark(coin) {
|
|
|
123
273
|
|
|
124
274
|
/**
|
|
125
275
|
* Resolve coin size + entryPx. Preferred first-place path: sizeUsd only
|
|
126
|
-
* (fetches mark, size = sizeUsd/mark, rounds to szDecimals
|
|
276
|
+
* (fetches mark, size = sizeUsd/mark, rounds to szDecimals, IOC ~0.5% slip).
|
|
277
|
+
* Explicit entryPx = GTC limit intent (no slip).
|
|
278
|
+
* @param {{coin, size, entryPx, sizeUsd, isBuy}} opts
|
|
127
279
|
*/
|
|
128
|
-
async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
280
|
+
async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy = true }) {
|
|
129
281
|
const { actions } = loadShipped();
|
|
130
|
-
const
|
|
282
|
+
const limitIntent = entryPx != null;
|
|
283
|
+
const needMark = !limitIntent || (size == null && sizeUsd != null);
|
|
131
284
|
const meta = needMark ? await assetMetaAndMark(coin) : await assetMeta(coin);
|
|
132
|
-
let
|
|
285
|
+
let markPx = meta.markPx ?? null;
|
|
286
|
+
let px;
|
|
287
|
+
let tif;
|
|
288
|
+
if (limitIntent) {
|
|
289
|
+
px = Number(entryPx);
|
|
290
|
+
tif = 'Gtc';
|
|
291
|
+
} else {
|
|
292
|
+
// Market/IOC path: same 0.5% slip through mark as closePosition
|
|
293
|
+
if (!(markPx > 0)) throw new Error('entryPx required (or omit and pass sizeUsd for auto mark)');
|
|
294
|
+
px = isBuy ? markPx * 1.005 : markPx * 0.995;
|
|
295
|
+
tif = 'Ioc';
|
|
296
|
+
}
|
|
133
297
|
if (!(px > 0)) throw new Error('entryPx required (or omit and pass sizeUsd for auto mark)');
|
|
134
298
|
let sz = size != null ? Number(size) : null;
|
|
135
299
|
let usedSizeUsd = sizeUsd != null ? Number(sizeUsd) : null;
|
|
300
|
+
// Size from notional uses mark (true market size), not slipped px
|
|
301
|
+
const sizePx = (!limitIntent && markPx > 0) ? markPx : px;
|
|
136
302
|
if (sz == null) {
|
|
137
303
|
if (!(usedSizeUsd > 0)) {
|
|
138
|
-
throw new Error('size or sizeUsd required — first place: {coin:"BTC", isBuy:true, sizeUsd:12, leverage:2} (
|
|
304
|
+
throw new Error('size or sizeUsd required — first place: {coin:"BTC", isBuy:true, sizeUsd:12, leverage:2} (IOC @ mark+0.5% slip; pass entryPx for GTC limit)');
|
|
139
305
|
}
|
|
140
|
-
sz = usedSizeUsd /
|
|
306
|
+
sz = usedSizeUsd / sizePx;
|
|
141
307
|
} else if (usedSizeUsd == null) {
|
|
142
|
-
usedSizeUsd = sz *
|
|
308
|
+
usedSizeUsd = sz * sizePx;
|
|
143
309
|
}
|
|
144
310
|
sz = actions.roundToDecimals(sz, meta.szDecimals);
|
|
145
311
|
// Validate wire size will not collapse to 0
|
|
@@ -154,8 +320,10 @@ async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
|
154
320
|
size: sz,
|
|
155
321
|
entryPx: px,
|
|
156
322
|
sizeUsd: usedSizeUsd,
|
|
157
|
-
markPx
|
|
158
|
-
|
|
323
|
+
markPx,
|
|
324
|
+
tif,
|
|
325
|
+
limitIntent,
|
|
326
|
+
autoRounded: size == null || !limitIntent
|
|
159
327
|
};
|
|
160
328
|
}
|
|
161
329
|
|
|
@@ -202,7 +370,7 @@ async function postL1(action) {
|
|
|
202
370
|
const { actions, signer } = loadShipped();
|
|
203
371
|
assertPlacementAllowed(actions);
|
|
204
372
|
const nonce = actions.nonce();
|
|
205
|
-
const signed = await signer.signL1(
|
|
373
|
+
const signed = await signer.signL1(placePk(), action, nonce, net() === 'testnet', null);
|
|
206
374
|
const res = await fetch(actions.NET[net()].exchange, {
|
|
207
375
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
208
376
|
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
@@ -300,14 +468,54 @@ export async function getPositions({ user, coin } = {}) {
|
|
|
300
468
|
next: positions.length
|
|
301
469
|
? 'optional hl_close_position / hl_cancel_order — or another ' + placeCall
|
|
302
470
|
: 'no positions yet — call hl_balances then ' + placeCall + ' (do not open heat tools)',
|
|
303
|
-
note: 'Paying path = mainnet place
|
|
471
|
+
note: 'Paying path = mainnet place on explicit user trading intent.'
|
|
304
472
|
};
|
|
305
473
|
}
|
|
306
474
|
|
|
307
|
-
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
475
|
+
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false, confirmTrade = false, intent = null, skipBuilderFeeCheck = false }) {
|
|
308
476
|
const { actions } = loadShipped();
|
|
309
477
|
assertPlacementAllowed(actions);
|
|
310
|
-
|
|
478
|
+
// Consent: place only on explicit trade intent (skill + confirmTrade/intent)
|
|
479
|
+
const intentOk = confirmTrade === true || ['trade', 'place', 'execute', 'buy', 'sell'].includes(String(intent || '').toLowerCase());
|
|
480
|
+
if (!intentOk) {
|
|
481
|
+
return {
|
|
482
|
+
placed: false,
|
|
483
|
+
refused: 'consent_required',
|
|
484
|
+
error: 'Place requires explicit trade intent — pass confirmTrade:true or intent:"trade"|"place"|"execute" (ClawScan-safe). Do not place unsolicited.',
|
|
485
|
+
builderFeeAttached: false,
|
|
486
|
+
next: 'hl_place_order({..., confirmTrade:true}) only after user asked to trade/place/execute'
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
// Hard fee gate: refuse place-with-builder until maxBuilderFee approved (no silent $0)
|
|
490
|
+
let feeGate = { approved: true, skipped: true };
|
|
491
|
+
if (!skipBuilderFeeCheck && process.env.HYPELENS_TEST_SKIP_FEE_GATE !== '1') {
|
|
492
|
+
try {
|
|
493
|
+
feeGate = await checkMaxBuilderFee();
|
|
494
|
+
} catch (e) {
|
|
495
|
+
return {
|
|
496
|
+
placed: false,
|
|
497
|
+
refused: 'maxBuilderFee_check_failed',
|
|
498
|
+
error: String(e && e.message ? e.message : e),
|
|
499
|
+
builderFeeAttached: false,
|
|
500
|
+
next: 'hl_approve_payloads → MASTER ApproveBuilderFee (or set HYPERLIQUID_PRIVATE_KEY for auto) → retry place'
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (!feeGate.approved) {
|
|
504
|
+
const payloads = await approvePayloads(agentAddress());
|
|
505
|
+
return {
|
|
506
|
+
placed: false,
|
|
507
|
+
refused: 'builder_fee_not_approved',
|
|
508
|
+
error: 'maxBuilderFee=' + feeGate.maxBuilderFeeTenthsBp + ' < required ' + feeGate.requiredTenthsBp + ' — refusing place-with-builder (would be $0 fees). MASTER must ApproveBuilderFee 0.01% for ' + actions.BUILDER,
|
|
509
|
+
maxBuilderFee: feeGate,
|
|
510
|
+
approvePayloads: payloads,
|
|
511
|
+
builderFeeAttached: false,
|
|
512
|
+
next: masterPk()
|
|
513
|
+
? 'hl_approve_payloads(agentAddress) — master key will auto-POST ApproveBuilderFee, then retry place'
|
|
514
|
+
: 'Export HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK and call hl_approve_payloads, OR MASTER-sign approvePayloads.approveBuilderFee and POST — then retry. No silent $0.'
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy });
|
|
311
519
|
let risk = null;
|
|
312
520
|
const lev = leverage != null ? Number(leverage) : null;
|
|
313
521
|
if (!skipRiskCheck && lev) {
|
|
@@ -346,7 +554,8 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
346
554
|
entryPx: resolved.entryPx,
|
|
347
555
|
size: resolved.size,
|
|
348
556
|
slPx,
|
|
349
|
-
tpPx
|
|
557
|
+
tpPx,
|
|
558
|
+
tif: resolved.tif
|
|
350
559
|
});
|
|
351
560
|
const posted = await postL1(action);
|
|
352
561
|
const out = {
|
|
@@ -354,11 +563,16 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
354
563
|
net: posted.net,
|
|
355
564
|
response: posted.response,
|
|
356
565
|
risk,
|
|
357
|
-
|
|
566
|
+
builder: { b: actions.BUILDER.toLowerCase(), f: actions.BUILDER_F },
|
|
567
|
+
builderFeeAttached: posted.ok && Boolean(action.builder && action.builder.f === actions.BUILDER_F),
|
|
568
|
+
maxBuilderFee: feeGate,
|
|
358
569
|
coin: resolved.name,
|
|
359
570
|
size: resolved.size,
|
|
360
571
|
entryPx: resolved.entryPx,
|
|
361
572
|
sizeUsd: resolved.sizeUsd,
|
|
573
|
+
markPx: resolved.markPx,
|
|
574
|
+
tif: resolved.tif,
|
|
575
|
+
limitIntent: resolved.limitIntent,
|
|
362
576
|
autoRounded: resolved.autoRounded,
|
|
363
577
|
sizingWarn,
|
|
364
578
|
feeModel: '1bp on fills only — no subscription'
|
|
@@ -389,9 +603,19 @@ export async function cancelOrder({ coin, oid = null, cloid = null }) {
|
|
|
389
603
|
}
|
|
390
604
|
|
|
391
605
|
/** IOC reduce-only close for a coin (uses position size if size omitted). */
|
|
392
|
-
export async function closePosition({ coin, size = null, px = null, user } = {}) {
|
|
606
|
+
export async function closePosition({ coin, size = null, px = null, user, confirmTrade = false, intent = null, skipBuilderFeeCheck = false } = {}) {
|
|
393
607
|
const { actions } = loadShipped();
|
|
394
608
|
assertPlacementAllowed(actions);
|
|
609
|
+
const intentOk = confirmTrade === true || ['trade', 'place', 'execute', 'close', 'cancel'].includes(String(intent || '').toLowerCase());
|
|
610
|
+
if (!intentOk) {
|
|
611
|
+
return { closed: false, refused: 'consent_required', error: 'Close requires explicit intent — pass confirmTrade:true', builderFeeAttached: false };
|
|
612
|
+
}
|
|
613
|
+
if (!skipBuilderFeeCheck && process.env.HYPELENS_TEST_SKIP_FEE_GATE !== '1') {
|
|
614
|
+
const feeGate = await checkMaxBuilderFee();
|
|
615
|
+
if (!feeGate.approved) {
|
|
616
|
+
return { closed: false, refused: 'builder_fee_not_approved', maxBuilderFee: feeGate, builderFeeAttached: false, error: 'ApproveBuilderFee required before close-with-builder' };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
395
619
|
const name = resolveCoin(coin).toUpperCase();
|
|
396
620
|
const { positions } = await getPositions({ user, coin: name });
|
|
397
621
|
const pos = positions[0];
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// HypeLens Agent Rail — EXECUTION (builder-code monetized).
|
|
2
|
+
// Reuses Module 3: hl-actions.js builds actions (builder fee pinned on place/close),
|
|
3
|
+
// hl-signer.js signs through the vendored SDK. Default HYPELENS_NET=testnet.
|
|
4
|
+
// Fee model: 1bp (0.01%) on fills only — no subscription. BUILDER_F=10.
|
|
5
|
+
// Risk/heat advisory when feed missing; danger-wall refuses unless override.
|
|
6
|
+
import { loadShipped } from './load.js';
|
|
7
|
+
import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
|
|
8
|
+
|
|
9
|
+
const net = () => process.env.HYPELENS_NET || 'testnet';
|
|
10
|
+
|
|
11
|
+
export function status() {
|
|
12
|
+
const { actions, signer } = loadShipped();
|
|
13
|
+
const st = signer.selfTest();
|
|
14
|
+
return {
|
|
15
|
+
net: net(),
|
|
16
|
+
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
17
|
+
builder: actions.BUILDER, builderFeeTenthsBp: actions.BUILDER_F, maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
18
|
+
feeModel: '1bp on fills only — no subscription',
|
|
19
|
+
signerReady: st.ok, signerError: st.ok ? null : st.error,
|
|
20
|
+
hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
|
|
21
|
+
fullFeedConfigured: fullFeedConfigured()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertPlacementAllowed(actions) {
|
|
26
|
+
if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
|
|
27
|
+
throw new Error('MAINNET PLACEMENT DISABLED — Set HYPELENS_NET=testnet.');
|
|
28
|
+
}
|
|
29
|
+
if (!process.env.HYPELENS_AGENT_PK) throw new Error('HYPELENS_AGENT_PK not set — run the approve flow first (see approvePayloads)');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function agentAddress() {
|
|
33
|
+
const { signer } = loadShipped();
|
|
34
|
+
if (!process.env.HYPELENS_AGENT_PK) throw new Error('HYPELENS_AGENT_PK not set');
|
|
35
|
+
return signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function approvePayloads(agentAddressArg) {
|
|
39
|
+
const { actions } = loadShipped();
|
|
40
|
+
return {
|
|
41
|
+
approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
|
|
42
|
+
approveBuilderFee: actions.buildApproveBuilderFee(net()),
|
|
43
|
+
note: 'Sign both with the MASTER wallet (EIP-712), POST each as {action, signature, nonce} to ' + actions.NET[net()].exchange
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function newAgentWallet() {
|
|
48
|
+
const { signer, sdk } = loadShipped();
|
|
49
|
+
const pk = sdk.randomPrivateKey();
|
|
50
|
+
return { privateKey: pk, address: signer.addressFromPrivateKey(pk), note: 'store as HYPELENS_AGENT_PK; approve via approvePayloads(address)' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function assetMeta(coin) {
|
|
54
|
+
const { actions } = loadShipped();
|
|
55
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
56
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ type: 'meta' })
|
|
58
|
+
});
|
|
59
|
+
const meta = await r.json();
|
|
60
|
+
const name = resolveCoin(coin).toUpperCase();
|
|
61
|
+
const i = meta.universe.findIndex((u) => u.name === name);
|
|
62
|
+
if (i < 0) throw new Error('coin not on ' + net() + ': ' + coin);
|
|
63
|
+
return { assetIndex: i, szDecimals: meta.universe[i].szDecimals, name };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function postL1(action) {
|
|
67
|
+
const { actions, signer } = loadShipped();
|
|
68
|
+
assertPlacementAllowed(actions);
|
|
69
|
+
const nonce = actions.nonce();
|
|
70
|
+
const signed = await signer.signL1(process.env.HYPELENS_AGENT_PK, action, nonce, net() === 'testnet', null);
|
|
71
|
+
const res = await fetch(actions.NET[net()].exchange, {
|
|
72
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
74
|
+
});
|
|
75
|
+
const body = await res.json().catch(() => ({}));
|
|
76
|
+
return { ok: res.ok && body.status === 'ok', net: net(), response: body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function clearinghouse(user) {
|
|
80
|
+
const { actions } = loadShipped();
|
|
81
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
82
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ type: 'clearinghouseState', user })
|
|
84
|
+
});
|
|
85
|
+
if (!r.ok) throw new Error('clearinghouseState HTTP ' + r.status);
|
|
86
|
+
return r.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Balances / margin summary for the agent (or explicit user). */
|
|
90
|
+
export async function getBalances({ user } = {}) {
|
|
91
|
+
const u = user || agentAddress();
|
|
92
|
+
const state = await clearinghouse(u);
|
|
93
|
+
const ms = state.marginSummary || {};
|
|
94
|
+
const equity = Number(ms.accountValue || 0);
|
|
95
|
+
return {
|
|
96
|
+
net: net(),
|
|
97
|
+
user: u,
|
|
98
|
+
marginSummary: ms,
|
|
99
|
+
withdrawable: state.withdrawable ?? null,
|
|
100
|
+
equity,
|
|
101
|
+
sizingWarnThresholdUsd: equity * 0.2,
|
|
102
|
+
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
103
|
+
feeModel: '1bp on fills only — no subscription'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Open perp positions (assetPositions). */
|
|
108
|
+
export async function getPositions({ user, coin } = {}) {
|
|
109
|
+
const u = user || agentAddress();
|
|
110
|
+
const state = await clearinghouse(u);
|
|
111
|
+
let positions = (state.assetPositions || []).map((ap) => {
|
|
112
|
+
const p = ap.position || ap;
|
|
113
|
+
return {
|
|
114
|
+
coin: p.coin,
|
|
115
|
+
szi: Number(p.szi || 0),
|
|
116
|
+
entryPx: p.entryPx != null ? Number(p.entryPx) : null,
|
|
117
|
+
positionValue: p.positionValue != null ? Number(p.positionValue) : null,
|
|
118
|
+
unrealizedPnl: p.unrealizedPnl != null ? Number(p.unrealizedPnl) : null,
|
|
119
|
+
leverage: p.leverage || null,
|
|
120
|
+
liquidationPx: p.liquidationPx != null ? Number(p.liquidationPx) : null,
|
|
121
|
+
marginUsed: p.marginUsed != null ? Number(p.marginUsed) : null
|
|
122
|
+
};
|
|
123
|
+
}).filter((p) => p.szi !== 0);
|
|
124
|
+
if (coin) {
|
|
125
|
+
const c = resolveCoin(coin).toUpperCase();
|
|
126
|
+
positions = positions.filter((p) => (p.coin || '').toUpperCase() === c);
|
|
127
|
+
}
|
|
128
|
+
return { net: net(), user: u, positions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
132
|
+
const { actions } = loadShipped();
|
|
133
|
+
assertPlacementAllowed(actions);
|
|
134
|
+
let risk = null;
|
|
135
|
+
if (!skipRiskCheck && leverage) {
|
|
136
|
+
risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage, entryPx });
|
|
137
|
+
if (risk.refuseReason === 'full_feed_unconfigured') {
|
|
138
|
+
risk = { ...risk, advisory: true, feedAdvisory: true, note: (risk.note || '') + ' Place allowed without full feed; heat is advisory.' };
|
|
139
|
+
}
|
|
140
|
+
if (risk.verdict === 'danger' && risk.refuseReason !== 'full_feed_unconfigured' && !override) {
|
|
141
|
+
const wallSz = risk.wall && risk.wall.sizeUsd != null
|
|
142
|
+
? Math.round(risk.wall.sizeUsd / 1e6)
|
|
143
|
+
: (risk.wall && risk.wall.sizeUsdCoarse != null ? Math.round(risk.wall.sizeUsdCoarse / 1e6) : '?');
|
|
144
|
+
return {
|
|
145
|
+
placed: false,
|
|
146
|
+
refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
|
|
147
|
+
risk,
|
|
148
|
+
builderFeeAttached: false
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const { assetIndex, szDecimals } = await assetMeta(coin);
|
|
153
|
+
const action = actions.buildOrderAction({ assetIndex, szDecimals, isBuy, entryPx, size, slPx, tpPx });
|
|
154
|
+
const posted = await postL1(action);
|
|
155
|
+
return { placed: posted.ok, net: posted.net, response: posted.response, risk, builderFeeAttached: posted.ok };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Cancel by oid and/or cloid. */
|
|
159
|
+
export async function cancelOrder({ coin, oid = null, cloid = null }) {
|
|
160
|
+
const { actions } = loadShipped();
|
|
161
|
+
assertPlacementAllowed(actions);
|
|
162
|
+
const { assetIndex } = await assetMeta(coin);
|
|
163
|
+
let action;
|
|
164
|
+
if (cloid) action = actions.buildCancelByCloidAction([{ assetIndex, cloid }]);
|
|
165
|
+
else if (oid != null) action = actions.buildCancelAction([{ assetIndex, oid: Number(oid) }]);
|
|
166
|
+
else throw new Error('oid or cloid required');
|
|
167
|
+
const posted = await postL1(action);
|
|
168
|
+
return { cancelled: posted.ok, net: posted.net, response: posted.response, coin: resolveCoin(coin).toUpperCase(), oid, cloid };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** IOC reduce-only close for a coin (uses position size if size omitted). */
|
|
172
|
+
export async function closePosition({ coin, size = null, px = null, user } = {}) {
|
|
173
|
+
const { actions } = loadShipped();
|
|
174
|
+
assertPlacementAllowed(actions);
|
|
175
|
+
const name = resolveCoin(coin).toUpperCase();
|
|
176
|
+
const { positions } = await getPositions({ user, coin: name });
|
|
177
|
+
const pos = positions[0];
|
|
178
|
+
if (!pos || !pos.szi) throw new Error('no open position for ' + name);
|
|
179
|
+
const absSz = Math.abs(pos.szi);
|
|
180
|
+
const closeSz = size != null ? Number(size) : absSz;
|
|
181
|
+
if (!(closeSz > 0) || closeSz > absSz + 1e-12) throw new Error('bad close size');
|
|
182
|
+
const isBuy = pos.szi < 0; // short -> buy to close
|
|
183
|
+
let entryPx = px;
|
|
184
|
+
if (entryPx == null) {
|
|
185
|
+
const { actions: a2 } = loadShipped();
|
|
186
|
+
const r = await fetch(a2.NET[net()].info, {
|
|
187
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' })
|
|
189
|
+
});
|
|
190
|
+
const [meta, ctxs] = await r.json();
|
|
191
|
+
const i = meta.universe.findIndex((u) => u.name === name);
|
|
192
|
+
if (i < 0) throw new Error('coin not in meta: ' + name);
|
|
193
|
+
entryPx = Number(ctxs[i].markPx);
|
|
194
|
+
// slip 0.5% through for IOC fill
|
|
195
|
+
entryPx = isBuy ? entryPx * 1.005 : entryPx * 0.995;
|
|
196
|
+
}
|
|
197
|
+
const { assetIndex, szDecimals } = await assetMeta(name);
|
|
198
|
+
const action = actions.buildCloseAction({ assetIndex, szDecimals, isBuy, entryPx, size: closeSz });
|
|
199
|
+
const posted = await postL1(action);
|
|
200
|
+
return {
|
|
201
|
+
closed: posted.ok,
|
|
202
|
+
net: posted.net,
|
|
203
|
+
response: posted.response,
|
|
204
|
+
coin: name,
|
|
205
|
+
size: closeSz,
|
|
206
|
+
isBuy,
|
|
207
|
+
entryPx,
|
|
208
|
+
builderFeeAttached: posted.ok,
|
|
209
|
+
feeModel: '1bp on fills only — no subscription'
|
|
210
|
+
};
|
|
211
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { walls, cascade, pretradeCheck, whaleBook, getFeed, getMeta, binWalls, resolveCoin, quickstart } from './core.js';
|
|
2
|
-
export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition } from './exchange.js';
|
|
2
|
+
export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee } from './exchange.js';
|