otto-intel-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/adapter/cdp-signer.d.ts +133 -0
- package/dist/adapter/cdp-signer.js +356 -0
- package/dist/adapter/cdp-signer.js.map +1 -0
- package/dist/adapter/chain.d.ts +36 -0
- package/dist/adapter/chain.js +65 -0
- package/dist/adapter/chain.js.map +1 -0
- package/dist/adapter/erc20.d.ts +39 -0
- package/dist/adapter/erc20.js +17 -0
- package/dist/adapter/erc20.js.map +1 -0
- package/dist/adapter/lifi-decode.d.ts +52 -0
- package/dist/adapter/lifi-decode.js +149 -0
- package/dist/adapter/lifi-decode.js.map +1 -0
- package/dist/adapter/refusal.d.ts +21 -0
- package/dist/adapter/refusal.js +55 -0
- package/dist/adapter/refusal.js.map +1 -0
- package/dist/adapter/sent-step.d.ts +15 -0
- package/dist/adapter/sent-step.js +6 -0
- package/dist/adapter/sent-step.js.map +1 -0
- package/dist/adapter/verify.d.ts +149 -0
- package/dist/adapter/verify.js +432 -0
- package/dist/adapter/verify.js.map +1 -0
- package/dist/adapter-cdp-index.d.ts +6 -0
- package/dist/adapter-cdp-index.js +7 -0
- package/dist/adapter-index.d.ts +15 -0
- package/dist/adapter-index.js +15 -0
- package/dist/artifact-id.d.ts +16 -0
- package/dist/artifact-id.js +60 -0
- package/dist/boot-redaction.d.ts +16 -0
- package/dist/boot-redaction.js +54 -0
- package/dist/data-source.d.ts +7 -0
- package/dist/data-source.js +2 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.js +221 -0
- package/dist/execution-config.d.ts +232 -0
- package/dist/execution-config.js +443 -0
- package/dist/execution-delegated-definition.d.ts +165 -0
- package/dist/execution-delegated-definition.js +116 -0
- package/dist/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/execution-delegation-admin-definition.js +170 -0
- package/dist/execution-delegation-admin.d.ts +74 -0
- package/dist/execution-delegation-admin.js +290 -0
- package/dist/execution-delegation-policy.d.ts +257 -0
- package/dist/execution-delegation-policy.js +279 -0
- package/dist/execution-delegation.d.ts +190 -0
- package/dist/execution-delegation.js +545 -0
- package/dist/execution-errors.d.ts +9 -0
- package/dist/execution-errors.js +128 -0
- package/dist/execution-index.d.ts +17 -0
- package/dist/execution-index.js +16 -0
- package/dist/execution-intent.d.ts +14 -0
- package/dist/execution-intent.js +36 -0
- package/dist/execution-registration.d.ts +113 -0
- package/dist/execution-registration.js +161 -0
- package/dist/execution-tool-definitions.d.ts +1103 -0
- package/dist/execution-tool-definitions.js +1051 -0
- package/dist/execution-tools.d.ts +85 -0
- package/dist/execution-tools.js +690 -0
- package/dist/execution-types.d.ts +274 -0
- package/dist/execution-types.js +157 -0
- package/dist/hyperliquid-info-client.d.ts +31 -0
- package/dist/hyperliquid-info-client.js +121 -0
- package/dist/hyperliquid-order-assertions.d.ts +89 -0
- package/dist/hyperliquid-order-assertions.js +325 -0
- package/dist/hyperliquid-order-builder.d.ts +47 -0
- package/dist/hyperliquid-order-builder.js +55 -0
- package/dist/lifi-bridge-assertions.d.ts +49 -0
- package/dist/lifi-bridge-assertions.js +303 -0
- package/dist/lifi-execution-client.d.ts +74 -0
- package/dist/lifi-execution-client.js +165 -0
- package/dist/lifi-fee-assertions.d.ts +32 -0
- package/dist/lifi-fee-assertions.js +103 -0
- package/dist/lifi-swap-assertions.d.ts +32 -0
- package/dist/lifi-swap-assertions.js +214 -0
- package/dist/local-config.d.ts +9 -0
- package/dist/local-config.js +45 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +44 -0
- package/dist/polymarket-clob-client.d.ts +73 -0
- package/dist/polymarket-clob-client.js +186 -0
- package/dist/polymarket-order-assertions.d.ts +60 -0
- package/dist/polymarket-order-assertions.js +273 -0
- package/dist/polymarket-order-builder.d.ts +43 -0
- package/dist/polymarket-order-builder.js +74 -0
- package/dist/prepared-artifacts.d.ts +45 -0
- package/dist/prepared-artifacts.js +92 -0
- package/dist/stdio-main.d.ts +2 -0
- package/dist/stdio-main.js +20 -0
- package/dist/tool-definitions.d.ts +20 -0
- package/dist/tool-definitions.js +162 -0
- package/dist/x402-read-source.d.ts +11 -0
- package/dist/x402-read-source.js +77 -0
- package/package.json +162 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { HYPERLIQUID_ACTION_KEY_ORDER, HYPERLIQUID_AGENT_EIP712_FIELDS, HYPERLIQUID_AGENT_PRIMARY_TYPE, HYPERLIQUID_EIP712_DOMAIN_NAME, HYPERLIQUID_EIP712_DOMAIN_VERSION, HYPERLIQUID_EIP712_VERIFYING_CONTRACT, HYPERLIQUID_L1_SIGNATURE_CHAIN_ID, HYPERLIQUID_MAINNET_AGENT_SOURCE, HYPERLIQUID_ORDER_KEY_ORDER, HYPERLIQUID_PERP_MAX_DECIMALS, HYPERLIQUID_PRICE_MAX_SIGNIFICANT_DIGITS, HYPERLIQUID_TRIGGER_KEY_ORDER, } from './execution-config.js';
|
|
2
|
+
import { refuse } from './execution-errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Assert-what-you-built for the Hyperliquid rail (spec §8.2): every check below runs on the action the
|
|
5
|
+
* venue's own normalizer actually produced — the object whose msgpack bytes enter the digest the
|
|
6
|
+
* user's signer will sign — never on the values this constructor intended to build.
|
|
7
|
+
*/
|
|
8
|
+
function orderRefuse(message) {
|
|
9
|
+
return refuse('HYPERLIQUID_ORDER_INVALID', message);
|
|
10
|
+
}
|
|
11
|
+
function marketRefuse(message) {
|
|
12
|
+
return refuse('UNSUPPORTED_HYPERLIQUID_MARKET', message);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse an exact decimal string. The caller's string form is authoritative and is what lands in the
|
|
16
|
+
* signed action verbatim: no float ever touches a price or a size here.
|
|
17
|
+
*
|
|
18
|
+
* This is the deliberate divergence from the trade-execution agent, which formats prices through
|
|
19
|
+
* `parseFloat(price.toPrecision(5))` and `Math.floor(price * 10 ** d) / 10 ** d`
|
|
20
|
+
* (`hyperliquid-client.ts:55,1291`) and sizes through `size.toFixed(szDecimals)`. Those ROUND, and two
|
|
21
|
+
* of them round differently from each other. Rounding is acceptable when a caller's intent is "fill me
|
|
22
|
+
* around here" and the round trip is milliseconds; it is not acceptable in a prepare-only artifact the
|
|
23
|
+
* caller signs later, because the price they signed would not be the price they named. So the grid is
|
|
24
|
+
* extracted as a VALIDATION and an off-grid request is refused by name.
|
|
25
|
+
*/
|
|
26
|
+
export function hyperliquidDecimal(value) {
|
|
27
|
+
const match = /^(0|[1-9]\d*)(?:\.(\d*[1-9]))?$/.exec(value);
|
|
28
|
+
if (!match) {
|
|
29
|
+
marketRefuse(`${value} is not a normalized decimal (no leading zeros, no trailing zeros after the point)`);
|
|
30
|
+
}
|
|
31
|
+
const [, whole, fraction] = match;
|
|
32
|
+
const digits = `${whole}${fraction ?? ''}`.replace(/^0+/, '');
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
raw: value,
|
|
35
|
+
decimals: fraction?.length ?? 0,
|
|
36
|
+
isInteger: fraction === undefined,
|
|
37
|
+
significantDigits: digits.length,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Exact scaled integer for comparison. Refuses rather than truncating excess precision. */
|
|
41
|
+
export function hyperliquidScaledUnits(value, decimals) {
|
|
42
|
+
const parsed = hyperliquidDecimal(value);
|
|
43
|
+
if (parsed.decimals > decimals) {
|
|
44
|
+
orderRefuse(`${value} carries more precision than ${decimals} decimal places`);
|
|
45
|
+
}
|
|
46
|
+
const [whole, fraction = ''] = value.split('.');
|
|
47
|
+
return BigInt(`${whole}${fraction.padEnd(decimals, '0')}`);
|
|
48
|
+
}
|
|
49
|
+
/** Fractional digits the venue accepts in a perp price for this asset: `MAX_DECIMALS - szDecimals`. */
|
|
50
|
+
export function hyperliquidMaxPriceDecimals(szDecimals) {
|
|
51
|
+
return Math.max(HYPERLIQUID_PERP_MAX_DECIMALS - szDecimals, 0);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The venue's perp price grid: at most `MAX_DECIMALS - szDecimals` fractional digits AND at most 5
|
|
55
|
+
* significant digits, with integer prices always accepted regardless of significant figures.
|
|
56
|
+
*/
|
|
57
|
+
export function assertHyperliquidPriceOnGrid(price, szDecimals) {
|
|
58
|
+
const parsed = hyperliquidDecimal(price);
|
|
59
|
+
if (parsed.raw === '0')
|
|
60
|
+
marketRefuse('price must be greater than zero');
|
|
61
|
+
const maxDecimals = hyperliquidMaxPriceDecimals(szDecimals);
|
|
62
|
+
if (parsed.decimals > maxDecimals) {
|
|
63
|
+
marketRefuse(`price ${price} carries ${parsed.decimals} decimal places; this perp accepts at most ${maxDecimals} (6 - szDecimals ${szDecimals})`);
|
|
64
|
+
}
|
|
65
|
+
if (!parsed.isInteger && parsed.significantDigits > HYPERLIQUID_PRICE_MAX_SIGNIFICANT_DIGITS) {
|
|
66
|
+
marketRefuse(`price ${price} carries ${parsed.significantDigits} significant digits; the venue accepts at most ${HYPERLIQUID_PRICE_MAX_SIGNIFICANT_DIGITS} for a non-integer price`);
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
/** The venue's size grid: at most `szDecimals` fractional digits, strictly positive. */
|
|
71
|
+
export function assertHyperliquidSizeOnGrid(size, szDecimals) {
|
|
72
|
+
const parsed = hyperliquidDecimal(size);
|
|
73
|
+
if (parsed.raw === '0')
|
|
74
|
+
marketRefuse('size must be greater than zero');
|
|
75
|
+
if (parsed.decimals > szDecimals) {
|
|
76
|
+
marketRefuse(`size ${size} carries ${parsed.decimals} decimal places; this perp accepts at most szDecimals ${szDecimals}`);
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A take-profit below the entry on a long — or above it on a short — is not a take-profit: it is an
|
|
82
|
+
* instruction to close at a loss the moment the entry fills, and the same inversion on the stop-loss
|
|
83
|
+
* leg arms an immediate close. The venue accepts both, and the trade-execution agent does not check
|
|
84
|
+
* either (`hyperliquid-client.ts:1829,1851`). An inverted bracket is a defect in the REQUEST, so
|
|
85
|
+
* naming it is P0-clean (§7.0): a coherent bracket always constructs.
|
|
86
|
+
*/
|
|
87
|
+
export function assertHyperliquidBracketCoherent(input) {
|
|
88
|
+
const decimals = hyperliquidMaxPriceDecimals(input.szDecimals);
|
|
89
|
+
const entry = hyperliquidScaledUnits(input.price, decimals);
|
|
90
|
+
if (input.takeProfitPrice !== undefined) {
|
|
91
|
+
const takeProfit = hyperliquidScaledUnits(input.takeProfitPrice, decimals);
|
|
92
|
+
const coherent = input.isBuy ? takeProfit > entry : takeProfit < entry;
|
|
93
|
+
if (!coherent) {
|
|
94
|
+
marketRefuse(`take profit ${input.takeProfitPrice} is not ${input.isBuy ? 'above' : 'below'} the ${input.isBuy ? 'long' : 'short'} entry ${input.price}; it would close the position at a loss on fill`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (input.stopLossPrice !== undefined) {
|
|
98
|
+
const stopLoss = hyperliquidScaledUnits(input.stopLossPrice, decimals);
|
|
99
|
+
const coherent = input.isBuy ? stopLoss < entry : stopLoss > entry;
|
|
100
|
+
if (!coherent) {
|
|
101
|
+
marketRefuse(`stop loss ${input.stopLossPrice} is not ${input.isBuy ? 'below' : 'above'} the ${input.isBuy ? 'long' : 'short'} entry ${input.price}; it would trigger immediately on fill`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------------------------
|
|
106
|
+
// Decode-back over the SDK-normalized action
|
|
107
|
+
// ---------------------------------------------------------------------------------------------
|
|
108
|
+
const CLIENT_ORDER_ID_PATTERN = /^0x[0-9a-f]{32}$/;
|
|
109
|
+
function keyOrderOf(value, expected, what) {
|
|
110
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
111
|
+
orderRefuse(`constructed Hyperliquid ${what} is not an object`);
|
|
112
|
+
}
|
|
113
|
+
const actual = Object.keys(value);
|
|
114
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
115
|
+
// msgpack encodes keys in insertion order, so this is not cosmetic: a reordered action hashes to
|
|
116
|
+
// a different connectionId and the signature the user produced would authorize nothing.
|
|
117
|
+
orderRefuse(`constructed Hyperliquid ${what} key order is [${actual.join(',')}], not the canonical [${expected.join(',')}]`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function requiredString(source, key, what) {
|
|
121
|
+
const value = source[key];
|
|
122
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
123
|
+
orderRefuse(`constructed Hyperliquid ${what} is missing a string ${key}`);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function requiredBoolean(source, key, what) {
|
|
128
|
+
const value = source[key];
|
|
129
|
+
if (typeof value !== 'boolean') {
|
|
130
|
+
orderRefuse(`constructed Hyperliquid ${what} is missing a boolean ${key}`);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Decode the normalizer's output back into a strictly typed action and assert every fund-critical
|
|
136
|
+
* field: the positional asset id, the exact price and size strings, the side, reduce-only, the trigger
|
|
137
|
+
* shape, the per-leg client order ids, the grouping, the canonical key order, and the ABSENCE of the
|
|
138
|
+
* builder key (the ruled valueless state, spec §7.2).
|
|
139
|
+
*/
|
|
140
|
+
export function decodeAndAssertHyperliquidAction(input) {
|
|
141
|
+
const raw = input.built.action;
|
|
142
|
+
keyOrderOf(raw, HYPERLIQUID_ACTION_KEY_ORDER, 'action');
|
|
143
|
+
if (raw.type !== 'order') {
|
|
144
|
+
orderRefuse('constructed Hyperliquid action is not an order action');
|
|
145
|
+
}
|
|
146
|
+
// The builder key carries Otto's attribution and its VALUE is an open Founder ruling (spec §7.2).
|
|
147
|
+
// Its absence is therefore asserted, not tolerated: a builder key here would mean an unruled
|
|
148
|
+
// address or fee rate had entered the bytes the user signs.
|
|
149
|
+
if ('builder' in raw) {
|
|
150
|
+
orderRefuse('constructed Hyperliquid action carries a builder field; the builder value is unruled until the Step-0 approveBuilderFee reversal lands');
|
|
151
|
+
}
|
|
152
|
+
if (raw.grouping !== input.grouping) {
|
|
153
|
+
orderRefuse(`constructed Hyperliquid action grouping is ${String(raw.grouping)}, not the expected ${input.grouping}`);
|
|
154
|
+
}
|
|
155
|
+
const orders = raw.orders;
|
|
156
|
+
if (!Array.isArray(orders) || orders.length !== input.legs.length) {
|
|
157
|
+
orderRefuse(`constructed Hyperliquid action carries ${Array.isArray(orders) ? orders.length : 0} legs, not the expected ${input.legs.length}`);
|
|
158
|
+
}
|
|
159
|
+
const seenClientOrderIds = new Set();
|
|
160
|
+
const decodedLegs = [];
|
|
161
|
+
for (const [index, expected] of input.legs.entries()) {
|
|
162
|
+
const leg = orders[index];
|
|
163
|
+
keyOrderOf(leg, [...HYPERLIQUID_ORDER_KEY_ORDER, 'c'], `${expected.role} leg`);
|
|
164
|
+
if (leg.a !== input.asset.assetId) {
|
|
165
|
+
// The asset id is the POSITION in the live perp universe. A wrong index is a different market.
|
|
166
|
+
refuse('ACCOUNT_BINDING_MISMATCH', `constructed Hyperliquid ${expected.role} leg names asset id ${String(leg.a)}, not ${input.asset.assetId} (${input.asset.name})`);
|
|
167
|
+
}
|
|
168
|
+
if (requiredBoolean(leg, 'b', `${expected.role} leg`) !== expected.isBuy) {
|
|
169
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg is on the wrong side`);
|
|
170
|
+
}
|
|
171
|
+
const price = requiredString(leg, 'p', `${expected.role} leg`);
|
|
172
|
+
const size = requiredString(leg, 's', `${expected.role} leg`);
|
|
173
|
+
if (price !== expected.price) {
|
|
174
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg is priced ${price}, not the requested ${expected.price}`);
|
|
175
|
+
}
|
|
176
|
+
if (size !== expected.size) {
|
|
177
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg is sized ${size}, not the requested ${expected.size}`);
|
|
178
|
+
}
|
|
179
|
+
if (requiredBoolean(leg, 'r', `${expected.role} leg`) !== expected.reduceOnly) {
|
|
180
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg has the wrong reduce-only flag`);
|
|
181
|
+
}
|
|
182
|
+
const clientOrderId = requiredString(leg, 'c', `${expected.role} leg`);
|
|
183
|
+
if (!CLIENT_ORDER_ID_PATTERN.test(clientOrderId)) {
|
|
184
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg carries a malformed client order id`);
|
|
185
|
+
}
|
|
186
|
+
if (clientOrderId !== expected.clientOrderId) {
|
|
187
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg carries a client order id this constructor did not mint`);
|
|
188
|
+
}
|
|
189
|
+
if (seenClientOrderIds.has(clientOrderId)) {
|
|
190
|
+
// Two legs sharing an id is exactly what forces a positional read of the venue's response, the
|
|
191
|
+
// failure mode `hyperliquid-client.ts:1801` documents.
|
|
192
|
+
orderRefuse('constructed Hyperliquid action reuses a client order id across legs');
|
|
193
|
+
}
|
|
194
|
+
seenClientOrderIds.add(clientOrderId);
|
|
195
|
+
const orderType = leg.t;
|
|
196
|
+
if (orderType === null || typeof orderType !== 'object') {
|
|
197
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg has no order type`);
|
|
198
|
+
}
|
|
199
|
+
const typeKeys = Object.keys(orderType);
|
|
200
|
+
if (expected.trigger) {
|
|
201
|
+
if (typeKeys.length !== 1 || typeKeys[0] !== 'trigger') {
|
|
202
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg is not a trigger order`);
|
|
203
|
+
}
|
|
204
|
+
const trigger = orderType.trigger;
|
|
205
|
+
keyOrderOf(trigger, HYPERLIQUID_TRIGGER_KEY_ORDER, `${expected.role} trigger`);
|
|
206
|
+
const triggerRecord = trigger;
|
|
207
|
+
// `isMarket: true` is what makes the trigger close the position at the market once armed; a
|
|
208
|
+
// limit trigger can arm and never fill, leaving the bracket the caller asked for unenforced.
|
|
209
|
+
if (triggerRecord.isMarket !== true) {
|
|
210
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} trigger is not a market trigger`);
|
|
211
|
+
}
|
|
212
|
+
if (requiredString(triggerRecord, 'triggerPx', `${expected.role} trigger`) !== expected.trigger.triggerPx) {
|
|
213
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} trigger price is not the requested ${expected.trigger.triggerPx}`);
|
|
214
|
+
}
|
|
215
|
+
if (triggerRecord.tpsl !== expected.trigger.tpsl) {
|
|
216
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} trigger is not tagged ${expected.trigger.tpsl}`);
|
|
217
|
+
}
|
|
218
|
+
decodedLegs.push({
|
|
219
|
+
a: input.asset.assetId,
|
|
220
|
+
b: expected.isBuy,
|
|
221
|
+
p: price,
|
|
222
|
+
s: size,
|
|
223
|
+
r: expected.reduceOnly,
|
|
224
|
+
t: {
|
|
225
|
+
trigger: {
|
|
226
|
+
isMarket: true,
|
|
227
|
+
triggerPx: expected.trigger.triggerPx,
|
|
228
|
+
tpsl: expected.trigger.tpsl,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
c: clientOrderId,
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (typeKeys.length !== 1 || typeKeys[0] !== 'limit') {
|
|
236
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg is not a limit order`);
|
|
237
|
+
}
|
|
238
|
+
const limit = orderType.limit;
|
|
239
|
+
keyOrderOf(limit, ['tif'], `${expected.role} limit`);
|
|
240
|
+
if (limit.tif !== expected.tif) {
|
|
241
|
+
orderRefuse(`constructed Hyperliquid ${expected.role} leg time-in-force is not the requested ${String(expected.tif)}`);
|
|
242
|
+
}
|
|
243
|
+
decodedLegs.push({
|
|
244
|
+
a: input.asset.assetId,
|
|
245
|
+
b: expected.isBuy,
|
|
246
|
+
p: price,
|
|
247
|
+
s: size,
|
|
248
|
+
r: expected.reduceOnly,
|
|
249
|
+
t: { limit: { tif: expected.tif } },
|
|
250
|
+
c: clientOrderId,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// Rebuild the action from the asserted values, in the canonical key order, and require it to
|
|
254
|
+
// serialize byte-for-byte identically to what the venue's normalizer produced. msgpack is
|
|
255
|
+
// order-sensitive, so equality of the JSON SERIALIZATION — not merely of the values — is what ties
|
|
256
|
+
// the published action to the bytes the signature will cover.
|
|
257
|
+
//
|
|
258
|
+
// Honest note on this specific line: it has no reachable mutant today. Every field, key order, and
|
|
259
|
+
// key-set check above runs first, so any single-field mutation of a real normalized action is killed
|
|
260
|
+
// before reaching here (`hyperliquid-order-mutation-proof.ts` demonstrates that for 26 action
|
|
261
|
+
// mutants). It is kept deliberately as a forward guard: the moment a future field is added to the
|
|
262
|
+
// rebuild without a matching decode-back check, this line is what fails instead of shipping an
|
|
263
|
+
// artifact whose published action differs from the asserted one. The reddenable part of
|
|
264
|
+
// `hyperliquid_action_matches_venue_schema` is the venue-schema parse itself, which the mutation
|
|
265
|
+
// proof exercises directly against `buildUnsignedHyperliquidOrderAction`.
|
|
266
|
+
const action = {
|
|
267
|
+
type: 'order',
|
|
268
|
+
orders: decodedLegs,
|
|
269
|
+
grouping: input.grouping,
|
|
270
|
+
};
|
|
271
|
+
if (JSON.stringify(action) !== JSON.stringify(raw)) {
|
|
272
|
+
orderRefuse('re-derived Hyperliquid action does not serialize identically to the action the venue schema produced');
|
|
273
|
+
}
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
action,
|
|
276
|
+
assertions: Object.freeze([
|
|
277
|
+
{ id: 'hyperliquid_asset_index_from_live_meta', result: 'passed' },
|
|
278
|
+
{ id: 'hyperliquid_price_on_venue_grid', result: 'passed' },
|
|
279
|
+
{ id: 'hyperliquid_size_within_sz_decimals', result: 'passed' },
|
|
280
|
+
{ id: 'hyperliquid_order_sides_bound', result: 'passed' },
|
|
281
|
+
{ id: 'hyperliquid_grouping_matches_legs', result: 'passed' },
|
|
282
|
+
{ id: 'hyperliquid_client_order_ids_distinct', result: 'passed' },
|
|
283
|
+
{ id: 'hyperliquid_action_key_order_canonical', result: 'passed' },
|
|
284
|
+
{ id: 'hyperliquid_action_matches_venue_schema', result: 'passed' },
|
|
285
|
+
{ id: 'hyperliquid_builder_absent_pending_step0', result: 'passed' },
|
|
286
|
+
{ id: 'hyperliquid_action_carries_no_nonce', result: 'passed' },
|
|
287
|
+
]),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The signing declaration: exactly the domain, struct, primary type, and `source` the venue's own
|
|
292
|
+
* signer uses (`@nktkas/hyperliquid` signing/mod.js:170-189), plus the `connectionId` recipe.
|
|
293
|
+
*
|
|
294
|
+
* `connectionId` itself is NOT published, and cannot be: it is
|
|
295
|
+
* `keccak256(msgpack(action) ++ nonce ++ vault ++ expiresAfter)` and the nonce is adapter-owned
|
|
296
|
+
* (§6/§10). Pre-committing a nonce here is precisely what would turn a correct artifact into a stale
|
|
297
|
+
* or replayable one, so the artifact publishes the preimage layout and leaves the nonce to the signer.
|
|
298
|
+
*/
|
|
299
|
+
export function hyperliquidSigningDeclaration() {
|
|
300
|
+
return {
|
|
301
|
+
scheme: 'hyperliquid_l1_agent_eip712',
|
|
302
|
+
primary_type: HYPERLIQUID_AGENT_PRIMARY_TYPE,
|
|
303
|
+
domain: {
|
|
304
|
+
name: HYPERLIQUID_EIP712_DOMAIN_NAME,
|
|
305
|
+
version: HYPERLIQUID_EIP712_DOMAIN_VERSION,
|
|
306
|
+
chainId: HYPERLIQUID_L1_SIGNATURE_CHAIN_ID,
|
|
307
|
+
verifyingContract: HYPERLIQUID_EIP712_VERIFYING_CONTRACT,
|
|
308
|
+
},
|
|
309
|
+
types: { Agent: HYPERLIQUID_AGENT_EIP712_FIELDS.map((field) => ({ ...field })) },
|
|
310
|
+
source: HYPERLIQUID_MAINNET_AGENT_SOURCE,
|
|
311
|
+
connection_id: {
|
|
312
|
+
hash: 'keccak256',
|
|
313
|
+
preimage: [
|
|
314
|
+
'msgpack(action)',
|
|
315
|
+
'nonce_uint64_be',
|
|
316
|
+
'vault_address_marker',
|
|
317
|
+
'vault_address_bytes',
|
|
318
|
+
'expires_after_marker',
|
|
319
|
+
'expires_after_uint64_be',
|
|
320
|
+
],
|
|
321
|
+
nonce_owner: 'adapter',
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=hyperliquid-order-assertions.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type HyperliquidOrderGrouping, type HyperliquidTimeInForce } from './execution-config.js';
|
|
2
|
+
export type HyperliquidOrderKind = {
|
|
3
|
+
readonly limit: {
|
|
4
|
+
readonly tif: HyperliquidTimeInForce;
|
|
5
|
+
};
|
|
6
|
+
} | {
|
|
7
|
+
readonly trigger: {
|
|
8
|
+
readonly isMarket: boolean;
|
|
9
|
+
readonly triggerPx: string;
|
|
10
|
+
readonly tpsl: 'tp' | 'sl';
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export interface HyperliquidOrderLegRequest {
|
|
14
|
+
/** Positional index in the live perp universe; never a value this repo invents. */
|
|
15
|
+
readonly assetId: number;
|
|
16
|
+
readonly isBuy: boolean;
|
|
17
|
+
/** Exact decimal price string, already validated against the venue grid by the caller. */
|
|
18
|
+
readonly price: string;
|
|
19
|
+
/** Exact decimal size string, already validated against `szDecimals` by the caller. */
|
|
20
|
+
readonly size: string;
|
|
21
|
+
readonly reduceOnly: boolean;
|
|
22
|
+
readonly orderType: HyperliquidOrderKind;
|
|
23
|
+
/** Constructor-minted, one distinct id per leg, so no leg is correlated positionally. */
|
|
24
|
+
readonly clientOrderId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* NOTE the fields this request type deliberately does NOT have: `builder`, `nonce`, `signature`,
|
|
28
|
+
* `vaultAddress`, `expiresAfter`. Attribution has no caller seam (§5) and is valueless today (§7.2);
|
|
29
|
+
* the nonce and the final request serialization are adapter-owned (§6/§10); the vault address is a
|
|
30
|
+
* declared profile fact recorded in the envelope, not something the action carries.
|
|
31
|
+
*/
|
|
32
|
+
export interface UnsignedHyperliquidActionRequest {
|
|
33
|
+
readonly legs: readonly HyperliquidOrderLegRequest[];
|
|
34
|
+
readonly grouping: HyperliquidOrderGrouping;
|
|
35
|
+
}
|
|
36
|
+
export interface UnsignedHyperliquidAction {
|
|
37
|
+
/** The SDK's own normalized action, in the SDK's own key order. */
|
|
38
|
+
readonly action: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run the venue's own normalizer over the requested legs and return the canonical action.
|
|
42
|
+
*
|
|
43
|
+
* `v.object` DROPS keys the venue schema does not declare, which is the "canonicalize declared
|
|
44
|
+
* fields, strip undeclared" rule the CDP sitting made mandatory (spec §12, C2x row) applied at the
|
|
45
|
+
* action layer: whatever this function returns contains only fields the venue itself declares.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildUnsignedHyperliquidOrderAction(request: UnsignedHyperliquidActionRequest): UnsignedHyperliquidAction;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { OrderRequest } from '@nktkas/hyperliquid/api/exchange';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import { refuse } from './execution-errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Drives `@nktkas/hyperliquid`'s OWN action normalizer and stops before signing (spec §7.2: extract
|
|
6
|
+
* by behavior from `hyperliquid-client.ts`, excluding wallet creation, signing, and submission).
|
|
7
|
+
*
|
|
8
|
+
* The prepare-only split is structural, not a matter of discipline. This module imports exactly one
|
|
9
|
+
* thing from the SDK — the `order` action's valibot schema — and nothing from `signing/mod.js`,
|
|
10
|
+
* `HttpTransport`, or `ExchangeClient`. There is no wallet in scope, so there is nothing here that
|
|
11
|
+
* could sign or submit, and the root-secret-derived agent wallet (`hyperliquid-client.ts:558`) is not
|
|
12
|
+
* reachable from this file at all.
|
|
13
|
+
*
|
|
14
|
+
* Why the SDK's normalizer rather than a hand-built object: the signature commits to
|
|
15
|
+
* `keccak256(msgpack(action) ++ …)`, and msgpack serializes object keys in INSERTION order. The
|
|
16
|
+
* normalizer's output key order therefore IS part of the signed bytes. Running the venue's own schema
|
|
17
|
+
* is what makes the artifact's action byte-identical to what the SDK would have sent, instead of
|
|
18
|
+
* merely value-equal to it.
|
|
19
|
+
*/
|
|
20
|
+
/** The venue's action schema minus the discriminator, exactly as the SDK's `order()` derives it. */
|
|
21
|
+
const HYPERLIQUID_ORDER_PARAMETERS = v.omit(v.object(OrderRequest.entries.action.entries), ['type']);
|
|
22
|
+
/**
|
|
23
|
+
* Run the venue's own normalizer over the requested legs and return the canonical action.
|
|
24
|
+
*
|
|
25
|
+
* `v.object` DROPS keys the venue schema does not declare, which is the "canonicalize declared
|
|
26
|
+
* fields, strip undeclared" rule the CDP sitting made mandatory (spec §12, C2x row) applied at the
|
|
27
|
+
* action layer: whatever this function returns contains only fields the venue itself declares.
|
|
28
|
+
*/
|
|
29
|
+
export function buildUnsignedHyperliquidOrderAction(request) {
|
|
30
|
+
if (request.legs.length === 0) {
|
|
31
|
+
return refuse('HYPERLIQUID_ORDER_INVALID', 'Hyperliquid action must carry at least one order leg');
|
|
32
|
+
}
|
|
33
|
+
let normalized;
|
|
34
|
+
try {
|
|
35
|
+
normalized = v.parse(HYPERLIQUID_ORDER_PARAMETERS, {
|
|
36
|
+
orders: request.legs.map((leg) => ({
|
|
37
|
+
a: leg.assetId,
|
|
38
|
+
b: leg.isBuy,
|
|
39
|
+
p: leg.price,
|
|
40
|
+
s: leg.size,
|
|
41
|
+
r: leg.reduceOnly,
|
|
42
|
+
t: leg.orderType,
|
|
43
|
+
c: leg.clientOrderId,
|
|
44
|
+
})),
|
|
45
|
+
grouping: request.grouping,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return refuse('HYPERLIQUID_ORDER_INVALID', `Hyperliquid order action failed the venue schema: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
50
|
+
}
|
|
51
|
+
// `type` first is the SDK's own composition (`api/exchange/_methods/order.js:193`), and it is part
|
|
52
|
+
// of the msgpacked bytes, so it is reproduced here rather than appended afterwards.
|
|
53
|
+
return Object.freeze({ action: { type: 'order', ...normalized } });
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=hyperliquid-order-builder.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Address, type Hex } from 'viem';
|
|
2
|
+
import { type ExecutionServiceConfig } from './execution-config.js';
|
|
3
|
+
import type { AccountProfile, ExecutionAssertion } from './execution-types.js';
|
|
4
|
+
import type { LiFiQuote } from './lifi-execution-client.js';
|
|
5
|
+
/** The enumerated v1 bridge facet. Anything else — eco, stargate, a plain no-swap layout, or a
|
|
6
|
+
* post-redeploy selector — is outside the reviewed set and refused by name. */
|
|
7
|
+
export declare const LIFI_BRIDGE_SELECTORS: Readonly<{
|
|
8
|
+
polymerCctp: Hex;
|
|
9
|
+
}>;
|
|
10
|
+
export interface LiFiBridgeAssertionInput {
|
|
11
|
+
readonly quote: LiFiQuote;
|
|
12
|
+
readonly profile: AccountProfile;
|
|
13
|
+
readonly fromChainId: number;
|
|
14
|
+
readonly toChainId: number;
|
|
15
|
+
readonly fromToken: Address;
|
|
16
|
+
readonly toToken: Address;
|
|
17
|
+
readonly fromAmount: bigint;
|
|
18
|
+
/**
|
|
19
|
+
* Where the destination mint lands — `BridgeData.receiver`, the field CCTP actually mints to,
|
|
20
|
+
* and the quote's `toAddress`. It is deliberately NOT bound to the PolymerData refund field,
|
|
21
|
+
* which carries the sender: see the tuple comment. It need not equal `profile.address`.
|
|
22
|
+
*/
|
|
23
|
+
readonly destinationRecipient: Address;
|
|
24
|
+
readonly maxSlippageBps: number;
|
|
25
|
+
readonly minAmountOut?: bigint;
|
|
26
|
+
readonly config: ExecutionServiceConfig;
|
|
27
|
+
}
|
|
28
|
+
export interface AssertedLiFiBridge {
|
|
29
|
+
readonly approvalSpender: Address;
|
|
30
|
+
readonly transaction: {
|
|
31
|
+
readonly to: Address;
|
|
32
|
+
readonly value: '0';
|
|
33
|
+
readonly data: Hex;
|
|
34
|
+
};
|
|
35
|
+
/** The net amount the destination chain mints, after the reviewed fee legs. */
|
|
36
|
+
readonly destinationMinAmount: bigint;
|
|
37
|
+
readonly assertions: readonly ExecutionAssertion[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decode the constructed bridge calldata and assert every fund-safety bind, fail-closed.
|
|
41
|
+
*
|
|
42
|
+
* The destination binds matter more here than on a same-chain swap: a wrong destination recipient
|
|
43
|
+
* is an irreversible cross-chain loss, and the source-chain calldata is the last place it is
|
|
44
|
+
* checkable. CCTP burns the source USDC and mints canonical USDC to `BridgeData.receiver` on the
|
|
45
|
+
* destination, so the destination asset is same-token by construction once the facet and the
|
|
46
|
+
* enumerated route are pinned — that, plus every recipient-shaped field being bound, is what makes
|
|
47
|
+
* the destination side assertable from source calldata alone.
|
|
48
|
+
*/
|
|
49
|
+
export declare function decodeAndAssertLiFiBridge(input: LiFiBridgeAssertionInput): AssertedLiFiBridge;
|