@hypelens/hypelens-agent-rail 0.1.21 → 0.1.23
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 +39 -25
- package/bin/hypelens-setup.js +204 -0
- package/package.json +6 -4
- package/skill/hypelens-agent-rail/SKILL.md +15 -52
- package/src/core.js +52 -20
- package/src/exchange.js +227 -25
- package/src/index.js +1 -1
- package/src/mcp.js +51 -9
- package/vendor/hl-sdk.js +13 -0
- package/vendor/hl-signer.js +12 -3
package/README.md
CHANGED
|
@@ -1,37 +1,51 @@
|
|
|
1
1
|
# @hypelens/hypelens-agent-rail
|
|
2
|
-
**Place, cancel, close Hyperliquid perps.** Agent wallet once · auto-round · **1bp on fills — no sub.**
|
|
3
|
-
## Install → approve → place
|
|
4
|
-
```bash
|
|
5
|
-
# MCP stdio (Claude / Cursor / any MCP host)
|
|
6
|
-
npx @hypelens/hypelens-agent-rail@0.1.21
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
claude mcp add hypelens -- npx @hypelens/hypelens-agent-rail@0.1.21
|
|
3
|
+
Outside-agent Hyperliquid **place** rail. Builder fees → `0x9548B8E9554a1968843B3C380431b10996247c88` at **1bp** (`f=10`) on fills. **Never** trade/farm that wallet.
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
## Fee capture (OpenBroker pattern)
|
|
6
|
+
|
|
7
|
+
1. Every `hl_place_order` / close **always** attaches `builder: { b: 0x9548…, f: 10 }`.
|
|
8
|
+
2. **Setup / `hl_approve_payloads`:**
|
|
9
|
+
- If `HYPERLIQUID_PRIVATE_KEY` or `HYPELENS_MASTER_PK` is set → **auto sign+POST** `ApproveBuilderFee` (and `approveAgent` when agent ≠ master).
|
|
10
|
+
- If only agent key (`HYPELENS_AGENT_PK`) → return clear unsigned approve payloads + `maxBuilderFee` status; **refuse place-with-builder** until `maxBuilderFee >= 10` (no silent $0).
|
|
11
|
+
3. **Consent:** `hl_place_order` requires `confirmTrade: true` or `intent: "trade"|"place"|"execute"`.
|
|
12
|
+
|
|
13
|
+
## Post-install (REQUIRED — downloads ≠ fees)
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
```bash
|
|
16
|
+
cd skills/hyperliquid-place/scripts && npm install && node setup.mjs
|
|
17
|
+
# or: npx -y @hypelens/hypelens-agent-rail@0.1.23 hypelens-setup
|
|
18
|
+
```
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
`setup.mjs` / `hypelens-setup`:
|
|
21
|
+
1. Registers MCP in OpenClaw/Claude/Cursor config when possible
|
|
22
|
+
2. Auto `ApproveBuilderFee` when `HYPERLIQUID_PRIVATE_KEY` / `HYPELENS_MASTER_PK` present
|
|
23
|
+
3. Prints `maxBuilderFee` — place stays refused until approved (no silent $0)
|
|
24
|
+
4. Next: fund agent if equity=0 → place on explicit trade intent
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Quick path
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @hypelens/hypelens-agent-rail@0.1.23
|
|
30
|
+
# or sticky ClawHub:
|
|
31
|
+
clawhub install hyperliquid-place
|
|
32
|
+
cd skills/hyperliquid-place/scripts && npm install && node start-mcp.mjs
|
|
33
|
+
```
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
Env:
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
| Var | Role |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `HYPELENS_AGENT_PK` | Agent key for places |
|
|
40
|
+
| `HYPERLIQUID_PRIVATE_KEY` / `HYPELENS_MASTER_PK` | Master — auto ApproveBuilderFee |
|
|
41
|
+
| `HYPERLIQUID_ACCOUNT_ADDRESS` | Master address when using API wallet |
|
|
42
|
+
| `HYPELENS_NET` | `mainnet` (default) or `testnet` |
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
## Funnel
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
1. `hl_quickstart` / `hl_new_agent_wallet`
|
|
47
|
+
2. `hl_approve_payloads(agentAddress)` — auto if master key; else MASTER signs ApproveBuilderFee **0.01%**
|
|
48
|
+
3. `hl_balances` (+ fundHow if equity=0)
|
|
49
|
+
4. On explicit trade intent: `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})`
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
Skipping ApproveBuilderFee = **$0 fees**. Place is hard-refused until on-chain `maxBuilderFee` covers 1bp.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenBroker-style post-install one-shot:
|
|
4
|
+
* 1) Wire MCP into host config (OpenClaw / Claude / Cursor) when possible
|
|
5
|
+
* 2) Ensure agent key (generate if missing)
|
|
6
|
+
* 3) Auto ApproveBuilderFee when HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK present
|
|
7
|
+
* 4) Print maxBuilderFee + fund + place next (never silent $0)
|
|
8
|
+
* NEVER trades/farms 0x9548…
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const FORBIDDEN = '0x9548B8E9554a1968843B3C380431b10996247c88';
|
|
17
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
|
|
19
|
+
function loadRail() {
|
|
20
|
+
process.env.HYPELENS_QUIET = '1';
|
|
21
|
+
return import(pathToFileURL(join(ROOT, 'src', 'index.js')).href);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function expand(p) {
|
|
25
|
+
return p.replace(/^~(?=\/|$)/, homedir());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Candidate MCP host config files (best-effort; never overwrite unrelated keys). */
|
|
29
|
+
function hostConfigPaths() {
|
|
30
|
+
const h = homedir();
|
|
31
|
+
return [
|
|
32
|
+
{ kind: 'openclaw', path: join(h, '.openclaw', 'openclaw.json') },
|
|
33
|
+
{ kind: 'openclaw', path: join(h, '.openclaw', 'config.json') },
|
|
34
|
+
{ kind: 'openclaw', path: join(h, '.openclaw', 'mcp.json') },
|
|
35
|
+
{ kind: 'claude', path: join(h, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
|
|
36
|
+
{ kind: 'claude', path: join(h, '.config', 'Claude', 'claude_desktop_config.json') },
|
|
37
|
+
{ kind: 'cursor', path: join(h, '.cursor', 'mcp.json') },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mcpServerEntry(scriptsDir) {
|
|
42
|
+
const start = scriptsDir ? join(scriptsDir, 'start-mcp.mjs') : null;
|
|
43
|
+
if (start && existsSync(start)) {
|
|
44
|
+
return {
|
|
45
|
+
command: 'node',
|
|
46
|
+
args: [start],
|
|
47
|
+
env: {
|
|
48
|
+
HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
|
|
49
|
+
...(process.env.HYPELENS_AGENT_PK ? { HYPELENS_AGENT_PK: process.env.HYPELENS_AGENT_PK } : {}),
|
|
50
|
+
...(process.env.HYPERLIQUID_PRIVATE_KEY ? { HYPERLIQUID_PRIVATE_KEY: process.env.HYPERLIQUID_PRIVATE_KEY } : {}),
|
|
51
|
+
...(process.env.HYPELENS_MASTER_PK ? { HYPELENS_MASTER_PK: process.env.HYPELENS_MASTER_PK } : {}),
|
|
52
|
+
...(process.env.HYPERLIQUID_ACCOUNT_ADDRESS ? { HYPERLIQUID_ACCOUNT_ADDRESS: process.env.HYPERLIQUID_ACCOUNT_ADDRESS } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
command: 'npx',
|
|
58
|
+
args: ['-y', '@hypelens/hypelens-agent-rail@0.1.23'],
|
|
59
|
+
env: {
|
|
60
|
+
HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
|
|
61
|
+
...(process.env.HYPELENS_AGENT_PK ? { HYPELENS_AGENT_PK: process.env.HYPELENS_AGENT_PK } : {}),
|
|
62
|
+
...(process.env.HYPERLIQUID_PRIVATE_KEY ? { HYPERLIQUID_PRIVATE_KEY: process.env.HYPERLIQUID_PRIVATE_KEY } : {}),
|
|
63
|
+
...(process.env.HYPELENS_MASTER_PK ? { HYPELENS_MASTER_PK: process.env.HYPELENS_MASTER_PK } : {}),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wireMcp({ scriptsDir, name = 'hyperliquid-place' } = {}) {
|
|
69
|
+
const entry = mcpServerEntry(scriptsDir);
|
|
70
|
+
const wired = [];
|
|
71
|
+
const suggested = { mcpServers: { [name]: entry } };
|
|
72
|
+
for (const host of hostConfigPaths()) {
|
|
73
|
+
try {
|
|
74
|
+
const dir = dirname(host.path);
|
|
75
|
+
if (!existsSync(dir) && host.kind === 'openclaw') {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
if (!existsSync(dirname(host.path))) continue;
|
|
79
|
+
let cfg = {};
|
|
80
|
+
if (existsSync(host.path)) {
|
|
81
|
+
cfg = JSON.parse(readFileSync(host.path, 'utf8'));
|
|
82
|
+
} else if (host.kind !== 'openclaw') {
|
|
83
|
+
// only auto-create openclaw configs; for others print snippet
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!cfg.mcpServers || typeof cfg.mcpServers !== 'object') cfg.mcpServers = {};
|
|
87
|
+
cfg.mcpServers[name] = entry;
|
|
88
|
+
writeFileSync(host.path, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
89
|
+
try { chmodSync(host.path, 0o600); } catch (_) {}
|
|
90
|
+
wired.push({ kind: host.kind, path: host.path });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
wired.push({ kind: host.kind, path: host.path, error: String(e && e.message ? e.message : e) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { wired, suggested, entry };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function runApprove() {
|
|
99
|
+
const rail = await loadRail();
|
|
100
|
+
const { newAgentWallet, approvePayloads, checkMaxBuilderFee, exchangeStatus } = rail;
|
|
101
|
+
const status = exchangeStatus();
|
|
102
|
+
let agentPk = process.env.HYPELENS_AGENT_PK || '';
|
|
103
|
+
let agentAddress = null;
|
|
104
|
+
let generated = null;
|
|
105
|
+
if (!agentPk && process.env.HYPERLIQUID_PRIVATE_KEY) {
|
|
106
|
+
// Fresh-wallet OpenBroker: sole key places + can auto-approve
|
|
107
|
+
agentPk = process.env.HYPERLIQUID_PRIVATE_KEY;
|
|
108
|
+
process.env.HYPELENS_AGENT_PK = agentPk;
|
|
109
|
+
}
|
|
110
|
+
if (!agentPk) {
|
|
111
|
+
generated = newAgentWallet();
|
|
112
|
+
agentPk = generated.privateKey;
|
|
113
|
+
agentAddress = generated.address;
|
|
114
|
+
process.env.HYPELENS_AGENT_PK = agentPk;
|
|
115
|
+
} else {
|
|
116
|
+
const { loadShipped } = await import(pathToFileURL(join(ROOT, 'src', 'load.js')).href);
|
|
117
|
+
const { signer } = loadShipped();
|
|
118
|
+
agentAddress = signer.addressFromPrivateKey(agentPk);
|
|
119
|
+
}
|
|
120
|
+
if (agentAddress && agentAddress.toLowerCase() === FORBIDDEN.toLowerCase()) {
|
|
121
|
+
throw new Error('Refusing setup for builder sink wallet ' + FORBIDDEN);
|
|
122
|
+
}
|
|
123
|
+
const approve = await approvePayloads(agentAddress);
|
|
124
|
+
let fee = null;
|
|
125
|
+
try { fee = await checkMaxBuilderFee({ user: agentAddress }); } catch (e) {
|
|
126
|
+
fee = { error: String(e && e.message ? e.message : e), approved: false };
|
|
127
|
+
}
|
|
128
|
+
return { status, generated, agentAddress, approve, maxBuilderFee: fee };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseArgs(argv) {
|
|
132
|
+
const out = { scriptsDir: null, writeEnv: null, json: false };
|
|
133
|
+
for (let i = 2; i < argv.length; i++) {
|
|
134
|
+
const a = argv[i];
|
|
135
|
+
if (a === '--json') out.json = true;
|
|
136
|
+
else if (a === '--scripts-dir' && argv[i + 1]) out.scriptsDir = expand(argv[++i]);
|
|
137
|
+
else if (a === '--write-env' && argv[i + 1]) out.writeEnv = expand(argv[++i]);
|
|
138
|
+
}
|
|
139
|
+
// default scripts dir = cwd if start-mcp.mjs present
|
|
140
|
+
if (!out.scriptsDir && existsSync(join(process.cwd(), 'start-mcp.mjs'))) out.scriptsDir = process.cwd();
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function main() {
|
|
145
|
+
const args = parseArgs(process.argv);
|
|
146
|
+
const mcp = wireMcp({ scriptsDir: args.scriptsDir });
|
|
147
|
+
const feePath = await runApprove();
|
|
148
|
+
if (args.writeEnv && feePath.generated) {
|
|
149
|
+
const lines = [
|
|
150
|
+
`# hypelens setup ${new Date().toISOString()}`,
|
|
151
|
+
`HYPELENS_NET=${process.env.HYPELENS_NET || 'mainnet'}`,
|
|
152
|
+
`HYPELENS_AGENT_PK=${feePath.generated.privateKey}`,
|
|
153
|
+
process.env.HYPERLIQUID_PRIVATE_KEY ? `HYPERLIQUID_PRIVATE_KEY=${process.env.HYPERLIQUID_PRIVATE_KEY}` : '# export HYPERLIQUID_PRIVATE_KEY=<master> for auto ApproveBuilderFee',
|
|
154
|
+
'',
|
|
155
|
+
];
|
|
156
|
+
writeFileSync(args.writeEnv, lines.join('\n'), { mode: 0o600 });
|
|
157
|
+
try { chmodSync(args.writeEnv, 0o600); } catch (_) {}
|
|
158
|
+
}
|
|
159
|
+
const approved = Boolean(feePath.maxBuilderFee && feePath.maxBuilderFee.approved) || Boolean(feePath.approve && feePath.approve.autoApproved);
|
|
160
|
+
const report = {
|
|
161
|
+
ok: true,
|
|
162
|
+
package: '@hypelens/hypelens-agent-rail',
|
|
163
|
+
builder: FORBIDDEN,
|
|
164
|
+
builderFee: '1bp (f=10) — never trade builder wallet',
|
|
165
|
+
mcpWired: mcp.wired.filter((w) => !w.error),
|
|
166
|
+
mcpWireErrors: mcp.wired.filter((w) => w.error),
|
|
167
|
+
mcpSuggested: mcp.suggested,
|
|
168
|
+
agentAddress: feePath.agentAddress,
|
|
169
|
+
generatedAgent: Boolean(feePath.generated),
|
|
170
|
+
generatedPrivateKey: feePath.generated ? feePath.generated.privateKey : undefined,
|
|
171
|
+
approveMode: feePath.approve && feePath.approve.mode,
|
|
172
|
+
autoApproved: Boolean(feePath.approve && feePath.approve.autoApproved),
|
|
173
|
+
maxBuilderFee: feePath.maxBuilderFee,
|
|
174
|
+
placeAllowed: approved,
|
|
175
|
+
next: !approved
|
|
176
|
+
? 'Export HYPERLIQUID_PRIVATE_KEY=<master> and re-run setup (auto ApproveBuilderFee), OR MASTER-sign approve payloads. Place is REFUSED until maxBuilderFee>=10 — no silent $0.'
|
|
177
|
+
: 'Restart MCP host. hl_balances — if equity=0 deposit USDC to agentAddress. On explicit trade intent: hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})',
|
|
178
|
+
funnel: ['mcp_wired', 'approve_builder_fee', 'fund_if_needed', 'place_on_intent'],
|
|
179
|
+
};
|
|
180
|
+
if (args.json) {
|
|
181
|
+
console.log(JSON.stringify(report, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
console.log('HypeLens setup (OpenBroker)');
|
|
184
|
+
console.log('- MCP wired:', report.mcpWired.length ? report.mcpWired.map((w) => w.path).join(', ') : '(none — paste mcpSuggested into host config)');
|
|
185
|
+
if (report.mcpWireErrors.length) console.log('- MCP wire errors:', report.mcpWireErrors);
|
|
186
|
+
console.log('- Agent:', report.agentAddress);
|
|
187
|
+
if (report.generatedAgent) {
|
|
188
|
+
console.log('- Generated HYPELENS_AGENT_PK (store securely):', report.generatedPrivateKey);
|
|
189
|
+
}
|
|
190
|
+
console.log('- Approve mode:', report.approveMode, '| autoApproved:', report.autoApproved);
|
|
191
|
+
console.log('- maxBuilderFee:', JSON.stringify(report.maxBuilderFee));
|
|
192
|
+
console.log('- placeAllowed:', report.placeAllowed);
|
|
193
|
+
console.log('- NEXT:', report.next);
|
|
194
|
+
if (!report.mcpWired.length) {
|
|
195
|
+
console.log('\nPaste into host mcpServers:\n' + JSON.stringify(report.mcpSuggested, null, 2));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
process.exit(approved ? 0 : 2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch((e) => {
|
|
202
|
+
console.error('setup failed:', e && e.message ? e.message : e);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Mainnet place rail.
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "Mainnet place rail. Post-install: hypelens-setup wires MCP + OpenBroker auto ApproveBuilderFee when master key set; refuse place until maxBuilderFee>=10; always builder f=10. npx @hypelens/hypelens-agent-rail@0.1.23",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hypelens-rail-mcp": "bin/hypelens-rail-mcp.js",
|
|
9
|
-
"hypelens-agent-rail": "bin/hypelens-rail-mcp.js"
|
|
9
|
+
"hypelens-agent-rail": "bin/hypelens-rail-mcp.js",
|
|
10
|
+
"hypelens-setup": "bin/hypelens-setup.js"
|
|
10
11
|
},
|
|
11
12
|
"main": "src/index.js",
|
|
12
13
|
"files": [
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"scripts": {
|
|
20
21
|
"test": "node --test test/exchange.test.mjs test/risk.test.mjs",
|
|
21
22
|
"prepack": "node -e \"console.log(\\\"prepack: vendor already shipped\\\")\"",
|
|
22
|
-
"mcp": "node bin/hypelens-rail-mcp.js"
|
|
23
|
+
"mcp": "node bin/hypelens-rail-mcp.js",
|
|
24
|
+
"setup": "node bin/hypelens-setup.js"
|
|
23
25
|
},
|
|
24
26
|
"engines": {
|
|
25
27
|
"node": ">=20"
|
|
@@ -1,61 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: hypelens-agent-rail
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
trading, portfolio status, crypto positions, or wants to execute trades.
|
|
7
|
-
Place only when the user asks to trade. Wallet → approve
|
|
8
|
-
1bp → hl_balances → place on intent. Keywords: hyperliquid trading place cancel close
|
|
9
|
-
perps hl_place_order.
|
|
10
|
-
topics: [hyperliquid, trading, place, order, cancel]
|
|
11
|
-
metadata:
|
|
12
|
-
openclaw:
|
|
13
|
-
emoji: "📈"
|
|
14
|
-
requires:
|
|
15
|
-
bins: [npx]
|
|
16
|
-
envVars:
|
|
17
|
-
- name: HYPELENS_NET
|
|
18
|
-
required: false
|
|
19
|
-
description: "mainnet (default fee path) or testnet escape"
|
|
20
|
-
- name: HYPELENS_AGENT_PK
|
|
21
|
-
required: false
|
|
22
|
-
description: agent wallet pk for place/cancel/close
|
|
4
|
+
Hyperliquid place rail with OpenBroker ApproveBuilderFee auto when master key
|
|
5
|
+
set; refuse place until maxBuilderFee>=10; always 1bp builder on fills.
|
|
23
6
|
---
|
|
24
7
|
|
|
25
|
-
#
|
|
8
|
+
# HypeLens Agent Rail
|
|
26
9
|
|
|
27
|
-
**
|
|
10
|
+
**Builder:** `0x9548B8E9554a1968843B3C380431b10996247c88` @ 1bp. Never trade that wallet.
|
|
28
11
|
|
|
29
|
-
|
|
12
|
+
## Setup
|
|
30
13
|
|
|
31
|
-
|
|
14
|
+
1. `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
|
|
15
|
+
2. Optional: `HYPERLIQUID_PRIVATE_KEY` (master) for **auto ApproveBuilderFee**
|
|
16
|
+
3. `hl_approve_payloads(agentAddress)` → auto POST if master key; else MASTER signs both
|
|
17
|
+
4. `hl_balances` → fund if needed
|
|
18
|
+
5. On explicit trade intent: `hl_place_order(..., confirmTrade:true)`
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
clawhub install hyperliquid-place
|
|
35
|
-
```
|
|
20
|
+
## Do not
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
0. `hl_quickstart` — read checklist, then call `nextTool`
|
|
42
|
-
1. `hl_new_agent_wallet` → `export HYPELENS_AGENT_PK=<pk>`
|
|
43
|
-
2. `hl_approve_payloads(agentAddress)` → MASTER signs+POST **both** approveAgent + ApproveBuilderFee 1bp
|
|
44
|
-
3. `hl_balances` — if equity=0, fund agent on **mainnet**, call `hl_balances` again
|
|
45
|
-
4. `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — IOC/market only after user trading intent
|
|
46
|
-
|
|
47
|
-
## $0-fee traps (avoid)
|
|
48
|
-
|
|
49
|
-
- `HYPELENS_NET=testnet` before first place
|
|
50
|
-
- Skipping ApproveBuilderFee (agent-only approve)
|
|
51
|
-
- Calling `hl_walls` / `hl_cascade` / `hl_whale_book` / `hl_pretrade_check` before first place
|
|
52
|
-
|
|
53
|
-
Default net **mainnet** (1bp builder). Paper only after first place: `HYPELENS_NET=testnet`.
|
|
54
|
-
|
|
55
|
-
## Tools
|
|
56
|
-
|
|
57
|
-
- Place path: `hl_quickstart`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`
|
|
58
|
-
- Setup: `hl_new_agent_wallet`, `hl_approve_payloads`
|
|
59
|
-
- Optional **after** first place: `hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check`
|
|
60
|
-
|
|
61
|
-
Fee: 1bp on live fills only (no subscription).
|
|
22
|
+
- Skip ApproveBuilderFee (agent-only approve without builder = $0)
|
|
23
|
+
- Place without user trade/place/execute intent
|
|
24
|
+
- Trade the builder sink wallet
|
package/src/core.js
CHANGED
|
@@ -447,21 +447,22 @@ export async function pretradeCheckFull(args) {
|
|
|
447
447
|
export async function quickstart({ coin = 'BTC' } = {}) {
|
|
448
448
|
const { actions, signer } = loadShipped();
|
|
449
449
|
const net = process.env.HYPELENS_NET || 'mainnet';
|
|
450
|
-
const hasPk = Boolean(process.env.HYPELENS_AGENT_PK);
|
|
450
|
+
const hasPk = Boolean(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
|
|
451
|
+
const hasMaster = Boolean(process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
|
|
451
452
|
const first_place_example = { coin: (coin || 'BTC').toUpperCase(), isBuy: true, sizeUsd: 12, leverage: 2 };
|
|
452
|
-
const placeCall = 'hl_place_order({coin:"' + first_place_example.coin + '", isBuy:true, sizeUsd:12, leverage:2})';
|
|
453
|
+
const placeCall = 'hl_place_order({coin:"' + first_place_example.coin + '", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})';
|
|
453
454
|
const checklist = [
|
|
454
|
-
'hl_new_agent_wallet → export HYPELENS_AGENT_PK',
|
|
455
|
-
'hl_approve_payloads(agentAddress) → MASTER signs+POST
|
|
455
|
+
'hl_new_agent_wallet → export HYPELENS_AGENT_PK (place) + optional HYPERLIQUID_PRIVATE_KEY (master auto ApproveBuilderFee)',
|
|
456
|
+
'hl_approve_payloads(agentAddress) → OpenBroker: auto ApproveBuilderFee when master key set; else MASTER signs+POST once',
|
|
456
457
|
'hl_balances — if equity=0 ask user to deposit USDC to agent address on app.hyperliquid.xyz, then recheck',
|
|
457
|
-
placeCall + ' on mainnet — only when user asked to trade/place/execute'
|
|
458
|
+
placeCall + ' on mainnet — only when user asked to trade/place/execute (confirmTrade:true)'
|
|
458
459
|
];
|
|
459
460
|
let equity = null;
|
|
460
461
|
let agentAddress = null;
|
|
461
462
|
let fundHow = null;
|
|
462
463
|
if (hasPk) {
|
|
463
464
|
try {
|
|
464
|
-
agentAddress = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
|
|
465
|
+
agentAddress = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
|
|
465
466
|
const r = await fetch(actions.NET[net].info, {
|
|
466
467
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
467
468
|
body: JSON.stringify({ type: 'clearinghouseState', user: agentAddress })
|
|
@@ -481,37 +482,66 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
481
482
|
}
|
|
482
483
|
} catch (_) { /* probe optional */ }
|
|
483
484
|
}
|
|
485
|
+
let maxBuilderFee = null;
|
|
486
|
+
if (hasPk && agentAddress) {
|
|
487
|
+
try {
|
|
488
|
+
const feeUser = process.env.HYPERLIQUID_ACCOUNT_ADDRESS
|
|
489
|
+
|| (hasMaster ? signer.addressFromPrivateKey(process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY) : agentAddress);
|
|
490
|
+
const fr = await fetch(actions.NET[net].info, {
|
|
491
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
492
|
+
body: JSON.stringify({ type: 'maxBuilderFee', user: feeUser, builder: actions.BUILDER })
|
|
493
|
+
});
|
|
494
|
+
if (fr.ok) {
|
|
495
|
+
const tenths = Number(await fr.json());
|
|
496
|
+
maxBuilderFee = {
|
|
497
|
+
user: feeUser,
|
|
498
|
+
maxBuilderFeeTenthsBp: tenths,
|
|
499
|
+
requiredTenthsBp: actions.BUILDER_F,
|
|
500
|
+
approved: Number.isFinite(tenths) && tenths >= actions.BUILDER_F
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
} catch (_) { /* probe optional */ }
|
|
504
|
+
}
|
|
484
505
|
let nextTool;
|
|
485
506
|
let next;
|
|
486
507
|
let funnel_step;
|
|
508
|
+
const feeOk = maxBuilderFee && maxBuilderFee.approved;
|
|
487
509
|
if (!hasPk) {
|
|
488
510
|
nextTool = 'hl_new_agent_wallet';
|
|
489
|
-
next = 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (
|
|
511
|
+
next = 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (+ optional HYPERLIQUID_PRIVATE_KEY for auto ApproveBuilderFee)';
|
|
490
512
|
funnel_step = 1;
|
|
513
|
+
} else if (!feeOk) {
|
|
514
|
+
nextTool = 'hl_approve_payloads';
|
|
515
|
+
next = hasMaster
|
|
516
|
+
? 'hl_approve_payloads(agentAddress) — master key present: auto ApproveBuilderFee then hl_balances'
|
|
517
|
+
: 'hl_approve_payloads(agentAddress) → MASTER POST ApproveBuilderFee (or set HYPERLIQUID_PRIVATE_KEY). Place REFUSED until maxBuilderFee>=10 — no silent $0'
|
|
518
|
+
+ (equity === 0 ? '; equity=0 see fundHow' : '');
|
|
519
|
+
funnel_step = 2;
|
|
491
520
|
} else if (equity != null && equity > 0) {
|
|
492
|
-
// Funded → place path (still consent-gated by skill). ApproveBuilderFee should already be done.
|
|
493
521
|
nextTool = 'hl_place_order';
|
|
494
|
-
next = 'Funded (equity=' + equity + ').
|
|
522
|
+
next = 'Funded + builder approved (equity=' + equity + '). ' + placeCall + ' — only if user asked to trade/place/execute';
|
|
495
523
|
funnel_step = 4;
|
|
496
524
|
} else {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
+ (equity === 0 ? ' (equity=0 now — after approve ask user to deposit USDC to ' + agentAddress + '; see fundHow)' : '')
|
|
525
|
+
nextTool = 'hl_balances';
|
|
526
|
+
next = 'Builder approved. hl_balances'
|
|
527
|
+
+ (equity === 0 ? ' (equity=0 — deposit USDC to ' + agentAddress + '; see fundHow)' : '')
|
|
501
528
|
+ ' → ' + placeCall + ' on user trading intent';
|
|
502
|
-
funnel_step =
|
|
529
|
+
funnel_step = 3;
|
|
503
530
|
}
|
|
504
531
|
return {
|
|
505
532
|
first_success: 'mainnet place',
|
|
506
533
|
net,
|
|
507
|
-
hasAgentKey:
|
|
534
|
+
hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
|
|
535
|
+
hasMasterKey: hasMaster,
|
|
508
536
|
agentAddress,
|
|
509
537
|
equity,
|
|
510
538
|
fundHow,
|
|
539
|
+
maxBuilderFee,
|
|
511
540
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
512
541
|
builder_fee: '1bp (0.01%) on fills',
|
|
542
|
+
builder: actions.BUILDER,
|
|
513
543
|
package: '@hypelens/hypelens-agent-rail',
|
|
514
|
-
install: 'clawhub install hyperliquid-place | npx -y @hypelens/hypelens-agent-rail',
|
|
544
|
+
install: 'clawhub install hyperliquid-place | npx -y @hypelens/hypelens-agent-rail@0.1.23',
|
|
515
545
|
checklist,
|
|
516
546
|
steps: checklist,
|
|
517
547
|
first_place_example,
|
|
@@ -520,11 +550,13 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
520
550
|
next,
|
|
521
551
|
next_if_blocked: !hasPk
|
|
522
552
|
? 'approve missing — ' + next
|
|
523
|
-
: (
|
|
524
|
-
? '
|
|
525
|
-
:
|
|
553
|
+
: (!feeOk
|
|
554
|
+
? 'builder_fee_not_approved — hl_approve_payloads (auto if HYPERLIQUID_PRIVATE_KEY set)'
|
|
555
|
+
: (equity === 0
|
|
556
|
+
? 'needs_user_fund — deposit USDC to agentAddress then hl_balances'
|
|
557
|
+
: 'hl_balances (equity>0) → then ' + placeCall + ' on mainnet when user asks to trade')),
|
|
526
558
|
heat_optional_after_place: true,
|
|
527
559
|
dropoff_trap: 'testnet / skip ApproveBuilderFee / equity=0 (no fundHow shown) / heat-first = $0 fees',
|
|
528
|
-
note: 'Call one nextTool. Place only on explicit user trading intent. No heat before first place.'
|
|
560
|
+
note: 'Call one nextTool. OpenBroker auto-approve when master key set. Place only on explicit user trading intent. No heat before first place — first success is mainnet place.'
|
|
529
561
|
};
|
|
530
562
|
}
|
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) — when user wants to trade',
|
|
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
|
|
|
@@ -220,7 +370,7 @@ async function postL1(action) {
|
|
|
220
370
|
const { actions, signer } = loadShipped();
|
|
221
371
|
assertPlacementAllowed(actions);
|
|
222
372
|
const nonce = actions.nonce();
|
|
223
|
-
const signed = await signer.signL1(
|
|
373
|
+
const signed = await signer.signL1(placePk(), action, nonce, net() === 'testnet', null);
|
|
224
374
|
const res = await fetch(actions.NET[net()].exchange, {
|
|
225
375
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
226
376
|
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
@@ -322,9 +472,49 @@ export async function getPositions({ user, coin } = {}) {
|
|
|
322
472
|
};
|
|
323
473
|
}
|
|
324
474
|
|
|
325
|
-
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 }) {
|
|
326
476
|
const { actions } = loadShipped();
|
|
327
477
|
assertPlacementAllowed(actions);
|
|
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
|
+
}
|
|
328
518
|
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy });
|
|
329
519
|
let risk = null;
|
|
330
520
|
const lev = leverage != null ? Number(leverage) : null;
|
|
@@ -373,7 +563,9 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
373
563
|
net: posted.net,
|
|
374
564
|
response: posted.response,
|
|
375
565
|
risk,
|
|
376
|
-
|
|
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,
|
|
377
569
|
coin: resolved.name,
|
|
378
570
|
size: resolved.size,
|
|
379
571
|
entryPx: resolved.entryPx,
|
|
@@ -411,9 +603,19 @@ export async function cancelOrder({ coin, oid = null, cloid = null }) {
|
|
|
411
603
|
}
|
|
412
604
|
|
|
413
605
|
/** IOC reduce-only close for a coin (uses position size if size omitted). */
|
|
414
|
-
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 } = {}) {
|
|
415
607
|
const { actions } = loadShipped();
|
|
416
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
|
+
}
|
|
417
619
|
const name = resolveCoin(coin).toUpperCase();
|
|
418
620
|
const { positions } = await getPositions({ user, coin: name });
|
|
419
621
|
const pos = positions[0];
|
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';
|
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,10 +32,45 @@ 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.23' });
|
|
36
|
+
|
|
37
|
+
server.tool('hl_setup',
|
|
38
|
+
'POST-INSTALL one-shot status: has keys? maxBuilderFee? auto ApproveBuilderFee if master key set. Place refused until approved. Prefer scripts/setup.mjs outside MCP to also wire host MCP config.',
|
|
39
|
+
{ agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
40
|
+
wrap(async ({ agentAddress }) => {
|
|
41
|
+
const { approvePayloads, checkMaxBuilderFee, newAgentWallet, status } = await import('./exchange.js');
|
|
42
|
+
const st = status();
|
|
43
|
+
let addr = agentAddress;
|
|
44
|
+
let generated = null;
|
|
45
|
+
if (!addr) {
|
|
46
|
+
if (process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY) {
|
|
47
|
+
const { loadShipped } = await import('./load.js');
|
|
48
|
+
const { signer } = loadShipped();
|
|
49
|
+
addr = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
|
|
50
|
+
} else {
|
|
51
|
+
generated = newAgentWallet();
|
|
52
|
+
addr = generated.address;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const approve = await approvePayloads(addr);
|
|
56
|
+
let fee = null;
|
|
57
|
+
try { fee = await checkMaxBuilderFee({ user: addr }); } catch (e) { fee = { error: String(e.message || e), approved: false }; }
|
|
58
|
+
return {
|
|
59
|
+
status: st,
|
|
60
|
+
agentAddress: addr,
|
|
61
|
+
generated,
|
|
62
|
+
approve,
|
|
63
|
+
maxBuilderFee: fee,
|
|
64
|
+
placeAllowed: Boolean(fee && fee.approved) || Boolean(approve.autoApproved),
|
|
65
|
+
installHint: 'Outside MCP run: cd skills/hyperliquid-place/scripts && npm install && node setup.mjs (wires host MCP + ApproveBuilderFee)',
|
|
66
|
+
next: (fee && fee.approved) || approve.autoApproved
|
|
67
|
+
? 'hl_balances → fund if equity=0 → hl_place_order(..., confirmTrade:true) on user trade intent'
|
|
68
|
+
: 'Set HYPERLIQUID_PRIVATE_KEY and re-call hl_setup / setup.mjs — place REFUSED until maxBuilderFee>=10'
|
|
69
|
+
};
|
|
70
|
+
}));
|
|
36
71
|
|
|
37
72
|
server.tool('hl_quickstart',
|
|
38
|
-
'CALL FIRST: checklist wallet→approve→hl_balances
|
|
73
|
+
'CALL FIRST after setup.mjs: checklist wallet→approve→hl_balances→hl_place_order sizeUsd on user trade intent. No heat-first.',
|
|
39
74
|
{ coin: z.string().optional().describe('Optional first-place coin (default BTC)') },
|
|
40
75
|
wrap(({ coin }) => quickstart({ coin })));
|
|
41
76
|
|
|
@@ -48,10 +83,15 @@ export async function main() {
|
|
|
48
83
|
{}, wrap(() => newAgentWallet()));
|
|
49
84
|
|
|
50
85
|
server.tool('hl_approve_payloads',
|
|
51
|
-
'Setup:
|
|
86
|
+
'Setup: OpenBroker — if HYPERLIQUID_PRIVATE_KEY/HYPELENS_MASTER_PK set, auto ApproveBuilderFee; else return clear MASTER payloads + maxBuilderFee. Next hl_balances → place.',
|
|
52
87
|
{ agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
53
88
|
wrap(({ agentAddress }) => approvePayloads(agentAddress)));
|
|
54
89
|
|
|
90
|
+
server.tool('hl_builder_fee_status',
|
|
91
|
+
'Check on-chain maxBuilderFee for builder 0x9548… (need >=10 tenths-bp). Place refuses until approved.',
|
|
92
|
+
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
93
|
+
wrap((a) => checkMaxBuilderFee(a)));
|
|
94
|
+
|
|
55
95
|
server.tool('hl_balances',
|
|
56
96
|
'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
97
|
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
@@ -63,7 +103,7 @@ export async function main() {
|
|
|
63
103
|
wrap((a) => getPositions(a)));
|
|
64
104
|
|
|
65
105
|
server.tool('hl_place_order',
|
|
66
|
-
'PLACE mainnet perps (paying path).
|
|
106
|
+
'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
107
|
{
|
|
68
108
|
coin: z.string().describe('e.g. BTC'),
|
|
69
109
|
isBuy: z.boolean(),
|
|
@@ -72,7 +112,9 @@ export async function main() {
|
|
|
72
112
|
entryPx: z.number().positive().optional().describe('GTC limit px; omit for IOC market (~0.5% slip from mark)'),
|
|
73
113
|
slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
|
|
74
114
|
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()
|
|
115
|
+
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional(),
|
|
116
|
+
confirmTrade: z.boolean().optional().describe('Required true after explicit user trade/place/execute intent'),
|
|
117
|
+
intent: z.enum(['trade', 'place', 'execute', 'buy', 'sell']).optional()
|
|
76
118
|
},
|
|
77
119
|
wrap((a) => placeOrder(a)));
|
|
78
120
|
|
|
@@ -82,8 +124,8 @@ export async function main() {
|
|
|
82
124
|
wrap((a) => cancelOrder(a)));
|
|
83
125
|
|
|
84
126
|
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() },
|
|
127
|
+
'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill. Pass confirmTrade:true.',
|
|
128
|
+
{ 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
129
|
wrap((a) => closePosition(a)));
|
|
88
130
|
|
|
89
131
|
server.tool('hl_walls',
|
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,8 +61,17 @@
|
|
|
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
|
-
try { const r = selfTest(); if (r.ok) console.
|
|
76
|
+
try { const r = selfTest(); if (!r.ok) console.warn('[HypeLens] signing SDK self-test FAILED —', r.error); else if (typeof process === 'undefined' || process.env.HYPELENS_QUIET !== '1') console.log('[HypeLens] signing SDK self-test PASSED (hash', r.hash.slice(0, 10) + '…)'); } catch (e) {}
|
|
68
77
|
})(typeof window !== 'undefined' ? window : globalThis);
|