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,690 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { encodeFunctionData, getAddress, parseAbi } from 'viem';
3
+ import { attributedFeeAmount, HL_BUILDER_ATTRIBUTION_STATUS, HYPERLIQUID_L1_SIGNATURE_CHAIN_ID, isEnumeratedCctpBridgeRoute, reviewedLiFiFeeForwarders, loadExecutionServiceConfig, POLYMARKET_CHAIN_ID, } from './execution-config.js';
4
+ import { refuse } from './execution-errors.js';
5
+ import { createDelegationServicesFromEnv, } from './execution-delegation.js';
6
+ import { recordPreparedArtifact } from './prepared-artifacts.js';
7
+ import { swapIntentDigestInput } from './execution-intent.js';
8
+ import { BRIDGE_EXECUTION_ENVELOPE_SCHEMA, EXECUTION_ENVELOPE_SCHEMA, HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA, POLYMARKET_EXECUTION_ENVELOPE_SCHEMA, PREPARE_BRIDGE_INPUT_SCHEMA, PREPARE_PERP_ORDER_INPUT_SCHEMA, PREPARE_POLYMARKET_ORDER_INPUT_SCHEMA, PREPARE_SWAP_INPUT_SCHEMA, } from './execution-tool-definitions.js';
9
+ import { accountBinding, artifactCommitmentDigest, bridgeAccountBinding, createArtifactId, executionDigest, hyperliquidAccountBinding, polymarketAccountBinding, } from './execution-types.js';
10
+ import { RestHyperliquidInfoClient, } from './hyperliquid-info-client.js';
11
+ import { assertHyperliquidBracketCoherent, assertHyperliquidPriceOnGrid, assertHyperliquidSizeOnGrid, decodeAndAssertHyperliquidAction, hyperliquidSigningDeclaration, } from './hyperliquid-order-assertions.js';
12
+ import { buildUnsignedHyperliquidOrderAction, } from './hyperliquid-order-builder.js';
13
+ import { decodeAndAssertLiFiBridge } from './lifi-bridge-assertions.js';
14
+ import { RestLiFiQuoteClient } from './lifi-execution-client.js';
15
+ import { decodeAndAssertLiFiSwap } from './lifi-swap-assertions.js';
16
+ import { RestPolymarketClobClient } from './polymarket-clob-client.js';
17
+ import { assertPolymarketBuilderAttributionRuled, assertPolymarketPriceOnTick, decodeAndAssertPolymarketOrder, polymarketAtomicUnits, } from './polymarket-order-assertions.js';
18
+ import { buildUnsignedPolymarketOrder, } from './polymarket-order-builder.js';
19
+ const ERC20_APPROVE_ABI = parseAbi([
20
+ 'function approve(address spender, uint256 amount) returns (bool)',
21
+ ]);
22
+ export function createExecutionToolRuntime(config, overrides = {}) {
23
+ return Object.freeze({
24
+ config,
25
+ quoteClient: overrides.quoteClient ?? new RestLiFiQuoteClient(config),
26
+ clobClient: overrides.clobClient ?? new RestPolymarketClobClient(config),
27
+ polymarketOrderBuilder: overrides.polymarketOrderBuilder ?? buildUnsignedPolymarketOrder,
28
+ hyperliquidInfoClient: overrides.hyperliquidInfoClient ?? new RestHyperliquidInfoClient(config),
29
+ hyperliquidActionBuilder: overrides.hyperliquidActionBuilder ?? buildUnsignedHyperliquidOrderAction,
30
+ nowMs: overrides.nowMs ?? Date.now,
31
+ entropy: overrides.entropy ?? (() => randomBytes(10)),
32
+ clientOrderIdEntropy: overrides.clientOrderIdEntropy ?? (() => randomBytes(16)),
33
+ ...(overrides.delegation ? { delegation: overrides.delegation } : {}),
34
+ });
35
+ }
36
+ export function createExecutionToolRuntimeFromEnv(env = process.env) {
37
+ const config = loadExecutionServiceConfig(env);
38
+ return config ? createExecutionToolRuntime(config) : undefined;
39
+ }
40
+ /**
41
+ * The hosted boot: the prepare runtime plus, when the five delegation variables are set, the delegated
42
+ * services (the optional SDK loaded, the Base RPC chain id checked). Delegation configured while
43
+ * execution is switched off is a misconfiguration and refuses to boot rather than silently dropping
44
+ * the tool.
45
+ */
46
+ export async function createExecutionToolRuntimeWithDelegationFromEnv(env = process.env) {
47
+ const config = loadExecutionServiceConfig(env);
48
+ const delegation = await createDelegationServicesFromEnv(env);
49
+ if (!config) {
50
+ if (delegation) {
51
+ throw new Error('delegated send is configured but OTTO_EXECUTION_MCP_ENABLED is not true; enable the execution tools or unset the delegation variables');
52
+ }
53
+ return undefined;
54
+ }
55
+ return createExecutionToolRuntime(config, delegation ? { delegation } : {});
56
+ }
57
+ function normalizedAddress(value) {
58
+ return getAddress(value.toLowerCase());
59
+ }
60
+ function normalizedIntent(input, profile) {
61
+ // One implementation, shared with consumers through `otto-intel-mcp/execution`.
62
+ return swapIntentDigestInput({
63
+ ...input,
64
+ accountProfile: { ...input.accountProfile, address: profile.address },
65
+ });
66
+ }
67
+ function normalizedBridgeIntent(input, profile, destinationRecipient) {
68
+ return {
69
+ accountProfile: {
70
+ kind: profile.kind,
71
+ address: profile.address,
72
+ chainId: profile.chainId,
73
+ },
74
+ fromChainId: input.fromChainId,
75
+ toChainId: input.toChainId,
76
+ fromToken: normalizedAddress(input.fromToken),
77
+ toToken: normalizedAddress(input.toToken),
78
+ fromAmount: input.fromAmount,
79
+ destinationRecipient,
80
+ slippage: {
81
+ maxBps: input.slippage.maxBps,
82
+ ...(input.slippage.minAmountOut ? { minAmountOut: input.slippage.minAmountOut } : {}),
83
+ },
84
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
85
+ };
86
+ }
87
+ function normalizedPolymarketIntent(input, profile) {
88
+ return {
89
+ accountProfile: {
90
+ kind: profile.kind,
91
+ address: profile.address,
92
+ chainId: profile.chainId,
93
+ },
94
+ tokenId: input.tokenId,
95
+ side: input.side,
96
+ price: input.price,
97
+ size: input.size,
98
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
99
+ };
100
+ }
101
+ function normalizedPerpIntent(input, profile, tradingSigner, vaultAddress) {
102
+ return {
103
+ accountProfile: {
104
+ kind: profile.kind,
105
+ address: profile.address,
106
+ chainId: profile.chainId,
107
+ },
108
+ asset: input.asset,
109
+ side: input.side,
110
+ size: input.size,
111
+ limitPrice: input.limitPrice,
112
+ timeInForce: input.timeInForce,
113
+ reduceOnly: input.reduceOnly,
114
+ tradingSigner,
115
+ vaultAddress: vaultAddress ?? null,
116
+ ...(input.takeProfitPrice ? { takeProfitPrice: input.takeProfitPrice } : {}),
117
+ ...(input.stopLossPrice ? { stopLossPrice: input.stopLossPrice } : {}),
118
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
119
+ };
120
+ }
121
+ /** 0x + 32 lowercase hex, the venue's client-order-id shape (`hl-write-request-schema.ts:549`). */
122
+ function mintClientOrderId(entropy) {
123
+ const bytes = entropy();
124
+ if (bytes.length !== 16) {
125
+ return refuse('HYPERLIQUID_ORDER_INVALID', 'client order id entropy source did not produce 16 bytes');
126
+ }
127
+ return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
128
+ }
129
+ function capabilityFor(profile) {
130
+ if (profile.kind === 'eoa')
131
+ return 'send_evm_transaction';
132
+ if (profile.kind === 'safe')
133
+ return 'submit_evm_batch';
134
+ return refuse('UNSUPPORTED_ACCOUNT_PROFILE', 'only EOA and Safe profiles are supported');
135
+ }
136
+ export async function prepareSwap(runtime, rawInput, signal) {
137
+ const input = PREPARE_SWAP_INPUT_SCHEMA.parse(rawInput);
138
+ const profile = {
139
+ kind: input.accountProfile.kind,
140
+ address: normalizedAddress(input.accountProfile.address),
141
+ chainId: input.accountProfile.chainId,
142
+ };
143
+ if (profile.chainId !== input.chainId) {
144
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'account profile chain does not match swap chain');
145
+ }
146
+ if (!reviewedLiFiFeeForwarders(input.chainId)) {
147
+ return refuse('UNSUPPORTED_LIFI_ROUTE', 'chain is outside the reviewed LI.FI v1 route set');
148
+ }
149
+ const fromToken = normalizedAddress(input.fromToken);
150
+ const toToken = normalizedAddress(input.toToken);
151
+ const fromAmount = BigInt(input.fromAmount);
152
+ if (attributedFeeAmount(fromAmount, runtime.config.lifiFee) <= 0n) {
153
+ return refuse('UNSUPPORTED_LIFI_ROUTE', 'input amount is too small for the configured Otto fee');
154
+ }
155
+ const quote = await runtime.quoteClient.getSameChainSwapQuote({
156
+ chainId: input.chainId,
157
+ fromToken,
158
+ toToken,
159
+ fromAmount: input.fromAmount,
160
+ sender: profile.address,
161
+ receiver: profile.address,
162
+ maxSlippageBps: input.slippage.maxBps,
163
+ }, signal);
164
+ const asserted = decodeAndAssertLiFiSwap({
165
+ quote,
166
+ profile,
167
+ chainId: input.chainId,
168
+ fromToken,
169
+ toToken,
170
+ fromAmount,
171
+ maxSlippageBps: input.slippage.maxBps,
172
+ minAmountOut: input.slippage.minAmountOut ? BigInt(input.slippage.minAmountOut) : undefined,
173
+ config: runtime.config,
174
+ });
175
+ const capability = capabilityFor(profile);
176
+ const approvalCalldata = (amount) => encodeFunctionData({
177
+ abi: ERC20_APPROVE_ABI,
178
+ functionName: 'approve',
179
+ args: [asserted.approvalSpender, amount],
180
+ });
181
+ const payload = Object.freeze({
182
+ kind: 'evm_execution_plan',
183
+ chain_id: input.chainId,
184
+ steps: Object.freeze([
185
+ // Approval-race-guarded ERC20s (USDT and friends) revert on a non-zero to non-zero
186
+ // approve, so a plan that only raises the allowance is not executable from every
187
+ // reachable prior allowance state. Resetting to zero first is executable from all of
188
+ // them, needs no allowance read, and leaves the swap step unchanged.
189
+ Object.freeze({
190
+ to: fromToken,
191
+ value: '0',
192
+ data: approvalCalldata(0n),
193
+ kind: 'approve_reset',
194
+ }),
195
+ Object.freeze({
196
+ to: fromToken,
197
+ value: '0',
198
+ data: approvalCalldata(fromAmount),
199
+ kind: 'approve',
200
+ }),
201
+ Object.freeze({
202
+ to: asserted.transaction.to,
203
+ value: asserted.transaction.value,
204
+ data: asserted.transaction.data,
205
+ kind: 'call',
206
+ }),
207
+ ]),
208
+ });
209
+ const nowMs = runtime.nowMs();
210
+ const artifactId = createArtifactId(nowMs, runtime.entropy());
211
+ const payloadDigest = executionDigest(payload);
212
+ const uncommitted = {
213
+ schema_version: '1',
214
+ artifact_id: artifactId,
215
+ rail: 'lifi',
216
+ operation: 'swap',
217
+ intent_digest: executionDigest(normalizedIntent(input, profile)),
218
+ account_binding: accountBinding(profile),
219
+ required_capabilities: [capability],
220
+ payload,
221
+ fee_attribution: {
222
+ mechanism: 'lifi_integrator_fee',
223
+ config_version: runtime.config.lifiFeeConfigVersion,
224
+ },
225
+ replay_disclosure: {
226
+ artifact_role: 'construction_aid',
227
+ freshness_boundary: 'valid_until',
228
+ onchain_replay_boundary: 'signer_account_nonce',
229
+ tool_guarantees_single_execution: false,
230
+ },
231
+ assertions: [
232
+ ...asserted.assertions,
233
+ { id: 'payload_digest_committed', result: 'passed' },
234
+ { id: 'erc20_allowance_reset_ordered', result: 'passed' },
235
+ { id: 'artifact_expiry_committed', result: 'passed' },
236
+ ],
237
+ valid_until: new Date(nowMs + runtime.config.artifactTtlMs).toISOString(),
238
+ submission: {
239
+ method: capability,
240
+ chain_id: input.chainId,
241
+ artifact_id: artifactId,
242
+ payload_digest: payloadDigest,
243
+ adapter_owned_fields: ['nonce', 'gas', 'serialization'],
244
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
245
+ },
246
+ };
247
+ const envelope = {
248
+ ...uncommitted,
249
+ submission: {
250
+ ...uncommitted.submission,
251
+ artifact_digest: artifactCommitmentDigest(uncommitted),
252
+ },
253
+ };
254
+ const parsed = EXECUTION_ENVELOPE_SCHEMA.parse(envelope);
255
+ // Bind the artifact — id AND whole-envelope commitment — to THIS process for the delegated tool: it submits
256
+ // only what this process prepared. At capacity the prepare is refused; nothing issued is ever dropped.
257
+ if (runtime.delegation) {
258
+ recordPreparedArtifact(runtime.delegation, artifactId, parsed.submission.artifact_digest, nowMs + runtime.config.artifactTtlMs, nowMs);
259
+ }
260
+ return parsed;
261
+ }
262
+ /**
263
+ * Build the ordered source-chain plan for a cross-chain native-USDC bridge over the enumerated v1
264
+ * Circle-CCTP route set. Prepare-only: the plan is handed to the caller's own custody to sign.
265
+ */
266
+ export async function prepareBridge(runtime, rawInput, signal) {
267
+ const input = PREPARE_BRIDGE_INPUT_SCHEMA.parse(rawInput);
268
+ const profile = {
269
+ kind: input.accountProfile.kind,
270
+ address: normalizedAddress(input.accountProfile.address),
271
+ chainId: input.accountProfile.chainId,
272
+ };
273
+ if (profile.chainId !== input.fromChainId) {
274
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'account profile chain does not match the bridge source chain');
275
+ }
276
+ // An EOA is the same account on every EVM chain, so its own address is a safe default
277
+ // destination. A Safe is not, which is why the schema requires the caller to declare one.
278
+ const destinationRecipient = input.destinationRecipient
279
+ ? normalizedAddress(input.destinationRecipient)
280
+ : profile.address;
281
+ const fromToken = normalizedAddress(input.fromToken);
282
+ const toToken = normalizedAddress(input.toToken);
283
+ const fromAmount = BigInt(input.fromAmount);
284
+ if (!isEnumeratedCctpBridgeRoute(input.fromChainId, input.toChainId, fromToken, toToken)) {
285
+ return refuse('UNSUPPORTED_LIFI_ROUTE', `route ${input.fromChainId}:${fromToken} -> ${input.toChainId}:${toToken} is outside the enumerated v1 PolymerCCTP USDC route set`);
286
+ }
287
+ if (attributedFeeAmount(fromAmount, runtime.config.lifiFee) <= 0n) {
288
+ return refuse('UNSUPPORTED_LIFI_ROUTE', 'input amount is too small for the configured Otto fee');
289
+ }
290
+ const quote = await runtime.quoteClient.getCrossChainBridgeQuote({
291
+ fromChainId: input.fromChainId,
292
+ toChainId: input.toChainId,
293
+ fromToken,
294
+ toToken,
295
+ fromAmount: input.fromAmount,
296
+ sender: profile.address,
297
+ receiver: destinationRecipient,
298
+ maxSlippageBps: input.slippage.maxBps,
299
+ }, signal);
300
+ const asserted = decodeAndAssertLiFiBridge({
301
+ quote,
302
+ profile,
303
+ fromChainId: input.fromChainId,
304
+ toChainId: input.toChainId,
305
+ fromToken,
306
+ toToken,
307
+ fromAmount,
308
+ destinationRecipient,
309
+ maxSlippageBps: input.slippage.maxBps,
310
+ minAmountOut: input.slippage.minAmountOut ? BigInt(input.slippage.minAmountOut) : undefined,
311
+ config: runtime.config,
312
+ });
313
+ const capability = capabilityFor(profile);
314
+ const approvalCalldata = (amount) => encodeFunctionData({
315
+ abi: ERC20_APPROVE_ABI,
316
+ functionName: 'approve',
317
+ args: [asserted.approvalSpender, amount],
318
+ });
319
+ const payload = Object.freeze({
320
+ kind: 'evm_execution_plan',
321
+ chain_id: input.fromChainId,
322
+ steps: Object.freeze([
323
+ // Same allowance-reset ordering as the swap plan: it is what keeps the plan executable from
324
+ // every reachable prior allowance state, including approval-race-guarded ERC20s.
325
+ Object.freeze({
326
+ to: fromToken,
327
+ value: '0',
328
+ data: approvalCalldata(0n),
329
+ kind: 'approve_reset',
330
+ }),
331
+ Object.freeze({
332
+ to: fromToken,
333
+ value: '0',
334
+ data: approvalCalldata(fromAmount),
335
+ kind: 'approve',
336
+ }),
337
+ Object.freeze({
338
+ to: asserted.transaction.to,
339
+ value: asserted.transaction.value,
340
+ data: asserted.transaction.data,
341
+ kind: 'call',
342
+ }),
343
+ ]),
344
+ });
345
+ const nowMs = runtime.nowMs();
346
+ const artifactId = createArtifactId(nowMs, runtime.entropy());
347
+ const payloadDigest = executionDigest(payload);
348
+ const uncommitted = {
349
+ schema_version: '1',
350
+ artifact_id: artifactId,
351
+ rail: 'lifi',
352
+ operation: 'bridge',
353
+ intent_digest: executionDigest(normalizedBridgeIntent(input, profile, destinationRecipient)),
354
+ account_binding: bridgeAccountBinding(profile, {
355
+ destinationRecipient,
356
+ destinationChainId: input.toChainId,
357
+ sendingAsset: fromToken,
358
+ receivingAsset: toToken,
359
+ }),
360
+ required_capabilities: [capability],
361
+ payload,
362
+ fee_attribution: {
363
+ mechanism: 'lifi_integrator_fee',
364
+ config_version: runtime.config.lifiFeeConfigVersion,
365
+ },
366
+ replay_disclosure: {
367
+ artifact_role: 'construction_aid',
368
+ freshness_boundary: 'valid_until',
369
+ onchain_replay_boundary: 'signer_account_nonce',
370
+ tool_guarantees_single_execution: false,
371
+ },
372
+ assertions: [
373
+ ...asserted.assertions,
374
+ { id: 'payload_digest_committed', result: 'passed' },
375
+ { id: 'erc20_allowance_reset_ordered', result: 'passed' },
376
+ { id: 'artifact_expiry_committed', result: 'passed' },
377
+ ],
378
+ valid_until: new Date(nowMs + runtime.config.artifactTtlMs).toISOString(),
379
+ submission: {
380
+ // The source chain: the only chain any step in this plan is submitted to.
381
+ method: capability,
382
+ chain_id: input.fromChainId,
383
+ artifact_id: artifactId,
384
+ payload_digest: payloadDigest,
385
+ adapter_owned_fields: ['nonce', 'gas', 'serialization'],
386
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
387
+ },
388
+ };
389
+ const envelope = {
390
+ ...uncommitted,
391
+ submission: {
392
+ ...uncommitted.submission,
393
+ artifact_digest: artifactCommitmentDigest(uncommitted),
394
+ },
395
+ };
396
+ return BRIDGE_EXECUTION_ENVELOPE_SCHEMA.parse(envelope);
397
+ }
398
+ /**
399
+ * Build an unsigned Polymarket CTF Exchange V2 limit order for the caller's own Polygon EOA, with
400
+ * Otto's builder code inside the signed struct (spec §7.3). Prepare-only: the artifact carries order
401
+ * args and the EIP-712 payload; the caller's own signer signs it and the caller's own credentials
402
+ * post it. Nothing here holds a key, a CLOB credential, or the caller's collateral.
403
+ */
404
+ export async function preparePolymarketOrder(runtime, rawInput, signal) {
405
+ const input = PREPARE_POLYMARKET_ORDER_INPUT_SCHEMA.parse(rawInput);
406
+ const profile = {
407
+ kind: input.accountProfile.kind,
408
+ address: normalizedAddress(input.accountProfile.address),
409
+ chainId: input.accountProfile.chainId,
410
+ };
411
+ if (profile.kind !== 'eoa') {
412
+ // A Safe maker needs a POLY_GNOSIS_SAFE-vs-POLY_1271 signature-type ruling that spec §13 still
413
+ // lists as open. Refused by name rather than guessed: the signature type is inside the signed
414
+ // order, so the wrong guess produces orders the exchange rejects or validates against the wrong
415
+ // owner.
416
+ return refuse('UNSUPPORTED_ACCOUNT_PROFILE', 'v1 supports the EOA profile only; the Safe signature type for Polymarket is an open ruling');
417
+ }
418
+ if (profile.chainId !== POLYMARKET_CHAIN_ID) {
419
+ return refuse('ACCOUNT_BINDING_MISMATCH', `Polymarket orders are Polygon-only; the account profile declares chain ${profile.chainId}`);
420
+ }
421
+ const metadata = await runtime.clobClient.getMarketMetadata(input.tokenId, signal);
422
+ if (!metadata.acceptingOrders ||
423
+ !metadata.active ||
424
+ metadata.closed ||
425
+ !metadata.orderBookEnabled) {
426
+ return refuse('UNSUPPORTED_POLYMARKET_MARKET', `market ${metadata.conditionId} is not accepting orders (accepting_orders=${metadata.acceptingOrders}, active=${metadata.active}, closed=${metadata.closed}, order_book=${metadata.orderBookEnabled})`);
427
+ }
428
+ const { priceUnits } = assertPolymarketPriceOnTick(input.price, metadata.tickSize);
429
+ const sizeAtomic = polymarketAtomicUnits(input.size);
430
+ const minimumOrderSizeAtomic = polymarketAtomicUnits(String(metadata.minimumOrderSize));
431
+ const builderRates = await runtime.clobClient.getBuilderFeeRates(runtime.config.polymarketBuilderCode, signal);
432
+ const attributionAssertions = assertPolymarketBuilderAttributionRuled(builderRates, runtime.config);
433
+ const nowMs = runtime.nowMs();
434
+ const built = await runtime.polymarketOrderBuilder({
435
+ maker: profile.address,
436
+ tokenId: input.tokenId,
437
+ side: input.side,
438
+ price: Number(input.price),
439
+ size: Number(input.size),
440
+ tickSize: metadata.tickSize,
441
+ negRisk: metadata.negRisk,
442
+ builderCode: runtime.config.polymarketBuilderCode,
443
+ });
444
+ const asserted = decodeAndAssertPolymarketOrder({
445
+ built,
446
+ maker: profile.address,
447
+ tokenId: input.tokenId,
448
+ side: input.side,
449
+ priceUnits,
450
+ sizeAtomic,
451
+ tickSize: metadata.tickSize,
452
+ negRisk: metadata.negRisk,
453
+ minimumOrderSizeAtomic,
454
+ config: runtime.config,
455
+ nowMs,
456
+ });
457
+ const payload = Object.freeze({
458
+ kind: 'polymarket_order_v2',
459
+ chain_id: POLYMARKET_CHAIN_ID,
460
+ exchange: asserted.exchange,
461
+ neg_risk: metadata.negRisk,
462
+ tick_size: metadata.tickSize,
463
+ order_type: 'GTC',
464
+ order: Object.freeze(asserted.order),
465
+ eip712: asserted.typedData,
466
+ order_hash: asserted.orderHash,
467
+ });
468
+ const artifactId = createArtifactId(nowMs, runtime.entropy());
469
+ const payloadDigest = executionDigest(payload);
470
+ const assertions = [
471
+ ...attributionAssertions,
472
+ ...asserted.assertions,
473
+ { id: 'payload_digest_committed', result: 'passed' },
474
+ { id: 'artifact_expiry_committed', result: 'passed' },
475
+ ];
476
+ const uncommitted = {
477
+ schema_version: '1',
478
+ artifact_id: artifactId,
479
+ rail: 'polymarket',
480
+ operation: 'pm_order',
481
+ intent_digest: executionDigest(normalizedPolymarketIntent(input, profile)),
482
+ account_binding: polymarketAccountBinding(profile, {
483
+ exchange: asserted.exchange,
484
+ tokenId: input.tokenId,
485
+ }),
486
+ required_capabilities: ['sign_eip712'],
487
+ payload,
488
+ fee_attribution: {
489
+ mechanism: 'polymarket_builder_code',
490
+ config_version: runtime.config.polymarketBuilderFeeConfigVersion,
491
+ },
492
+ replay_disclosure: {
493
+ artifact_role: 'construction_aid',
494
+ freshness_boundary: 'valid_until',
495
+ onchain_replay_boundary: 'exchange_order_hash',
496
+ tool_guarantees_single_execution: false,
497
+ },
498
+ assertions,
499
+ valid_until: new Date(nowMs + runtime.config.artifactTtlMs).toISOString(),
500
+ submission: {
501
+ method: 'sign_eip712',
502
+ chain_id: POLYMARKET_CHAIN_ID,
503
+ artifact_id: artifactId,
504
+ payload_digest: payloadDigest,
505
+ adapter_owned_fields: ['signature', 'clob_post'],
506
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
507
+ },
508
+ };
509
+ const envelope = {
510
+ ...uncommitted,
511
+ submission: {
512
+ ...uncommitted.submission,
513
+ artifact_digest: artifactCommitmentDigest(uncommitted),
514
+ },
515
+ };
516
+ return POLYMARKET_EXECUTION_ENVELOPE_SCHEMA.parse(envelope);
517
+ }
518
+ /**
519
+ * Build an unsigned Hyperliquid perp order L1 action for the caller's own Hyperliquid account, with
520
+ * optional reduce-only take-profit and stop-loss triggers bundled under `normalTpsl` (spec §7.2).
521
+ *
522
+ * Prepare-only, and structurally so: nothing on this path can sign or submit. The artifact carries the
523
+ * canonical action plus the declared EIP-712 Agent domain, struct, and `connectionId` recipe; the
524
+ * caller's own signer picks the nonce, computes the digest, and signs. The root-secret-derived agent
525
+ * wallet the trade-execution agent uses (`hyperliquid-client.ts:558`) is fenced out entirely (§11), and
526
+ * the builder attribution field is spec'd and VALUELESS until the Step-0 `approveBuilderFee` reversal
527
+ * lands (§7.2), so the action carries no builder key and the envelope declares that pending state.
528
+ */
529
+ export async function preparePerpOrder(runtime, rawInput, signal) {
530
+ const input = PREPARE_PERP_ORDER_INPUT_SCHEMA.parse(rawInput);
531
+ const profile = {
532
+ kind: input.accountProfile.kind,
533
+ address: normalizedAddress(input.accountProfile.address),
534
+ chainId: input.accountProfile.chainId,
535
+ };
536
+ if (profile.kind !== 'eoa') {
537
+ // Hyperliquid recovers an L1 action's signer with ecrecover and does not accept ERC-1271, so a
538
+ // Safe cannot sign one at all — and a Safe master cannot even approve an API wallet to sign for
539
+ // it. Refused by name rather than constructed into an artifact no signer could ever satisfy.
540
+ return refuse('UNSUPPORTED_ACCOUNT_PROFILE', 'Hyperliquid recovers the L1 action signer with ecrecover; a Safe cannot produce an accepted signature, so v1 supports the EOA profile only');
541
+ }
542
+ const tradingSigner = input.tradingSigner
543
+ ? normalizedAddress(input.tradingSigner)
544
+ : profile.address;
545
+ const vaultAddress = input.vaultAddress ? normalizedAddress(input.vaultAddress) : undefined;
546
+ const asset = await runtime.hyperliquidInfoClient.getPerpAsset(input.asset, signal);
547
+ if (asset.isDelisted) {
548
+ return refuse('UNSUPPORTED_HYPERLIQUID_MARKET', `perp ${asset.name} is delisted on Hyperliquid; it still occupies its universe index but no longer trades`);
549
+ }
550
+ // Grid checks, not rounding: an off-grid price or size is refused by name so the caller never signs
551
+ // an order at a price they did not ask for.
552
+ assertHyperliquidPriceOnGrid(input.limitPrice, asset.szDecimals);
553
+ assertHyperliquidSizeOnGrid(input.size, asset.szDecimals);
554
+ if (input.takeProfitPrice)
555
+ assertHyperliquidPriceOnGrid(input.takeProfitPrice, asset.szDecimals);
556
+ if (input.stopLossPrice)
557
+ assertHyperliquidPriceOnGrid(input.stopLossPrice, asset.szDecimals);
558
+ assertHyperliquidBracketCoherent({
559
+ isBuy: input.side === 'BUY',
560
+ price: input.limitPrice,
561
+ takeProfitPrice: input.takeProfitPrice,
562
+ stopLossPrice: input.stopLossPrice,
563
+ szDecimals: asset.szDecimals,
564
+ });
565
+ const isBuy = input.side === 'BUY';
566
+ // ONE DISTINCT CLIENT ID PER LEG, minted before construction. This is what lets the caller read the
567
+ // venue's per-leg verdict by identity instead of by position in the response array — the correlation
568
+ // failure `hyperliquid-client.ts:1801` documents.
569
+ const legs = [
570
+ {
571
+ role: 'entry',
572
+ isBuy,
573
+ price: input.limitPrice,
574
+ size: input.size,
575
+ reduceOnly: input.reduceOnly,
576
+ tif: input.timeInForce,
577
+ clientOrderId: mintClientOrderId(runtime.clientOrderIdEntropy),
578
+ },
579
+ ];
580
+ // Triggers close the entry, so they sit on the opposite side, reduce-only, at their own trigger
581
+ // price, sized to the entry (`hyperliquid-client.ts:1825-1870`).
582
+ if (input.takeProfitPrice) {
583
+ legs.push({
584
+ role: 'take_profit',
585
+ isBuy: !isBuy,
586
+ price: input.takeProfitPrice,
587
+ size: input.size,
588
+ reduceOnly: true,
589
+ trigger: { triggerPx: input.takeProfitPrice, tpsl: 'tp' },
590
+ clientOrderId: mintClientOrderId(runtime.clientOrderIdEntropy),
591
+ });
592
+ }
593
+ if (input.stopLossPrice) {
594
+ legs.push({
595
+ role: 'stop_loss',
596
+ isBuy: !isBuy,
597
+ price: input.stopLossPrice,
598
+ size: input.size,
599
+ reduceOnly: true,
600
+ trigger: { triggerPx: input.stopLossPrice, tpsl: 'sl' },
601
+ clientOrderId: mintClientOrderId(runtime.clientOrderIdEntropy),
602
+ });
603
+ }
604
+ const grouping = legs.length > 1 ? 'normalTpsl' : 'na';
605
+ const built = runtime.hyperliquidActionBuilder({
606
+ grouping,
607
+ legs: legs.map((leg) => ({
608
+ assetId: asset.assetId,
609
+ isBuy: leg.isBuy,
610
+ price: leg.price,
611
+ size: leg.size,
612
+ reduceOnly: leg.reduceOnly,
613
+ orderType: leg.trigger
614
+ ? { trigger: { isMarket: true, triggerPx: leg.trigger.triggerPx, tpsl: leg.trigger.tpsl } }
615
+ : { limit: { tif: leg.tif ?? input.timeInForce } },
616
+ clientOrderId: leg.clientOrderId,
617
+ })),
618
+ });
619
+ const asserted = decodeAndAssertHyperliquidAction({ built, asset, grouping, legs });
620
+ const nowMs = runtime.nowMs();
621
+ const validUntilMs = nowMs + runtime.config.artifactTtlMs;
622
+ const payload = Object.freeze({
623
+ kind: 'hyperliquid_l1_action',
624
+ chain_id: HYPERLIQUID_L1_SIGNATURE_CHAIN_ID,
625
+ action: asserted.action,
626
+ signing: hyperliquidSigningDeclaration(),
627
+ vault_address: vaultAddress ?? null,
628
+ // The one freshness control the constructor owns (§10), placed where the venue enforces it: inside
629
+ // the signed digest. The venue rejects the action after this instant, which makes `valid_until` a
630
+ // venue-side bound on this rail rather than an advisory one.
631
+ expires_after: validUntilMs,
632
+ });
633
+ const artifactId = createArtifactId(nowMs, runtime.entropy());
634
+ const payloadDigest = executionDigest(payload);
635
+ const assertions = [
636
+ ...asserted.assertions,
637
+ { id: 'hyperliquid_bracket_directionally_coherent', result: 'passed' },
638
+ { id: 'hyperliquid_signing_domain_pinned', result: 'passed' },
639
+ { id: 'hyperliquid_agent_struct_pinned', result: 'passed' },
640
+ { id: 'hyperliquid_expires_after_matches_valid_until', result: 'passed' },
641
+ { id: 'payload_digest_committed', result: 'passed' },
642
+ { id: 'artifact_expiry_committed', result: 'passed' },
643
+ ];
644
+ const uncommitted = {
645
+ schema_version: '1',
646
+ artifact_id: artifactId,
647
+ rail: 'hyperliquid',
648
+ operation: 'perp_order',
649
+ intent_digest: executionDigest(normalizedPerpIntent(input, profile, tradingSigner, vaultAddress)),
650
+ account_binding: hyperliquidAccountBinding(profile, {
651
+ tradingSigner,
652
+ vaultAddress,
653
+ asset: asset.name,
654
+ assetId: asset.assetId,
655
+ szDecimals: asset.szDecimals,
656
+ }),
657
+ required_capabilities: ['sign_hyperliquid_l1'],
658
+ payload,
659
+ fee_attribution: {
660
+ mechanism: 'hyperliquid_builder_fee',
661
+ config_version: runtime.config.hyperliquidBuilderFeeConfigVersion,
662
+ status: HL_BUILDER_ATTRIBUTION_STATUS,
663
+ },
664
+ replay_disclosure: {
665
+ artifact_role: 'construction_aid',
666
+ freshness_boundary: 'valid_until',
667
+ onchain_replay_boundary: 'signer_nonce_and_expires_after',
668
+ tool_guarantees_single_execution: false,
669
+ },
670
+ assertions,
671
+ valid_until: new Date(validUntilMs).toISOString(),
672
+ submission: {
673
+ method: 'sign_hyperliquid_l1',
674
+ chain_id: HYPERLIQUID_L1_SIGNATURE_CHAIN_ID,
675
+ artifact_id: artifactId,
676
+ payload_digest: payloadDigest,
677
+ adapter_owned_fields: ['nonce', 'signature', 'exchange_post'],
678
+ ...(input.client_ref ? { client_ref: input.client_ref } : {}),
679
+ },
680
+ };
681
+ const envelope = {
682
+ ...uncommitted,
683
+ submission: {
684
+ ...uncommitted.submission,
685
+ artifact_digest: artifactCommitmentDigest(uncommitted),
686
+ },
687
+ };
688
+ return HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA.parse(envelope);
689
+ }
690
+ //# sourceMappingURL=execution-tools.js.map