otto-execute 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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/cdp-signer.d.ts +2 -0
  4. package/dist/cdp-signer.js +3 -0
  5. package/dist/cdp.d.ts +7 -0
  6. package/dist/cdp.js +8 -0
  7. package/dist/chain.d.ts +2 -0
  8. package/dist/chain.js +3 -0
  9. package/dist/cli.d.ts +17 -0
  10. package/dist/cli.js +400 -0
  11. package/dist/delegate.d.ts +117 -0
  12. package/dist/delegate.js +394 -0
  13. package/dist/eoa-signer.d.ts +94 -0
  14. package/dist/eoa-signer.js +289 -0
  15. package/dist/erc20.d.ts +2 -0
  16. package/dist/erc20.js +3 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +19 -0
  19. package/dist/lifi-decode.d.ts +2 -0
  20. package/dist/lifi-decode.js +3 -0
  21. package/dist/mutations.d.ts +55 -0
  22. package/dist/mutations.js +363 -0
  23. package/dist/refusal.d.ts +2 -0
  24. package/dist/refusal.js +3 -0
  25. package/dist/vendor/otto-intel-mcp/VENDORED.json +52 -0
  26. package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.d.ts +133 -0
  27. package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.js +356 -0
  28. package/dist/vendor/otto-intel-mcp/adapter/chain.d.ts +36 -0
  29. package/dist/vendor/otto-intel-mcp/adapter/chain.js +65 -0
  30. package/dist/vendor/otto-intel-mcp/adapter/erc20.d.ts +39 -0
  31. package/dist/vendor/otto-intel-mcp/adapter/erc20.js +17 -0
  32. package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.d.ts +52 -0
  33. package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.js +149 -0
  34. package/dist/vendor/otto-intel-mcp/adapter/refusal.d.ts +21 -0
  35. package/dist/vendor/otto-intel-mcp/adapter/refusal.js +55 -0
  36. package/dist/vendor/otto-intel-mcp/adapter/sent-step.d.ts +15 -0
  37. package/dist/vendor/otto-intel-mcp/adapter/sent-step.js +6 -0
  38. package/dist/vendor/otto-intel-mcp/adapter/verify.d.ts +149 -0
  39. package/dist/vendor/otto-intel-mcp/adapter/verify.js +432 -0
  40. package/dist/vendor/otto-intel-mcp/adapter-cdp-index.d.ts +6 -0
  41. package/dist/vendor/otto-intel-mcp/adapter-cdp-index.js +7 -0
  42. package/dist/vendor/otto-intel-mcp/adapter-index.d.ts +15 -0
  43. package/dist/vendor/otto-intel-mcp/adapter-index.js +15 -0
  44. package/dist/vendor/otto-intel-mcp/artifact-id.d.ts +16 -0
  45. package/dist/vendor/otto-intel-mcp/artifact-id.js +60 -0
  46. package/dist/vendor/otto-intel-mcp/execution-config.d.ts +232 -0
  47. package/dist/vendor/otto-intel-mcp/execution-config.js +443 -0
  48. package/dist/vendor/otto-intel-mcp/execution-delegated-definition.d.ts +165 -0
  49. package/dist/vendor/otto-intel-mcp/execution-delegated-definition.js +116 -0
  50. package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.d.ts +208 -0
  51. package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.js +170 -0
  52. package/dist/vendor/otto-intel-mcp/execution-delegation-policy.d.ts +257 -0
  53. package/dist/vendor/otto-intel-mcp/execution-delegation-policy.js +279 -0
  54. package/dist/vendor/otto-intel-mcp/execution-errors.d.ts +9 -0
  55. package/dist/vendor/otto-intel-mcp/execution-errors.js +134 -0
  56. package/dist/vendor/otto-intel-mcp/execution-index.d.ts +17 -0
  57. package/dist/vendor/otto-intel-mcp/execution-index.js +16 -0
  58. package/dist/vendor/otto-intel-mcp/execution-intent.d.ts +14 -0
  59. package/dist/vendor/otto-intel-mcp/execution-intent.js +36 -0
  60. package/dist/vendor/otto-intel-mcp/execution-tool-definitions.d.ts +1103 -0
  61. package/dist/vendor/otto-intel-mcp/execution-tool-definitions.js +1051 -0
  62. package/dist/vendor/otto-intel-mcp/execution-types.d.ts +274 -0
  63. package/dist/vendor/otto-intel-mcp/execution-types.js +157 -0
  64. package/dist/verify.d.ts +2 -0
  65. package/dist/verify.js +3 -0
  66. package/dist/x402-table.d.ts +99 -0
  67. package/dist/x402-table.js +221 -0
  68. package/package.json +97 -0
