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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/adapter/cdp-signer.d.ts +133 -0
  4. package/dist/adapter/cdp-signer.js +356 -0
  5. package/dist/adapter/cdp-signer.js.map +1 -0
  6. package/dist/adapter/chain.d.ts +36 -0
  7. package/dist/adapter/chain.js +65 -0
  8. package/dist/adapter/chain.js.map +1 -0
  9. package/dist/adapter/erc20.d.ts +39 -0
  10. package/dist/adapter/erc20.js +17 -0
  11. package/dist/adapter/erc20.js.map +1 -0
  12. package/dist/adapter/lifi-decode.d.ts +52 -0
  13. package/dist/adapter/lifi-decode.js +149 -0
  14. package/dist/adapter/lifi-decode.js.map +1 -0
  15. package/dist/adapter/refusal.d.ts +21 -0
  16. package/dist/adapter/refusal.js +55 -0
  17. package/dist/adapter/refusal.js.map +1 -0
  18. package/dist/adapter/sent-step.d.ts +15 -0
  19. package/dist/adapter/sent-step.js +6 -0
  20. package/dist/adapter/sent-step.js.map +1 -0
  21. package/dist/adapter/verify.d.ts +149 -0
  22. package/dist/adapter/verify.js +432 -0
  23. package/dist/adapter/verify.js.map +1 -0
  24. package/dist/adapter-cdp-index.d.ts +6 -0
  25. package/dist/adapter-cdp-index.js +7 -0
  26. package/dist/adapter-index.d.ts +15 -0
  27. package/dist/adapter-index.js +15 -0
  28. package/dist/artifact-id.d.ts +16 -0
  29. package/dist/artifact-id.js +60 -0
  30. package/dist/boot-redaction.d.ts +16 -0
  31. package/dist/boot-redaction.js +54 -0
  32. package/dist/data-source.d.ts +7 -0
  33. package/dist/data-source.js +2 -0
  34. package/dist/errors.d.ts +70 -0
  35. package/dist/errors.js +221 -0
  36. package/dist/execution-config.d.ts +232 -0
  37. package/dist/execution-config.js +443 -0
  38. package/dist/execution-delegated-definition.d.ts +165 -0
  39. package/dist/execution-delegated-definition.js +116 -0
  40. package/dist/execution-delegation-admin-definition.d.ts +208 -0
  41. package/dist/execution-delegation-admin-definition.js +170 -0
  42. package/dist/execution-delegation-admin.d.ts +74 -0
  43. package/dist/execution-delegation-admin.js +290 -0
  44. package/dist/execution-delegation-policy.d.ts +257 -0
  45. package/dist/execution-delegation-policy.js +279 -0
  46. package/dist/execution-delegation.d.ts +190 -0
  47. package/dist/execution-delegation.js +545 -0
  48. package/dist/execution-errors.d.ts +9 -0
  49. package/dist/execution-errors.js +128 -0
  50. package/dist/execution-index.d.ts +17 -0
  51. package/dist/execution-index.js +16 -0
  52. package/dist/execution-intent.d.ts +14 -0
  53. package/dist/execution-intent.js +36 -0
  54. package/dist/execution-registration.d.ts +113 -0
  55. package/dist/execution-registration.js +161 -0
  56. package/dist/execution-tool-definitions.d.ts +1103 -0
  57. package/dist/execution-tool-definitions.js +1051 -0
  58. package/dist/execution-tools.d.ts +85 -0
  59. package/dist/execution-tools.js +690 -0
  60. package/dist/execution-types.d.ts +274 -0
  61. package/dist/execution-types.js +157 -0
  62. package/dist/hyperliquid-info-client.d.ts +31 -0
  63. package/dist/hyperliquid-info-client.js +121 -0
  64. package/dist/hyperliquid-order-assertions.d.ts +89 -0
  65. package/dist/hyperliquid-order-assertions.js +325 -0
  66. package/dist/hyperliquid-order-builder.d.ts +47 -0
  67. package/dist/hyperliquid-order-builder.js +55 -0
  68. package/dist/lifi-bridge-assertions.d.ts +49 -0
  69. package/dist/lifi-bridge-assertions.js +303 -0
  70. package/dist/lifi-execution-client.d.ts +74 -0
  71. package/dist/lifi-execution-client.js +165 -0
  72. package/dist/lifi-fee-assertions.d.ts +32 -0
  73. package/dist/lifi-fee-assertions.js +103 -0
  74. package/dist/lifi-swap-assertions.d.ts +32 -0
  75. package/dist/lifi-swap-assertions.js +214 -0
  76. package/dist/local-config.d.ts +9 -0
  77. package/dist/local-config.js +45 -0
  78. package/dist/mcp-server.d.ts +4 -0
  79. package/dist/mcp-server.js +44 -0
  80. package/dist/polymarket-clob-client.d.ts +73 -0
  81. package/dist/polymarket-clob-client.js +186 -0
  82. package/dist/polymarket-order-assertions.d.ts +60 -0
  83. package/dist/polymarket-order-assertions.js +273 -0
  84. package/dist/polymarket-order-builder.d.ts +43 -0
  85. package/dist/polymarket-order-builder.js +74 -0
  86. package/dist/prepared-artifacts.d.ts +45 -0
  87. package/dist/prepared-artifacts.js +92 -0
  88. package/dist/stdio-main.d.ts +2 -0
  89. package/dist/stdio-main.js +20 -0
  90. package/dist/tool-definitions.d.ts +20 -0
  91. package/dist/tool-definitions.js +162 -0
  92. package/dist/x402-read-source.d.ts +11 -0
  93. package/dist/x402-read-source.js +77 -0
  94. 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;