@@ -0,0 +1,1051 @@
1
+ import { hashTypedData } from 'viem';
2
+ import { z } from 'zod';
3
+ import { isArtifactFreshnessWellFormed } from './artifact-id.js';
4
+ import { HL_BUILDER_ATTRIBUTION_STATUS, 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_GROUPINGS, HYPERLIQUID_ORDER_KEY_ORDER, HYPERLIQUID_TIME_IN_FORCE, HYPERLIQUID_TRIGGER_KEY_ORDER, isEnumeratedCctpBridgeRoute, LIFI_CCTP_USDC_BY_CHAIN, POLYMARKET_CHAIN_ID, POLYMARKET_EIP712_DOMAIN_NAME, POLYMARKET_EIP712_DOMAIN_VERSION, POLYMARKET_ORDER_PRIMARY_TYPE, POLYMARKET_ORDER_V2_EIP712_FIELDS, POLYMARKET_TICK_SIZES, } from './execution-config.js';
5
+ const EVM_ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
6
+ const ERC20_APPROVE_SELECTOR = '0x095ea7b3';
7
+ const HEX_DATA_PATTERN = /^0x(?:[0-9a-fA-F]{2})*$/;
8
+ const DIGEST_PATTERN = /^0x[0-9a-fA-F]{64}$/;
9
+ const UINT256_MAX = (1n << 256n) - 1n;
10
+ const addressSchema = z
11
+ .string()
12
+ .regex(EVM_ADDRESS_PATTERN, 'must be a 20-byte EVM address')
13
+ .refine((value) => !/^0x0{40}$/i.test(value), 'must not be the zero address');
14
+ const positiveUintString = z
15
+ .string()
16
+ .regex(/^[1-9]\d*$/, 'must be a positive decimal integer')
17
+ .refine((value) => BigInt(value) <= UINT256_MAX, 'must fit uint256');
18
+ export const ACCOUNT_PROFILE_SCHEMA = z
19
+ .object({
20
+ kind: z.enum(['eoa', 'safe']),
21
+ address: addressSchema,
22
+ chainId: z.number().int().positive().safe(),
23
+ })
24
+ .strict();
25
+ export const PREPARE_SWAP_INPUT_SCHEMA = z
26
+ .object({
27
+ accountProfile: ACCOUNT_PROFILE_SCHEMA,
28
+ chainId: z.number().int().positive().safe(),
29
+ fromToken: addressSchema.describe('ERC20 input token address; native-token routes are out of v1.'),
30
+ toToken: addressSchema.describe('ERC20 output token address; native-token routes are out of v1.'),
31
+ fromAmount: positiveUintString.describe('Exact input amount in atomic token units.'),
32
+ slippage: z
33
+ .object({
34
+ maxBps: z.number().int().min(1).max(1_000),
35
+ minAmountOut: positiveUintString.optional(),
36
+ })
37
+ .strict(),
38
+ client_ref: z.string().trim().min(1).max(128).optional(),
39
+ })
40
+ .strict()
41
+ .superRefine((value, context) => {
42
+ if (value.fromToken.toLowerCase() === value.toToken.toLowerCase()) {
43
+ context.addIssue({
44
+ code: 'custom',
45
+ path: ['toToken'],
46
+ message: 'must differ from fromToken',
47
+ });
48
+ }
49
+ });
50
+ export const PREPARE_BRIDGE_INPUT_SCHEMA = z
51
+ .object({
52
+ accountProfile: ACCOUNT_PROFILE_SCHEMA,
53
+ fromChainId: z.number().int().positive().safe(),
54
+ toChainId: z.number().int().positive().safe(),
55
+ fromToken: addressSchema.describe('Native USDC on the source chain; the v1 bridge route set is USDC-only.'),
56
+ toToken: addressSchema.describe('Native USDC on the destination chain; the v1 bridge route set is USDC-only.'),
57
+ fromAmount: positiveUintString.describe('Exact gross input amount in atomic token units.'),
58
+ destinationRecipient: addressSchema
59
+ .optional()
60
+ .describe('Where the destination mint lands. Optional for an EOA, whose address is identical on every EVM chain; REQUIRED for a Safe, whose address is not guaranteed to be the same contract on the destination chain.'),
61
+ slippage: z
62
+ .object({
63
+ maxBps: z.number().int().min(1).max(1_000),
64
+ minAmountOut: positiveUintString.optional(),
65
+ })
66
+ .strict(),
67
+ client_ref: z.string().trim().min(1).max(128).optional(),
68
+ })
69
+ .strict()
70
+ .superRefine((value, context) => {
71
+ if (value.fromChainId === value.toChainId) {
72
+ context.addIssue({
73
+ code: 'custom',
74
+ path: ['toChainId'],
75
+ message: 'must differ from fromChainId; same-chain routes belong to otto_prepare_swap',
76
+ });
77
+ }
78
+ if (value.accountProfile.kind === 'safe' && value.destinationRecipient === undefined) {
79
+ context.addIssue({
80
+ code: 'custom',
81
+ path: ['destinationRecipient'],
82
+ message: 'required for a Safe profile: a Safe address is not guaranteed to be the same account on the destination chain',
83
+ });
84
+ }
85
+ });
86
+ const accountBindingSchema = ACCOUNT_PROFILE_SCHEMA.extend({
87
+ protocol_roles: z
88
+ .object({
89
+ lifi: z
90
+ .object({
91
+ sender: addressSchema,
92
+ receiver: addressSchema,
93
+ })
94
+ .strict(),
95
+ })
96
+ .strict(),
97
+ }).strict();
98
+ const evmStepSchema = z
99
+ .object({
100
+ to: addressSchema,
101
+ value: z.string().regex(/^\d+$/),
102
+ data: z.string().regex(HEX_DATA_PATTERN),
103
+ kind: z.enum(['approve_reset', 'approve', 'call']),
104
+ })
105
+ .strict();
106
+ /**
107
+ * Decode a canonical `approve(address,uint256)` call. Returns undefined for any other calldata,
108
+ * so the envelope schema can bind approval shape without trusting the step's declared kind.
109
+ */
110
+ export function decodeApprovalCalldata(data) {
111
+ if (data.length !== 138 || data.slice(0, 10).toLowerCase() !== ERC20_APPROVE_SELECTOR) {
112
+ return undefined;
113
+ }
114
+ const spenderWord = data.slice(10, 74);
115
+ if (!/^0{24}[0-9a-fA-F]{40}$/.test(spenderWord))
116
+ return undefined;
117
+ return {
118
+ spender: `0x${spenderWord.slice(24)}`.toLowerCase(),
119
+ amount: BigInt(`0x${data.slice(74)}`),
120
+ };
121
+ }
122
+ export const EXECUTION_ENVELOPE_SCHEMA = z
123
+ .object({
124
+ schema_version: z.literal('1'),
125
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
126
+ rail: z.literal('lifi'),
127
+ operation: z.literal('swap'),
128
+ intent_digest: z.string().regex(DIGEST_PATTERN),
129
+ account_binding: accountBindingSchema,
130
+ required_capabilities: z.array(z.enum(['send_evm_transaction', 'submit_evm_batch'])).length(1),
131
+ payload: z.discriminatedUnion('kind', [
132
+ z
133
+ .object({
134
+ kind: z.literal('evm_execution_plan'),
135
+ chain_id: z.number().int().positive().safe(),
136
+ steps: z.array(evmStepSchema).length(3),
137
+ })
138
+ .strict(),
139
+ ]),
140
+ fee_attribution: z
141
+ .object({
142
+ mechanism: z.literal('lifi_integrator_fee'),
143
+ config_version: z.string().min(1).max(64),
144
+ })
145
+ .strict(),
146
+ replay_disclosure: z
147
+ .object({
148
+ artifact_role: z.literal('construction_aid'),
149
+ freshness_boundary: z.literal('valid_until'),
150
+ onchain_replay_boundary: z.literal('signer_account_nonce'),
151
+ tool_guarantees_single_execution: z.literal(false),
152
+ })
153
+ .strict(),
154
+ assertions: z
155
+ .array(z
156
+ .object({
157
+ id: z.string().min(1).max(64),
158
+ result: z.literal('passed'),
159
+ })
160
+ .strict())
161
+ .min(1),
162
+ valid_until: z.string().datetime({ offset: true }),
163
+ submission: z
164
+ .object({
165
+ method: z.enum(['send_evm_transaction', 'submit_evm_batch']),
166
+ chain_id: z.number().int().positive().safe(),
167
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
168
+ payload_digest: z.string().regex(DIGEST_PATTERN),
169
+ artifact_digest: z.string().regex(DIGEST_PATTERN),
170
+ adapter_owned_fields: z.tuple([
171
+ z.literal('nonce'),
172
+ z.literal('gas'),
173
+ z.literal('serialization'),
174
+ ]),
175
+ client_ref: z.string().min(1).max(128).optional(),
176
+ })
177
+ .strict(),
178
+ })
179
+ .strict()
180
+ .superRefine((value, context) => {
181
+ const issue = (path, message) => {
182
+ context.addIssue({ code: 'custom', path, message });
183
+ };
184
+ const account = value.account_binding;
185
+ const accountAddress = account.address.toLowerCase();
186
+ if (account.protocol_roles.lifi.sender.toLowerCase() !== accountAddress ||
187
+ account.protocol_roles.lifi.receiver.toLowerCase() !== accountAddress) {
188
+ issue(['account_binding', 'protocol_roles', 'lifi'], 'sender and receiver must equal the bound account');
189
+ }
190
+ if (account.chainId !== value.payload.chain_id ||
191
+ account.chainId !== value.submission.chain_id) {
192
+ issue(['payload', 'chain_id'], 'all envelope chain identifiers must match');
193
+ }
194
+ if (value.submission.artifact_id !== value.artifact_id) {
195
+ issue(['submission', 'artifact_id'], 'must match the envelope artifact_id');
196
+ }
197
+ const expectedCapability = account.kind === 'eoa' ? 'send_evm_transaction' : 'submit_evm_batch';
198
+ if (value.required_capabilities[0] !== expectedCapability ||
199
+ value.submission.method !== expectedCapability) {
200
+ issue(['required_capabilities'], 'capability and submission method must match the account kind');
201
+ }
202
+ const [reset, approve, call] = value.payload.steps;
203
+ if (reset?.kind !== 'approve_reset' || approve?.kind !== 'approve' || call?.kind !== 'call') {
204
+ issue(['payload', 'steps'], 'execution steps must be ordered allowance reset, approve, then call');
205
+ }
206
+ if (value.payload.steps.some((step) => step.value !== '0')) {
207
+ issue(['payload', 'steps'], 'ERC20 execution steps must carry zero native value');
208
+ }
209
+ // The reset step is what makes the plan executable from any prior allowance state, including
210
+ // approval-race-guarded tokens that revert on a non-zero to non-zero approve. Bind its shape
211
+ // here so a dropped, re-pointed, or non-zero reset cannot pass envelope validation.
212
+ if (reset && approve) {
213
+ const decodedReset = decodeApprovalCalldata(reset.data);
214
+ const decodedApprove = decodeApprovalCalldata(approve.data);
215
+ if (!decodedReset || !decodedApprove) {
216
+ issue(['payload', 'steps'], 'both approval steps must be canonical ERC20 approve calls');
217
+ }
218
+ else if (reset.to.toLowerCase() !== approve.to.toLowerCase() ||
219
+ decodedReset.spender !== decodedApprove.spender) {
220
+ issue(['payload', 'steps'], 'the allowance reset must target the same token and spender as the approval');
221
+ }
222
+ else if (decodedReset.amount !== 0n || decodedApprove.amount <= 0n) {
223
+ issue(['payload', 'steps'], 'the allowance reset must approve zero and the approval must approve a positive amount');
224
+ }
225
+ }
226
+ // Freshness is constructor-owned: valid_until is re-derivable from the construction
227
+ // timestamp inside artifact_id and is capped by the ceiling, so a widened expiry is
228
+ // rejected without any external state.
229
+ if (!isArtifactFreshnessWellFormed(value.artifact_id, value.valid_until)) {
230
+ issue(['valid_until'], 'valid_until must follow the artifact_id construction time and stay within the freshness ceiling');
231
+ }
232
+ });
233
+ const bridgeAccountBindingSchema = ACCOUNT_PROFILE_SCHEMA.extend({
234
+ protocol_roles: z
235
+ .object({
236
+ lifi: z
237
+ .object({
238
+ sender: addressSchema,
239
+ receiver: addressSchema,
240
+ source_chain_id: z.number().int().positive().safe(),
241
+ destination_chain_id: z.number().int().positive().safe(),
242
+ sending_asset: addressSchema,
243
+ receiving_asset: addressSchema,
244
+ })
245
+ .strict(),
246
+ })
247
+ .strict(),
248
+ }).strict();
249
+ export const BRIDGE_EXECUTION_ENVELOPE_SCHEMA = z
250
+ .object({
251
+ schema_version: z.literal('1'),
252
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
253
+ rail: z.literal('lifi'),
254
+ operation: z.literal('bridge'),
255
+ intent_digest: z.string().regex(DIGEST_PATTERN),
256
+ account_binding: bridgeAccountBindingSchema,
257
+ required_capabilities: z.array(z.enum(['send_evm_transaction', 'submit_evm_batch'])).length(1),
258
+ payload: z.discriminatedUnion('kind', [
259
+ z
260
+ .object({
261
+ kind: z.literal('evm_execution_plan'),
262
+ chain_id: z.number().int().positive().safe(),
263
+ steps: z.array(evmStepSchema).length(3),
264
+ })
265
+ .strict(),
266
+ ]),
267
+ fee_attribution: z
268
+ .object({
269
+ mechanism: z.literal('lifi_integrator_fee'),
270
+ config_version: z.string().min(1).max(64),
271
+ })
272
+ .strict(),
273
+ replay_disclosure: z
274
+ .object({
275
+ artifact_role: z.literal('construction_aid'),
276
+ freshness_boundary: z.literal('valid_until'),
277
+ onchain_replay_boundary: z.literal('signer_account_nonce'),
278
+ tool_guarantees_single_execution: z.literal(false),
279
+ })
280
+ .strict(),
281
+ assertions: z
282
+ .array(z
283
+ .object({
284
+ id: z.string().min(1).max(64),
285
+ result: z.literal('passed'),
286
+ })
287
+ .strict())
288
+ .min(1),
289
+ valid_until: z.string().datetime({ offset: true }),
290
+ submission: z
291
+ .object({
292
+ method: z.enum(['send_evm_transaction', 'submit_evm_batch']),
293
+ chain_id: z.number().int().positive().safe(),
294
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
295
+ payload_digest: z.string().regex(DIGEST_PATTERN),
296
+ artifact_digest: z.string().regex(DIGEST_PATTERN),
297
+ adapter_owned_fields: z.tuple([
298
+ z.literal('nonce'),
299
+ z.literal('gas'),
300
+ z.literal('serialization'),
301
+ ]),
302
+ client_ref: z.string().min(1).max(128).optional(),
303
+ })
304
+ .strict(),
305
+ })
306
+ .strict()
307
+ .superRefine((value, context) => {
308
+ const issue = (path, message) => {
309
+ context.addIssue({ code: 'custom', path, message });
310
+ };
311
+ const account = value.account_binding;
312
+ const roles = account.protocol_roles.lifi;
313
+ if (roles.sender.toLowerCase() !== account.address.toLowerCase()) {
314
+ issue(['account_binding', 'protocol_roles', 'lifi', 'sender'], 'sender must equal the bound account');
315
+ }
316
+ // The source chain is the only chain the payload executes on; the destination chain is a
317
+ // declared fact of the route, never a chain any step is submitted to.
318
+ if (account.chainId !== roles.source_chain_id ||
319
+ account.chainId !== value.payload.chain_id ||
320
+ account.chainId !== value.submission.chain_id) {
321
+ issue(['payload', 'chain_id'], 'all source-chain identifiers must match the bound account');
322
+ }
323
+ // The enumerated v1 route set is carried in the envelope itself, so an adapter re-validates the
324
+ // route without trusting the constructor that built it. This also subsumes the
325
+ // destination-differs-from-source check: `isEnumeratedCctpBridgeRoute` returns false for an
326
+ // equal chain pair, so a separate clause for it would be unreachable by construction.
327
+ if (!isEnumeratedCctpBridgeRoute(roles.source_chain_id, roles.destination_chain_id, roles.sending_asset, roles.receiving_asset)) {
328
+ issue(['account_binding', 'protocol_roles', 'lifi'], 'route must be inside the enumerated v1 PolymerCCTP USDC route set');
329
+ }
330
+ if (value.submission.artifact_id !== value.artifact_id) {
331
+ issue(['submission', 'artifact_id'], 'must match the envelope artifact_id');
332
+ }
333
+ const expectedCapability = account.kind === 'eoa' ? 'send_evm_transaction' : 'submit_evm_batch';
334
+ if (value.required_capabilities[0] !== expectedCapability ||
335
+ value.submission.method !== expectedCapability) {
336
+ issue(['required_capabilities'], 'capability and submission method must match the account kind');
337
+ }
338
+ const [reset, approve, call] = value.payload.steps;
339
+ if (reset?.kind !== 'approve_reset' || approve?.kind !== 'approve' || call?.kind !== 'call') {
340
+ issue(['payload', 'steps'], 'execution steps must be ordered allowance reset, approve, then call');
341
+ }
342
+ if (value.payload.steps.some((step) => step.value !== '0')) {
343
+ issue(['payload', 'steps'], 'ERC20 execution steps must carry zero native value');
344
+ }
345
+ // Both approval steps must target the source-chain USDC being bridged: an approval pointed at
346
+ // any other token is an allowance the plan never needed.
347
+ if (reset &&
348
+ approve &&
349
+ (reset.to.toLowerCase() !== roles.sending_asset.toLowerCase() ||
350
+ approve.to.toLowerCase() !== roles.sending_asset.toLowerCase())) {
351
+ issue(['payload', 'steps'], 'approval steps must target the bridged source asset');
352
+ }
353
+ if (reset && approve) {
354
+ const decodedReset = decodeApprovalCalldata(reset.data);
355
+ const decodedApprove = decodeApprovalCalldata(approve.data);
356
+ if (!decodedReset || !decodedApprove) {
357
+ issue(['payload', 'steps'], 'both approval steps must be canonical ERC20 approve calls');
358
+ }
359
+ else if (reset.to.toLowerCase() !== approve.to.toLowerCase() ||
360
+ decodedReset.spender !== decodedApprove.spender) {
361
+ issue(['payload', 'steps'], 'the allowance reset must target the same token and spender as the approval');
362
+ }
363
+ else if (decodedReset.amount !== 0n || decodedApprove.amount <= 0n) {
364
+ issue(['payload', 'steps'], 'the allowance reset must approve zero and the approval must approve a positive amount');
365
+ }
366
+ }
367
+ if (!isArtifactFreshnessWellFormed(value.artifact_id, value.valid_until)) {
368
+ issue(['valid_until'], 'valid_until must follow the artifact_id construction time and stay within the freshness ceiling');
369
+ }
370
+ });
371
+ // =============================================================================
372
+ // Polymarket CLOB V2 limit order (spec §7.3)
373
+ // =============================================================================
374
+ const uint256String = z.string().regex(/^\d+$/, 'must be a decimal uint256');
375
+ const bytes32Schema = z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'must be a 32-byte hex value');
376
+ const salt = z.string().regex(/^[1-9]\d*$/, 'must be a positive decimal integer');
377
+ /** Probability price as an exact decimal string; 1-4 fractional digits covers every tick size. */
378
+ const polymarketPriceSchema = z
379
+ .string()
380
+ .regex(/^0\.\d{1,4}$/, 'must be a decimal probability strictly between 0 and 1');
381
+ /**
382
+ * Order size in shares. At most two fractional digits — the precision every tick size rounds size
383
+ * to — so the constructor never depends on the SDK silently truncating a third digit, and the
384
+ * derived collateral amount stays exact integer arithmetic.
385
+ */
386
+ const polymarketSizeSchema = z
387
+ .string()
388
+ .regex(/^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/, 'must be a decimal share size with at most 2 fractional digits')
389
+ .refine((value) => Number(value) > 0, 'must be greater than zero');
390
+ export const PREPARE_POLYMARKET_ORDER_INPUT_SCHEMA = z
391
+ .object({
392
+ accountProfile: ACCOUNT_PROFILE_SCHEMA,
393
+ tokenId: z
394
+ .string()
395
+ .regex(/^[1-9]\d{0,77}$/, 'must be a positive decimal CTF token id')
396
+ .describe('Outcome-token id (NOT the condition id): YES and NO are different token ids.'),
397
+ side: z.enum(['BUY', 'SELL']),
398
+ price: polymarketPriceSchema.describe("Limit price as a probability, e.g. '0.045'. Must be an exact multiple of the market's minimum tick size."),
399
+ size: polymarketSizeSchema.describe('Order size in outcome-token shares.'),
400
+ client_ref: z.string().trim().min(1).max(128).optional(),
401
+ })
402
+ .strict();
403
+ const polymarketAccountBindingSchema = ACCOUNT_PROFILE_SCHEMA.extend({
404
+ protocol_roles: z
405
+ .object({
406
+ polymarket: z
407
+ .object({
408
+ maker: addressSchema,
409
+ signer: addressSchema,
410
+ funder: addressSchema,
411
+ exchange: addressSchema,
412
+ token_id: uint256String,
413
+ signature_type: z.literal(0),
414
+ })
415
+ .strict(),
416
+ })
417
+ .strict(),
418
+ }).strict();
419
+ /** The SDK's own order args, pre-signing. No `signature` field exists on this shape by design. */
420
+ const POLYMARKET_ORDER_ARGS_SCHEMA = z
421
+ .object({
422
+ salt,
423
+ maker: addressSchema,
424
+ signer: addressSchema,
425
+ tokenId: uint256String,
426
+ makerAmount: uint256String,
427
+ takerAmount: uint256String,
428
+ side: z.enum(['BUY', 'SELL']),
429
+ signatureType: z.literal(0),
430
+ metadata: bytes32Schema,
431
+ builder: bytes32Schema,
432
+ timestamp: uint256String,
433
+ /** Unsigned POST-time field: the V2 exchange's Order struct does not cover expiration. */
434
+ expiration: uint256String,
435
+ })
436
+ .strict();
437
+ const POLYMARKET_TYPED_DATA_SCHEMA = z
438
+ .object({
439
+ primary_type: z.literal(POLYMARKET_ORDER_PRIMARY_TYPE),
440
+ domain: z
441
+ .object({
442
+ name: z.literal(POLYMARKET_EIP712_DOMAIN_NAME),
443
+ version: z.literal(POLYMARKET_EIP712_DOMAIN_VERSION),
444
+ chainId: z.literal(POLYMARKET_CHAIN_ID),
445
+ verifyingContract: addressSchema,
446
+ })
447
+ .strict(),
448
+ types: z
449
+ .object({
450
+ Order: z
451
+ .array(z.object({ name: z.string().min(1), type: z.string().min(1) }).strict())
452
+ .length(POLYMARKET_ORDER_V2_EIP712_FIELDS.length),
453
+ })
454
+ .strict(),
455
+ message: z
456
+ .object({
457
+ salt,
458
+ maker: addressSchema,
459
+ signer: addressSchema,
460
+ tokenId: uint256String,
461
+ makerAmount: uint256String,
462
+ takerAmount: uint256String,
463
+ side: z.union([z.literal(0), z.literal(1)]),
464
+ signatureType: z.literal(0),
465
+ timestamp: uint256String,
466
+ metadata: bytes32Schema,
467
+ builder: bytes32Schema,
468
+ })
469
+ .strict(),
470
+ })
471
+ .strict();
472
+ export const POLYMARKET_ORDER_PAYLOAD_SCHEMA = z
473
+ .object({
474
+ kind: z.literal('polymarket_order_v2'),
475
+ chain_id: z.literal(POLYMARKET_CHAIN_ID),
476
+ exchange: addressSchema,
477
+ neg_risk: z.boolean(),
478
+ tick_size: z.enum(POLYMARKET_TICK_SIZES),
479
+ /** v1 is a signed limit order; market orders walk a live book and are out of scope. */
480
+ order_type: z.literal('GTC'),
481
+ order: POLYMARKET_ORDER_ARGS_SCHEMA,
482
+ eip712: POLYMARKET_TYPED_DATA_SCHEMA,
483
+ order_hash: z.string().regex(DIGEST_PATTERN),
484
+ })
485
+ .strict()
486
+ .superRefine((value, context) => {
487
+ const issue = (path, message) => {
488
+ context.addIssue({ code: 'custom', path, message });
489
+ };
490
+ // The pinned struct is what gives every other field its meaning, so an adapter re-checks it
491
+ // rather than trusting whatever type array arrived with the artifact.
492
+ const fields = value.eip712.types.Order;
493
+ if (fields.some((field, index) => field.name !== POLYMARKET_ORDER_V2_EIP712_FIELDS[index]?.name ||
494
+ field.type !== POLYMARKET_ORDER_V2_EIP712_FIELDS[index]?.type)) {
495
+ issue(['eip712', 'types', 'Order'], 'must be the pinned CTF Exchange V2 Order struct');
496
+ }
497
+ if (value.eip712.domain.verifyingContract.toLowerCase() !== value.exchange.toLowerCase()) {
498
+ issue(['eip712', 'domain', 'verifyingContract'], 'must equal the payload exchange');
499
+ }
500
+ // Two representations of one order: the args the adapter POSTs and the struct it signs. They are
501
+ // cross-bound here so a tampered POST body cannot diverge from the signed digest.
502
+ const message = value.eip712.message;
503
+ const order = value.order;
504
+ const mismatched = message.salt !== order.salt ||
505
+ message.maker.toLowerCase() !== order.maker.toLowerCase() ||
506
+ message.signer.toLowerCase() !== order.signer.toLowerCase() ||
507
+ message.tokenId !== order.tokenId ||
508
+ message.makerAmount !== order.makerAmount ||
509
+ message.takerAmount !== order.takerAmount ||
510
+ message.timestamp !== order.timestamp ||
511
+ message.signatureType !== order.signatureType ||
512
+ message.metadata.toLowerCase() !== order.metadata.toLowerCase() ||
513
+ message.builder.toLowerCase() !== order.builder.toLowerCase() ||
514
+ message.side !== (order.side === 'BUY' ? 0 : 1);
515
+ if (mismatched) {
516
+ issue(['eip712', 'message'], 'signed order struct must match the order args exactly');
517
+ }
518
+ if (order.tokenId !== value.eip712.message.tokenId) {
519
+ issue(['order', 'tokenId'], 'must match the signed token id');
520
+ }
521
+ // The digest is recomputed from the declared domain, struct, and message: the exact bytes the
522
+ // user's signer will sign. A tampered field that skipped a digest recomputation is refused here.
523
+ let recomputed;
524
+ try {
525
+ recomputed = hashTypedData({
526
+ domain: {
527
+ ...value.eip712.domain,
528
+ verifyingContract: value.eip712.domain.verifyingContract,
529
+ },
530
+ types: { Order: [...value.eip712.types.Order] },
531
+ primaryType: value.eip712.primary_type,
532
+ message: value.eip712.message,
533
+ });
534
+ }
535
+ catch {
536
+ recomputed = undefined;
537
+ }
538
+ if (recomputed?.toLowerCase() !== value.order_hash.toLowerCase()) {
539
+ issue(['order_hash'], 'must equal the EIP-712 digest of the declared domain, types, and message');
540
+ }
541
+ if (BigInt(order.makerAmount) <= 0n || BigInt(order.takerAmount) <= 0n) {
542
+ issue(['order'], 'both order legs must carry a positive amount');
543
+ }
544
+ // `expiration` is NOT covered by the V2 signed struct, so a non-zero value on a GTC order is an
545
+ // unsigned promise the exchange may honour or ignore while the signature stays valid. A GTC
546
+ // artifact therefore declares zero, and an adapter re-checks it here rather than posting it.
547
+ if (value.order_type === 'GTC' && order.expiration !== '0') {
548
+ issue(['order', 'expiration'], 'a GTC order must carry a zero expiration');
549
+ }
550
+ });
551
+ export const POLYMARKET_EXECUTION_ENVELOPE_SCHEMA = z
552
+ .object({
553
+ schema_version: z.literal('1'),
554
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
555
+ rail: z.literal('polymarket'),
556
+ operation: z.literal('pm_order'),
557
+ intent_digest: z.string().regex(DIGEST_PATTERN),
558
+ account_binding: polymarketAccountBindingSchema,
559
+ required_capabilities: z.tuple([z.literal('sign_eip712')]),
560
+ payload: POLYMARKET_ORDER_PAYLOAD_SCHEMA,
561
+ fee_attribution: z
562
+ .object({
563
+ mechanism: z.literal('polymarket_builder_code'),
564
+ config_version: z.string().min(1).max(64),
565
+ })
566
+ .strict(),
567
+ replay_disclosure: z
568
+ .object({
569
+ artifact_role: z.literal('construction_aid'),
570
+ freshness_boundary: z.literal('valid_until'),
571
+ /**
572
+ * The PM rail's replay boundary is NOT a signer account nonce: an unsigned order struct
573
+ * carries its own salt and timestamp, so re-signing the same artifact reproduces the same
574
+ * order hash and the exchange's own order bookkeeping is what rejects the duplicate. Same
575
+ * Founder ruling as slice 1 (the constructor owns freshness only), correct boundary named.
576
+ */
577
+ onchain_replay_boundary: z.literal('exchange_order_hash'),
578
+ tool_guarantees_single_execution: z.literal(false),
579
+ })
580
+ .strict(),
581
+ assertions: z
582
+ .array(z.object({ id: z.string().min(1).max(64), result: z.literal('passed') }).strict())
583
+ .min(1),
584
+ valid_until: z.string().datetime({ offset: true }),
585
+ submission: z
586
+ .object({
587
+ method: z.literal('sign_eip712'),
588
+ chain_id: z.literal(POLYMARKET_CHAIN_ID),
589
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
590
+ payload_digest: z.string().regex(DIGEST_PATTERN),
591
+ artifact_digest: z.string().regex(DIGEST_PATTERN),
592
+ /** The adapter owns the signature and the authenticated CLOB post; nothing else. */
593
+ adapter_owned_fields: z.tuple([z.literal('signature'), z.literal('clob_post')]),
594
+ client_ref: z.string().min(1).max(128).optional(),
595
+ })
596
+ .strict(),
597
+ })
598
+ .strict()
599
+ .superRefine((value, context) => {
600
+ const issue = (path, message) => {
601
+ context.addIssue({ code: 'custom', path, message });
602
+ };
603
+ const account = value.account_binding;
604
+ const roles = account.protocol_roles.polymarket;
605
+ const accountAddress = account.address.toLowerCase();
606
+ // v1 binds one EOA to all three roles: it signs, it makes, and it funds. A Safe profile would
607
+ // need a signature type this spec has not ruled on, so it never reaches this schema.
608
+ if (account.kind !== 'eoa' ||
609
+ roles.maker.toLowerCase() !== accountAddress ||
610
+ roles.signer.toLowerCase() !== accountAddress ||
611
+ roles.funder.toLowerCase() !== accountAddress) {
612
+ issue(['account_binding', 'protocol_roles', 'polymarket'], 'maker, signer, and funder must all equal the bound EOA');
613
+ }
614
+ if (account.chainId !== POLYMARKET_CHAIN_ID ||
615
+ value.payload.chain_id !== POLYMARKET_CHAIN_ID ||
616
+ value.submission.chain_id !== POLYMARKET_CHAIN_ID) {
617
+ issue(['payload', 'chain_id'], 'Polymarket artifacts are Polygon-only');
618
+ }
619
+ if (roles.exchange.toLowerCase() !== value.payload.exchange.toLowerCase() ||
620
+ roles.token_id !== value.payload.order.tokenId ||
621
+ roles.signature_type !== value.payload.order.signatureType) {
622
+ issue(['account_binding', 'protocol_roles', 'polymarket'], 'bound exchange, token id, and signature type must match the signed order');
623
+ }
624
+ // Roles live INSIDE the signed struct (spec §4), so binding them outside is only meaningful if
625
+ // the signed struct carries the same addresses.
626
+ if (value.payload.order.maker.toLowerCase() !== roles.maker.toLowerCase() ||
627
+ value.payload.order.signer.toLowerCase() !== roles.signer.toLowerCase()) {
628
+ issue(['payload', 'order'], 'signed maker and signer must match the bound account roles');
629
+ }
630
+ if (value.submission.artifact_id !== value.artifact_id) {
631
+ issue(['submission', 'artifact_id'], 'must match the envelope artifact_id');
632
+ }
633
+ if (!isArtifactFreshnessWellFormed(value.artifact_id, value.valid_until)) {
634
+ issue(['valid_until'], 'valid_until must follow the artifact_id construction time and stay within the freshness ceiling');
635
+ }
636
+ });
637
+ // =============================================================================
638
+ // Hyperliquid perp L1 action (spec §7.2)
639
+ // =============================================================================
640
+ /**
641
+ * Exact, normalized decimal: no leading zeros, no trailing zeros after the point, strictly positive.
642
+ * The caller's string is what lands in the signed action verbatim, so the grid checks in
643
+ * `hyperliquid-order-assertions.ts` validate it rather than rounding it onto the venue's grid.
644
+ */
645
+ const hyperliquidDecimalSchema = z
646
+ .string()
647
+ .regex(/^(?:0|[1-9]\d{0,15})(?:\.\d{0,14}[1-9])?$/, 'must be a normalized positive decimal: no leading zeros, no trailing zeros after the point')
648
+ .refine((value) => value !== '0', 'must be greater than zero');
649
+ const hyperliquidClientOrderIdSchema = z
650
+ .string()
651
+ .regex(/^0x[0-9a-f]{32}$/, 'must be 0x followed by 32 lowercase hex characters');
652
+ const hyperliquidLimitTypeSchema = z
653
+ .object({
654
+ limit: z.object({ tif: z.enum(HYPERLIQUID_TIME_IN_FORCE) }).strict(),
655
+ })
656
+ .strict();
657
+ const hyperliquidTriggerTypeSchema = z
658
+ .object({
659
+ trigger: z
660
+ .object({
661
+ /** Market triggers only: a limit trigger can arm and never fill, leaving the bracket unenforced. */
662
+ isMarket: z.literal(true),
663
+ triggerPx: hyperliquidDecimalSchema,
664
+ tpsl: z.enum(['tp', 'sl']),
665
+ })
666
+ .strict(),
667
+ })
668
+ .strict();
669
+ /**
670
+ * One order leg, with keys declared in the venue's canonical msgpack order. The declaration order is
671
+ * load-bearing: zod rebuilds parsed objects in schema order, so this is what fixes the key order of
672
+ * the emitted artifact, and the envelope's own refinement checks it against
673
+ * `HYPERLIQUID_ORDER_KEY_ORDER` so a schema edit that drifts from the pinned order fails loudly.
674
+ */
675
+ const HYPERLIQUID_ORDER_LEG_SCHEMA = z
676
+ .object({
677
+ a: z.number().int().min(0).safe(),
678
+ b: z.boolean(),
679
+ p: hyperliquidDecimalSchema,
680
+ s: hyperliquidDecimalSchema,
681
+ r: z.boolean(),
682
+ t: z.union([hyperliquidLimitTypeSchema, hyperliquidTriggerTypeSchema]),
683
+ c: hyperliquidClientOrderIdSchema,
684
+ })
685
+ .strict();
686
+ /**
687
+ * The canonical `order` action. `.strict()` is the schema-level bind on the RULED valueless builder
688
+ * field (spec §7.2): a `builder` key cannot pass envelope validation at all, so an unruled attribution
689
+ * value cannot reach an artifact through this schema even if a future constructor tried to stamp one.
690
+ */
691
+ const HYPERLIQUID_ORDER_ACTION_SCHEMA = z
692
+ .object({
693
+ type: z.literal('order'),
694
+ orders: z.array(HYPERLIQUID_ORDER_LEG_SCHEMA).min(1).max(3),
695
+ grouping: z.enum(HYPERLIQUID_ORDER_GROUPINGS),
696
+ })
697
+ .strict();
698
+ const HYPERLIQUID_SIGNING_DECLARATION_SCHEMA = z
699
+ .object({
700
+ scheme: z.literal('hyperliquid_l1_agent_eip712'),
701
+ primary_type: z.literal(HYPERLIQUID_AGENT_PRIMARY_TYPE),
702
+ domain: z
703
+ .object({
704
+ name: z.literal(HYPERLIQUID_EIP712_DOMAIN_NAME),
705
+ version: z.literal(HYPERLIQUID_EIP712_DOMAIN_VERSION),
706
+ /** Hyperliquid pins 1337 for every L1 action, whatever chain the wallet is connected to. */
707
+ chainId: z.literal(HYPERLIQUID_L1_SIGNATURE_CHAIN_ID),
708
+ /** The L1-action domain has no verifying contract, so `addressSchema`'s zero-check cannot apply. */
709
+ verifyingContract: z.literal(HYPERLIQUID_EIP712_VERIFYING_CONTRACT),
710
+ })
711
+ .strict(),
712
+ types: z
713
+ .object({
714
+ Agent: z
715
+ .array(z.object({ name: z.string().min(1), type: z.string().min(1) }).strict())
716
+ .length(HYPERLIQUID_AGENT_EIP712_FIELDS.length),
717
+ })
718
+ .strict(),
719
+ /** `"a"` is mainnet; a testnet artifact would carry `"b"` and is out of v1 scope. */
720
+ source: z.literal(HYPERLIQUID_MAINNET_AGENT_SOURCE),
721
+ connection_id: z
722
+ .object({
723
+ hash: z.literal('keccak256'),
724
+ preimage: z.tuple([
725
+ z.literal('msgpack(action)'),
726
+ z.literal('nonce_uint64_be'),
727
+ z.literal('vault_address_marker'),
728
+ z.literal('vault_address_bytes'),
729
+ z.literal('expires_after_marker'),
730
+ z.literal('expires_after_uint64_be'),
731
+ ]),
732
+ /** The nonce is chosen at signing time; the constructor never pre-commits one (§10). */
733
+ nonce_owner: z.literal('adapter'),
734
+ })
735
+ .strict(),
736
+ })
737
+ .strict();
738
+ export const HYPERLIQUID_L1_ACTION_PAYLOAD_SCHEMA = z
739
+ .object({
740
+ kind: z.literal('hyperliquid_l1_action'),
741
+ chain_id: z.literal(HYPERLIQUID_L1_SIGNATURE_CHAIN_ID),
742
+ action: HYPERLIQUID_ORDER_ACTION_SCHEMA,
743
+ signing: HYPERLIQUID_SIGNING_DECLARATION_SCHEMA,
744
+ /** Declared profile fact only. `null` means the action trades the signer's own account. */
745
+ vault_address: addressSchema.nullable(),
746
+ /**
747
+ * Constructor-owned expiry, inside the signed digest. This is what makes the artifact's own
748
+ * freshness bound VENUE-ENFORCED on this rail: Hyperliquid rejects an action received after
749
+ * `expiresAfter` ("a timestamp in milliseconds after which the action will be rejected").
750
+ */
751
+ expires_after: z.number().int().positive().safe(),
752
+ })
753
+ .strict()
754
+ .superRefine((value, context) => {
755
+ const issue = (path, message) => {
756
+ context.addIssue({ code: 'custom', path, message });
757
+ };
758
+ // msgpack encodes keys in insertion order, so the emitted key order is part of what the signature
759
+ // commits to. Checked here against the pinned constants so a schema-declaration edit that drifts
760
+ // from them cannot ship silently.
761
+ const actionKeys = Object.keys(value.action);
762
+ if (actionKeys.length !== HYPERLIQUID_ACTION_KEY_ORDER.length ||
763
+ actionKeys.some((key, index) => key !== HYPERLIQUID_ACTION_KEY_ORDER[index])) {
764
+ issue(['action'], 'action keys must be in the canonical Hyperliquid msgpack order');
765
+ }
766
+ const expectedLegKeys = [...HYPERLIQUID_ORDER_KEY_ORDER, 'c'];
767
+ for (const [index, leg] of value.action.orders.entries()) {
768
+ const legKeys = Object.keys(leg);
769
+ if (legKeys.length !== expectedLegKeys.length ||
770
+ legKeys.some((key, position) => key !== expectedLegKeys[position])) {
771
+ issue(['action', 'orders', index], 'order leg keys must be in the canonical msgpack order');
772
+ }
773
+ if ('trigger' in leg.t) {
774
+ const triggerKeys = Object.keys(leg.t.trigger);
775
+ if (triggerKeys.length !== HYPERLIQUID_TRIGGER_KEY_ORDER.length ||
776
+ triggerKeys.some((key, position) => key !== HYPERLIQUID_TRIGGER_KEY_ORDER[position])) {
777
+ issue(['action', 'orders', index, 't', 'trigger'], 'trigger keys must be in the canonical msgpack order');
778
+ }
779
+ }
780
+ }
781
+ const fields = value.signing.types.Agent;
782
+ if (fields.some((field, index) => field.name !== HYPERLIQUID_AGENT_EIP712_FIELDS[index]?.name ||
783
+ field.type !== HYPERLIQUID_AGENT_EIP712_FIELDS[index]?.type)) {
784
+ issue(['signing', 'types', 'Agent'], 'must be the pinned Hyperliquid Agent struct');
785
+ }
786
+ if (value.signing.domain.chainId !== value.chain_id) {
787
+ issue(['signing', 'domain', 'chainId'], 'must equal the payload signature chain id');
788
+ }
789
+ // All legs are one order on one market: a bundle whose legs name different assets is not a
790
+ // bracket, and the grouping the venue applies would bind unrelated markets together.
791
+ const assetIds = new Set(value.action.orders.map((leg) => leg.a));
792
+ if (assetIds.size !== 1) {
793
+ issue(['action', 'orders'], 'every leg must name the same perp asset id');
794
+ }
795
+ const clientOrderIds = new Set(value.action.orders.map((leg) => leg.c));
796
+ if (clientOrderIds.size !== value.action.orders.length) {
797
+ issue(['action', 'orders'], 'each leg must carry a distinct client order id');
798
+ }
799
+ // `normalTpsl` is what binds fixed-size reduce-only triggers to the entry; a lone entry declaring
800
+ // it, or a bracket declaring `na`, describes a different venue behaviour than the legs present.
801
+ const triggerLegs = value.action.orders.filter((leg) => 'trigger' in leg.t);
802
+ const expectedGrouping = triggerLegs.length > 0 ? 'normalTpsl' : 'na';
803
+ if (value.action.grouping !== expectedGrouping) {
804
+ issue(['action', 'grouping'], `must be ${expectedGrouping} for ${triggerLegs.length} trigger legs`);
805
+ }
806
+ // Every trigger leg is a reduce-only close of the entry, on the opposite side, at its own trigger
807
+ // price. A trigger that is not reduce-only OPENS a position instead of closing one.
808
+ const entryLegs = value.action.orders.filter((leg) => 'limit' in leg.t);
809
+ if (entryLegs.length !== 1) {
810
+ issue(['action', 'orders'], 'exactly one limit entry leg is required');
811
+ }
812
+ const entry = entryLegs[0];
813
+ for (const [index, leg] of value.action.orders.entries()) {
814
+ if (!('trigger' in leg.t) || !entry)
815
+ continue;
816
+ if (leg.r !== true) {
817
+ issue(['action', 'orders', index, 'r'], 'a trigger leg must be reduce-only');
818
+ }
819
+ if (leg.b === entry.b) {
820
+ issue(['action', 'orders', index, 'b'], 'a trigger leg must close the entry, so it sits on the opposite side');
821
+ }
822
+ if (leg.s !== entry.s) {
823
+ issue(['action', 'orders', index, 's'], 'a fixed-size trigger leg must match the entry size');
824
+ }
825
+ if (leg.p !== leg.t.trigger.triggerPx) {
826
+ issue(['action', 'orders', index, 'p'], 'a market trigger leg prices at its own trigger price');
827
+ }
828
+ }
829
+ const takeProfitLegs = value.action.orders.filter((leg) => 'trigger' in leg.t && leg.t.trigger.tpsl === 'tp');
830
+ const stopLossLegs = value.action.orders.filter((leg) => 'trigger' in leg.t && leg.t.trigger.tpsl === 'sl');
831
+ if (takeProfitLegs.length > 1 || stopLossLegs.length > 1) {
832
+ issue(['action', 'orders'], 'at most one take-profit and one stop-loss leg are supported');
833
+ }
834
+ });
835
+ const hyperliquidAccountBindingSchema = ACCOUNT_PROFILE_SCHEMA.extend({
836
+ protocol_roles: z
837
+ .object({
838
+ hyperliquid: z
839
+ .object({
840
+ /** The Hyperliquid account of record whose margin and positions the action moves. */
841
+ master: addressSchema,
842
+ /**
843
+ * Who signs the L1 action. Equal to `master` when the user signs every order themselves, or
844
+ * a user-held approved API wallet. Otto NEVER derives one (spec §11).
845
+ */
846
+ trading_signer: addressSchema,
847
+ /** Declared profile fact; `null` for the signer's own account. */
848
+ vault_address: addressSchema.nullable(),
849
+ asset: z.string().min(1).max(32),
850
+ /** Positional index in the live perp universe. */
851
+ asset_id: z.number().int().min(0).safe(),
852
+ sz_decimals: z.number().int().min(0).max(18),
853
+ })
854
+ .strict(),
855
+ })
856
+ .strict(),
857
+ }).strict();
858
+ export const PREPARE_PERP_ORDER_INPUT_SCHEMA = z
859
+ .object({
860
+ accountProfile: ACCOUNT_PROFILE_SCHEMA.describe('The Hyperliquid master account. `chainId` must be 1337, the signature chain id Hyperliquid pins for every L1 action.'),
861
+ asset: z
862
+ .string()
863
+ .regex(/^[A-Za-z0-9]{1,32}$/, 'must be a perp symbol from the live Hyperliquid universe')
864
+ .describe("Perp symbol exactly as the venue names it, e.g. 'BTC', 'kPEPE'. Case-sensitive."),
865
+ side: z.enum(['BUY', 'SELL']),
866
+ size: hyperliquidDecimalSchema.describe("Order size in base units, e.g. '0.01'. At most szDecimals fractional digits for this perp."),
867
+ limitPrice: hyperliquidDecimalSchema.describe('Limit price. Must sit on the venue grid: at most 5 significant digits unless integer, and at most (6 - szDecimals) decimals.'),
868
+ timeInForce: z
869
+ .enum(HYPERLIQUID_TIME_IN_FORCE)
870
+ .default('Gtc')
871
+ .describe("Gtc rests, Ioc fills-or-cancels, Alo posts only. There is no market order: name your own aggressive limit price with 'Ioc' instead."),
872
+ reduceOnly: z.boolean().default(false),
873
+ takeProfitPrice: hyperliquidDecimalSchema
874
+ .optional()
875
+ .describe('Optional reduce-only market take-profit trigger, bundled with grouping normalTpsl.'),
876
+ stopLossPrice: hyperliquidDecimalSchema
877
+ .optional()
878
+ .describe('Optional reduce-only market stop-loss trigger, bundled with grouping normalTpsl.'),
879
+ tradingSigner: addressSchema
880
+ .optional()
881
+ .describe('The address that will sign, when it is not the master account itself: a user-held approved API wallet. Declared, never derived.'),
882
+ vaultAddress: addressSchema
883
+ .optional()
884
+ .describe('Trade on behalf of this vault or sub-account. A declared profile fact only.'),
885
+ client_ref: z.string().trim().min(1).max(128).optional(),
886
+ })
887
+ .strict()
888
+ .superRefine((value, context) => {
889
+ if (value.takeProfitPrice !== undefined &&
890
+ value.takeProfitPrice === value.stopLossPrice) {
891
+ context.addIssue({
892
+ code: 'custom',
893
+ path: ['stopLossPrice'],
894
+ message: 'must differ from takeProfitPrice',
895
+ });
896
+ }
897
+ if (value.accountProfile.chainId !== HYPERLIQUID_L1_SIGNATURE_CHAIN_ID) {
898
+ context.addIssue({
899
+ code: 'custom',
900
+ path: ['accountProfile', 'chainId'],
901
+ message: `must be ${HYPERLIQUID_L1_SIGNATURE_CHAIN_ID}, the signature chain id Hyperliquid pins for L1 actions`,
902
+ });
903
+ }
904
+ });
905
+ export const HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA = z
906
+ .object({
907
+ schema_version: z.literal('1'),
908
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
909
+ rail: z.literal('hyperliquid'),
910
+ operation: z.literal('perp_order'),
911
+ intent_digest: z.string().regex(DIGEST_PATTERN),
912
+ account_binding: hyperliquidAccountBindingSchema,
913
+ required_capabilities: z.tuple([z.literal('sign_hyperliquid_l1')]),
914
+ payload: HYPERLIQUID_L1_ACTION_PAYLOAD_SCHEMA,
915
+ fee_attribution: z
916
+ .object({
917
+ mechanism: z.literal('hyperliquid_builder_fee'),
918
+ config_version: z.string().min(1).max(64),
919
+ /**
920
+ * The RULED state of the builder value (Founder 2026-08-17, spec §7.2): spec'd and VALUELESS
921
+ * until the Step-0 `approveBuilderFee` reversal lands. Carried as a machine-readable marker so
922
+ * a consumer can tell "attribution pending a ruling" apart from "attribution missing", and so
923
+ * the follow-up fold that wires `{b, f}` is a visible state change rather than a silent one.
924
+ */
925
+ status: z.literal(HL_BUILDER_ATTRIBUTION_STATUS),
926
+ })
927
+ .strict(),
928
+ replay_disclosure: z
929
+ .object({
930
+ artifact_role: z.literal('construction_aid'),
931
+ freshness_boundary: z.literal('valid_until'),
932
+ /**
933
+ * The venue-correct boundary for this rail, and it is NOT a signer account nonce: the signed
934
+ * digest covers an adapter-chosen Hyperliquid nonce AND the constructor's `expiresAfter`.
935
+ * Hyperliquid stores "the 100 highest nonces per address" and requires every new action to
936
+ * carry a nonce larger than the smallest of them and never previously used, within
937
+ * `(T - 2 days, T + 1 day)`; it also rejects any action received after `expiresAfter`. So
938
+ * re-signing this artifact needs a fresh nonce and still expires with the artifact — but the
939
+ * constructor cannot enforce single execution, which is why it says so (§10).
940
+ */
941
+ onchain_replay_boundary: z.literal('signer_nonce_and_expires_after'),
942
+ tool_guarantees_single_execution: z.literal(false),
943
+ })
944
+ .strict(),
945
+ assertions: z
946
+ .array(z.object({ id: z.string().min(1).max(64), result: z.literal('passed') }).strict())
947
+ .min(1),
948
+ valid_until: z.string().datetime({ offset: true }),
949
+ submission: z
950
+ .object({
951
+ method: z.literal('sign_hyperliquid_l1'),
952
+ chain_id: z.literal(HYPERLIQUID_L1_SIGNATURE_CHAIN_ID),
953
+ artifact_id: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
954
+ payload_digest: z.string().regex(DIGEST_PATTERN),
955
+ artifact_digest: z.string().regex(DIGEST_PATTERN),
956
+ /** The adapter owns the nonce, the signature, and the authenticated exchange POST; nothing else. */
957
+ adapter_owned_fields: z.tuple([
958
+ z.literal('nonce'),
959
+ z.literal('signature'),
960
+ z.literal('exchange_post'),
961
+ ]),
962
+ client_ref: z.string().min(1).max(128).optional(),
963
+ })
964
+ .strict(),
965
+ })
966
+ .strict()
967
+ .superRefine((value, context) => {
968
+ const issue = (path, message) => {
969
+ context.addIssue({ code: 'custom', path, message });
970
+ };
971
+ const account = value.account_binding;
972
+ const roles = account.protocol_roles.hyperliquid;
973
+ // Hyperliquid recovers an L1 action's signer with ecrecover, so the signing account must be an
974
+ // EOA. A Safe would need ERC-1271, which the venue does not accept.
975
+ if (account.kind !== 'eoa' || roles.master.toLowerCase() !== account.address.toLowerCase()) {
976
+ issue(['account_binding', 'protocol_roles', 'hyperliquid', 'master'], 'the master account must be the bound EOA');
977
+ }
978
+ if (account.chainId !== HYPERLIQUID_L1_SIGNATURE_CHAIN_ID ||
979
+ value.payload.chain_id !== HYPERLIQUID_L1_SIGNATURE_CHAIN_ID ||
980
+ value.submission.chain_id !== HYPERLIQUID_L1_SIGNATURE_CHAIN_ID) {
981
+ issue(['payload', 'chain_id'], 'Hyperliquid L1 actions pin signature chain id 1337');
982
+ }
983
+ // The vault address lives in the SIGNED digest preimage, so the binding outside is only
984
+ // meaningful if the payload declares the same value.
985
+ if ((roles.vault_address?.toLowerCase() ?? null) !==
986
+ (value.payload.vault_address?.toLowerCase() ?? null)) {
987
+ issue(['account_binding', 'protocol_roles', 'hyperliquid', 'vault_address'], 'must equal the vault address declared in the payload');
988
+ }
989
+ if (value.payload.action.orders.some((leg) => leg.a !== roles.asset_id)) {
990
+ issue(['payload', 'action', 'orders'], 'every leg must name the bound asset id');
991
+ }
992
+ if (value.submission.artifact_id !== value.artifact_id) {
993
+ issue(['submission', 'artifact_id'], 'must match the envelope artifact_id');
994
+ }
995
+ // `expiresAfter` is the constructor's own freshness bound placed inside the signed digest, so the
996
+ // two must agree exactly; otherwise the envelope promises one deadline and the venue enforces
997
+ // another.
998
+ if (value.payload.expires_after !== Date.parse(value.valid_until)) {
999
+ issue(['payload', 'expires_after'], 'must equal valid_until in unix milliseconds: the venue enforces this deadline inside the signed digest');
1000
+ }
1001
+ if (!isArtifactFreshnessWellFormed(value.artifact_id, value.valid_until)) {
1002
+ issue(['valid_until'], 'valid_until must follow the artifact_id construction time and stay within the freshness ceiling');
1003
+ }
1004
+ });
1005
+ /**
1006
+ * Every artifact this MCP produces. Operation is a literal on each branch, so a swap artifact can
1007
+ * never satisfy the bridge branch or the reverse — the union widens what adapters accept without
1008
+ * loosening either branch's binds.
1009
+ */
1010
+ export const EXECUTION_ARTIFACT_SCHEMA = z.union([
1011
+ EXECUTION_ENVELOPE_SCHEMA,
1012
+ BRIDGE_EXECUTION_ENVELOPE_SCHEMA,
1013
+ POLYMARKET_EXECUTION_ENVELOPE_SCHEMA,
1014
+ HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA,
1015
+ ]);
1016
+ /** The enumerated v1 bridge chains, surfaced for the tool description and for callers. */
1017
+ export const ENUMERATED_BRIDGE_CHAIN_IDS = Object.freeze(Object.keys(LIFI_CCTP_USDC_BY_CHAIN)
1018
+ .map((chainId) => Number(chainId))
1019
+ .sort((left, right) => left - right));
1020
+ export const EXECUTION_TOOL_DEFINITIONS = Object.freeze([
1021
+ Object.freeze({
1022
+ name: 'otto_prepare_swap',
1023
+ title: 'Prepare an Otto-attributed LI.FI swap',
1024
+ description: "Prepare, decode, and verify an unsigned same-chain EVM ERC20 swap construction aid for the caller-owned EOA or Safe. The envelope's valid_until bounds freshness and is committed to the artifact digest; the signer account nonce is the on-chain replay boundary, and this tool cannot guarantee single execution. Returns ordered allowance-reset, approval, and call steps only; never signs or submits.",
1025
+ inputSchema: PREPARE_SWAP_INPUT_SCHEMA,
1026
+ outputSchema: EXECUTION_ENVELOPE_SCHEMA,
1027
+ }),
1028
+ Object.freeze({
1029
+ name: 'otto_prepare_bridge',
1030
+ title: 'Prepare an Otto-attributed LI.FI cross-chain USDC bridge',
1031
+ description: "Prepare, decode, and verify an unsigned cross-chain native-USDC bridge construction aid for the caller-owned EOA or Safe, over the enumerated v1 Circle-CCTP route set (Ethereum, Polygon, Base, Arbitrum, Avalanche); any other chain, token, or bridge route is refused by name. The envelope's valid_until bounds freshness and is committed to the artifact digest; the signer account nonce is the on-chain replay boundary, and this tool cannot guarantee single execution. Returns ordered allowance-reset, approval, and call steps on the source chain only; never signs or submits.",
1032
+ inputSchema: PREPARE_BRIDGE_INPUT_SCHEMA,
1033
+ outputSchema: BRIDGE_EXECUTION_ENVELOPE_SCHEMA,
1034
+ }),
1035
+ Object.freeze({
1036
+ name: 'otto_prepare_polymarket_order',
1037
+ title: 'Prepare an Otto-attributed Polymarket CLOB limit order',
1038
+ description: "Prepare and verify an unsigned Polymarket CTF Exchange V2 limit order (GTC) for the caller-owned Polygon EOA, with Otto's builder code stamped inside the EIP-712 order struct and re-asserted on the exact digest the signer will sign. The price must be an exact multiple of the market's live minimum tick size and the size at or above its published minimum. The envelope's valid_until bounds freshness and is committed to the artifact digest; the exchange's own order hash is the replay boundary, and this tool cannot guarantee single execution. Returns order args and typed data only; never signs or submits, holds no CLOB credentials, and touches no collateral or allowance.",
1039
+ inputSchema: PREPARE_POLYMARKET_ORDER_INPUT_SCHEMA,
1040
+ outputSchema: POLYMARKET_EXECUTION_ENVELOPE_SCHEMA,
1041
+ }),
1042
+ Object.freeze({
1043
+ name: 'otto_prepare_perp_order',
1044
+ title: 'Prepare an unsigned Hyperliquid perp order L1 action',
1045
+ description: "Prepare and verify an unsigned Hyperliquid perpetuals order (limit, with optional reduce-only take-profit and stop-loss triggers bundled as normalTpsl) for the caller-owned Hyperliquid account, returned as the canonical L1 action plus the exact EIP-712 Agent domain, struct, and connectionId recipe the caller's own signer needs. The asset id, size grid, and price grid come from the live perp universe: an unknown or delisted perp, a size finer than the asset's szDecimals, or a price off the venue's 5-significant-digit / (6 - szDecimals) decimal grid is refused by name rather than rounded to a different order. There is no market order — name your own aggressive limit price with 'Ioc'. Otto's builder attribution is spec'd and valueless today, so the action carries no builder field and the envelope says so. The nonce is chosen by the signer, never here; the envelope's valid_until is committed to the artifact digest AND placed inside the signed digest as expiresAfter, so the venue itself rejects a stale action, while the Hyperliquid signer nonce remains the replay boundary and this tool cannot guarantee single execution. This tool never signs or submits, holds no key, and derives no agent wallet.",
1046
+ inputSchema: PREPARE_PERP_ORDER_INPUT_SCHEMA,
1047
+ outputSchema: HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA,
1048
+ }),
1049
+ ]);
1050
+ export const EXECUTION_TOOL_DEFINITION_BY_NAME = new Map(EXECUTION_TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
1051
+ //# sourceMappingURL=execution-tool-definitions.js.map