@orbs-network/spot-skill 2.4.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.
@@ -0,0 +1,1209 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const MAX_SLIPPAGE = 5000n;
9
+ const DEF_SLIPPAGE = 500n;
10
+ const EXCLUSIVITY = 0;
11
+ const REF_SHARE = 0;
12
+ const FRESHNESS = 30;
13
+ const TTL = 300n;
14
+ const U32_MAX = 4294967295n;
15
+ const MIN_NON_ZERO_EPOCH = 31n;
16
+ const MAX_APPROVAL = (1n << 256n) - 1n;
17
+ const DEF_WATCH_INTERVAL = 5;
18
+ const DEF_WATCH_TIMEOUT = 0;
19
+ const TERMINAL_ORDER_STATUSES = new Set(['filled', 'completed', 'cancelled', 'canceled', 'expired', 'failed', 'rejected']);
20
+ const NOTE_ORACLE = 'Oracle protection applies to all order types and every chunk.';
21
+ const NOTE_EPOCH = 'epoch is the delay between chunks, but it is not exact: one chunk can fill once anywhere inside each epoch window.';
22
+ const NOTE_SIGN = 'Sign typedData with any EIP-712 flow. eth_signTypedData_v4 is only an example.';
23
+ const WARN_LOW_SLIPPAGE = 'slippage below 5% can reduce fill probability. 5% is the default compromise; higher slippage still uses oracle pricing and offchain executors.';
24
+ const WARN_RECIPIENT = 'recipient differs from swapper and is dangerous to change';
25
+
26
+ const SCRIPT_DIR = __dirname;
27
+ const SKILL_ROOT = path.resolve(SCRIPT_DIR, '..');
28
+ const SKILL_MD = path.join(SKILL_ROOT, 'SKILL.md');
29
+ const SKELETON = path.join(SKILL_ROOT, 'assets', 'repermit.skeleton.json');
30
+
31
+ let runtimeConfig = null;
32
+ let skeletonCache = null;
33
+ let ZERO = '';
34
+ let SINK = '';
35
+ let CREATE_URL = '';
36
+ let QUERY_URL = '';
37
+ let REPERMIT = '';
38
+ let REACTOR = '';
39
+ let EXECUTOR = '';
40
+ let SUPPORTED_CHAIN_IDS = '';
41
+ let warnings = [];
42
+
43
+ class CliError extends Error {}
44
+
45
+ function die(message) {
46
+ throw new CliError(message);
47
+ }
48
+
49
+ function lower(value) {
50
+ return String(value).toLowerCase();
51
+ }
52
+
53
+ function trim(value) {
54
+ return String(value ?? '').trim();
55
+ }
56
+
57
+ function warn(message) {
58
+ warnings.push(message);
59
+ process.stderr.write(`warning: ${message}\n`);
60
+ }
61
+
62
+ function note(message) {
63
+ process.stderr.write(`info: ${message}\n`);
64
+ }
65
+
66
+ function firstDefined(...values) {
67
+ for (const value of values) {
68
+ if (value !== undefined && value !== null) {
69
+ return value;
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function isPlainObject(value) {
76
+ return !!value && typeof value === 'object' && !Array.isArray(value);
77
+ }
78
+
79
+ function objectOrEmpty(value) {
80
+ return isPlainObject(value) ? value : {};
81
+ }
82
+
83
+ function decimalString(value, name = 'value') {
84
+ const text = trim(value);
85
+ if (!text) {
86
+ die(`${name} is required`);
87
+ }
88
+ if (/^\d+$/.test(text)) {
89
+ return BigInt(text).toString(10);
90
+ }
91
+ if (/^0[xX][0-9a-fA-F]+$/.test(text)) {
92
+ return BigInt(text).toString(10);
93
+ }
94
+ die(`${name} must be decimal or 0x integer`);
95
+ }
96
+
97
+ function decimalOnly(value, name = 'value') {
98
+ const text = trim(value);
99
+ if (!/^\d+$/.test(text)) {
100
+ die(`${name} must be decimal`);
101
+ }
102
+ return BigInt(text);
103
+ }
104
+
105
+ function compare(a, b) {
106
+ const left = decimalOnly(a);
107
+ const right = decimalOnly(b);
108
+ if (left < right) {
109
+ return -1;
110
+ }
111
+ if (left > right) {
112
+ return 1;
113
+ }
114
+ return 0;
115
+ }
116
+
117
+ function eq(a, b) {
118
+ return compare(a, b) === 0;
119
+ }
120
+
121
+ function gt(a, b) {
122
+ return compare(a, b) === 1;
123
+ }
124
+
125
+ function add(a, b) {
126
+ return (decimalOnly(a) + decimalOnly(b)).toString(10);
127
+ }
128
+
129
+ function subtract(a, b) {
130
+ const left = decimalOnly(a);
131
+ const right = decimalOnly(b);
132
+ if (left < right) {
133
+ die('internal subtraction underflow');
134
+ }
135
+ return (left - right).toString(10);
136
+ }
137
+
138
+ function multiply(a, b) {
139
+ return (decimalOnly(a) * decimalOnly(b)).toString(10);
140
+ }
141
+
142
+ function divideAndRemainder(a, b) {
143
+ const left = decimalOnly(a);
144
+ const right = decimalOnly(b);
145
+ if (right === 0n) {
146
+ die('division by zero');
147
+ }
148
+ return {
149
+ quotient: (left / right).toString(10),
150
+ remainder: (left % right).toString(10),
151
+ };
152
+ }
153
+
154
+ function ensureU32(value, name) {
155
+ if (decimalOnly(value, name) > U32_MAX) {
156
+ die(`${name} must fit in uint32`);
157
+ }
158
+ }
159
+
160
+ function hexBody(value, name, { allowBare = false } = {}) {
161
+ const text = trim(value);
162
+ if (/^0x[0-9a-fA-F]*$/.test(text)) {
163
+ return text.slice(2);
164
+ }
165
+ if (allowBare && /^[0-9a-fA-F]+$/.test(text)) {
166
+ return text;
167
+ }
168
+ die(`${name} must be hex`);
169
+ }
170
+
171
+ function parseAddress(value, name, allowZero = false) {
172
+ const text = `0x${hexBody(value, name)}`;
173
+ if (text.length !== 42) {
174
+ die(`${name} must be a 20-byte 0x address`);
175
+ }
176
+ if (!allowZero && ZERO && lower(text) === lower(ZERO)) {
177
+ die(`${name} cannot be zero`);
178
+ }
179
+ return text;
180
+ }
181
+
182
+ function requireHex(value, name) {
183
+ const raw = hexBody(value, name);
184
+ if (raw.length % 2 !== 0) {
185
+ die(`${name} must be hex`);
186
+ }
187
+ return `0x${raw}`;
188
+ }
189
+
190
+ function formatError(error) {
191
+ if (!error) {
192
+ return 'unknown error';
193
+ }
194
+ const parts = [];
195
+ if (error.message) {
196
+ parts.push(error.message);
197
+ }
198
+ if (error.cause && error.cause.message) {
199
+ parts.push(error.cause.message);
200
+ } else if (error.cause && error.cause.code) {
201
+ parts.push(String(error.cause.code));
202
+ }
203
+ return parts.filter(Boolean).join(': ') || 'unknown error';
204
+ }
205
+
206
+ function normalizeSizedHex(value, name, size) {
207
+ const raw = hexBody(value, name, { allowBare: true });
208
+ if (raw.length !== size) {
209
+ die(`${name} must be ${size} hex chars`);
210
+ }
211
+ return `0x${raw}`;
212
+ }
213
+
214
+ function padHex64(value, name = 'value') {
215
+ const raw = lower(hexBody(value, name, { allowBare: true }));
216
+ if (raw.length > 64) {
217
+ die(`${name} must fit in uint256`);
218
+ }
219
+ return `0x${raw.padStart(64, '0')}`;
220
+ }
221
+
222
+ function parseOptions(args, spec, command) {
223
+ const values = Object.fromEntries(Object.values(spec).map((key) => [key, '']));
224
+ for (let index = 0; index < args.length; index += 1) {
225
+ const arg = args[index];
226
+ const key = spec[arg];
227
+ if (!key) {
228
+ die(`unknown ${command} arg: ${arg}`);
229
+ }
230
+ const value = args[index + 1];
231
+ if (value === undefined) {
232
+ die(`${command} arg requires a value: ${arg}`);
233
+ }
234
+ values[key] = value;
235
+ index += 1;
236
+ }
237
+ return values;
238
+ }
239
+
240
+ function countPresent(...values) {
241
+ return values.reduce((count, value) => count + (value ? 1 : 0), 0);
242
+ }
243
+
244
+ function approveCalldata(spender, amount) {
245
+ const spenderHex = padHex64(parseAddress(spender, 'approve.spender'), 'approve.spender');
246
+ const amountHex = padHex64(decimalOnly(amount, 'approve.amount').toString(16), 'approve.amount');
247
+ return `0x095ea7b3${spenderHex.slice(2)}${amountHex.slice(2)}`;
248
+ }
249
+
250
+ function normalizeSigV(value, name = 'signature.v') {
251
+ const raw = trim(value);
252
+ if (!raw) {
253
+ die(`${name} is required`);
254
+ }
255
+
256
+ let decimal;
257
+ if (/^0[xX][0-9a-fA-F]+$/.test(raw)) {
258
+ decimal = BigInt(raw);
259
+ } else if (/^\d+$/.test(raw)) {
260
+ decimal = BigInt(raw);
261
+ } else if (/^[0-9a-fA-F]+$/.test(raw)) {
262
+ decimal = BigInt(`0x${raw}`);
263
+ } else {
264
+ die(`${name} must be 0, 1, 27, 28, or equivalent hex`);
265
+ }
266
+
267
+ switch (decimal.toString(10)) {
268
+ case '0':
269
+ case '27':
270
+ return '0x1b';
271
+ case '1':
272
+ case '28':
273
+ return '0x1c';
274
+ default:
275
+ die(`${name} must be 0, 1, 27, 28, or equivalent hex`);
276
+ }
277
+ }
278
+
279
+ function now() {
280
+ return Math.floor(Date.now() / 1000).toString(10);
281
+ }
282
+
283
+ function iso() {
284
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
285
+ }
286
+
287
+ function readSource(src, name) {
288
+ const source = trim(src);
289
+ if (!source) {
290
+ die(`${name} is required`);
291
+ }
292
+ if (source === '-') {
293
+ return fs.readFileSync(0, 'utf8');
294
+ }
295
+ try {
296
+ return fs.readFileSync(source, 'utf8');
297
+ } catch (error) {
298
+ if (error && error.code === 'ENOENT') {
299
+ die(`${name} not found: ${source}`);
300
+ }
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ function parseJson(text, name) {
306
+ try {
307
+ return JSON.parse(text);
308
+ } catch {
309
+ die(`${name} must be valid JSON`);
310
+ }
311
+ }
312
+
313
+ function readJsonSource(src, name) {
314
+ return parseJson(readSource(src, name), name);
315
+ }
316
+
317
+ function parseSkillConfig(markdown) {
318
+ const match = markdown.match(/## Config\n\n```json\n([\s\S]*?)\n```/);
319
+ if (!match) {
320
+ die(`skill config JSON block not found: ${SKILL_MD}`);
321
+ }
322
+ return parseJson(match[1], 'skill config');
323
+ }
324
+
325
+ function jsonOrText(text) {
326
+ if (text === '') {
327
+ return '';
328
+ }
329
+ try {
330
+ return JSON.parse(text);
331
+ } catch {
332
+ return text;
333
+ }
334
+ }
335
+
336
+ function writeOutput(value, outFile) {
337
+ const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
338
+ if (outFile) {
339
+ fs.writeFileSync(outFile, `${text}\n`);
340
+ return;
341
+ }
342
+ process.stdout.write(`${text}\n`);
343
+ }
344
+
345
+ function loadRuntimeConfig() {
346
+ if (runtimeConfig) {
347
+ return runtimeConfig;
348
+ }
349
+
350
+ let skillConfig;
351
+ try {
352
+ skillConfig = parseSkillConfig(fs.readFileSync(SKILL_MD, 'utf8'));
353
+ } catch (error) {
354
+ if (error instanceof CliError) {
355
+ throw error;
356
+ }
357
+ if (error && error.code === 'ENOENT') {
358
+ die(`skill metadata not found: ${SKILL_MD}`);
359
+ }
360
+ throw error;
361
+ }
362
+
363
+ const runtime = skillConfig && typeof skillConfig === 'object' ? skillConfig.runtime : null;
364
+ const contracts = runtime && typeof runtime === 'object' ? runtime.contracts : null;
365
+ const chains = runtime && typeof runtime === 'object' ? runtime.chains : null;
366
+ const url = runtime && typeof runtime.url === 'string' ? runtime.url : '';
367
+ const zero = contracts && typeof contracts.zero === 'string' ? contracts.zero : '';
368
+ const repermit = contracts && typeof contracts.repermit === 'string' ? contracts.repermit : '';
369
+ const reactor = contracts && typeof contracts.reactor === 'string' ? contracts.reactor : '';
370
+ const executor = contracts && typeof contracts.executor === 'string' ? contracts.executor : '';
371
+
372
+ const invalidRuntime =
373
+ !url ||
374
+ !zero ||
375
+ !repermit ||
376
+ !reactor ||
377
+ !executor ||
378
+ !chains ||
379
+ typeof chains !== 'object' ||
380
+ Array.isArray(chains) ||
381
+ Object.keys(chains).length === 0;
382
+
383
+ if (invalidRuntime) {
384
+ die(`invalid skill runtime config in ${SKILL_MD}`);
385
+ }
386
+
387
+ ZERO = parseAddress(zero, 'runtime.contracts.zero', true);
388
+ REPERMIT = parseAddress(repermit, 'runtime.contracts.repermit');
389
+ REACTOR = parseAddress(reactor, 'runtime.contracts.reactor');
390
+ EXECUTOR = parseAddress(executor, 'runtime.contracts.executor');
391
+ SINK = url;
392
+
393
+ if (!/^https?:\/\//.test(SINK)) {
394
+ die('runtime.url must be http(s)');
395
+ }
396
+
397
+ const supported = Object.keys(chains)
398
+ .map((chainId) => {
399
+ const parsed = Number(chainId);
400
+ if (!Number.isInteger(parsed)) {
401
+ die(`invalid skill runtime config in ${SKILL_MD}`);
402
+ }
403
+ return parsed;
404
+ })
405
+ .sort((left, right) => left - right)
406
+ .map((value) => String(value));
407
+
408
+ SUPPORTED_CHAIN_IDS = supported.join(', ');
409
+ if (!SUPPORTED_CHAIN_IDS) {
410
+ die(`skill runtime has no supported chains in ${SKILL_MD}`);
411
+ }
412
+
413
+ CREATE_URL = `${SINK}/orders/new`;
414
+ QUERY_URL = `${SINK}/orders`;
415
+ runtimeConfig = { chains };
416
+ return runtimeConfig;
417
+ }
418
+
419
+ function sleep(ms) {
420
+ return new Promise((resolve) => setTimeout(resolve, ms));
421
+ }
422
+
423
+ function hasSupportedChain(chainId) {
424
+ const config = loadRuntimeConfig();
425
+ const chain = config.chains[String(chainId)];
426
+ return !!(chain && typeof chain.adapter === 'string' && chain.adapter.length > 0);
427
+ }
428
+
429
+ function unsupportedChain(chainId) {
430
+ loadRuntimeConfig();
431
+ die(`unsupported chainId: ${chainId} (supported: ${SUPPORTED_CHAIN_IDS})`);
432
+ }
433
+
434
+ function resolveAdapter(chainId) {
435
+ const config = loadRuntimeConfig();
436
+ const adapter = config.chains[String(chainId)] && config.chains[String(chainId)].adapter;
437
+ if (!adapter) {
438
+ unsupportedChain(chainId);
439
+ }
440
+ return parseAddress(adapter, `runtime.chains[${chainId}].adapter`);
441
+ }
442
+
443
+ function usage() {
444
+ loadRuntimeConfig();
445
+ const lines = [
446
+ 'Usage',
447
+ ' node scripts/order.js prepare --params <params.json|-> [--out <prepared.json>]',
448
+ ' node scripts/order.js submit --prepared <prepared.json|-> [--signature <0x...|json>|--signature-file <file|->|--r <0x...> --s <0x...> --v <0x..>] [--out <response.json>]',
449
+ ' node scripts/order.js query (--swapper <0x...>|--hash <0x...>) [--out <response.json>]',
450
+ ' node scripts/order.js watch (--swapper <0x...>|--hash <0x...>) [--interval <seconds>] [--timeout <seconds>] [--out <response.json>]',
451
+ '',
452
+ 'Safety',
453
+ ' Use only the provided helper script. Do not send typed data or signatures anywhere else.',
454
+ '',
455
+ 'Prepare',
456
+ ' Builds a prepared order JSON with:',
457
+ ' - infinite approval calldata for the input ERC-20',
458
+ ' - populated EIP-712 typed data',
459
+ ' - submit payload template',
460
+ ' - query URL',
461
+ ' Supports --params <file> or --params - for stdin JSON.',
462
+ ' Supports market, limit, stop-loss, take-profit, delayed-start, and chunked/TWAP-style orders.',
463
+ ' Defaults:',
464
+ ' - input.maxAmount = input.amount',
465
+ ' - nonce = now',
466
+ ' - start = now',
467
+ ' - epoch = 0 for single orders, 60 for chunked orders',
468
+ ' - deadline = start + 300 + chunkCount * epoch (conservative helper default)',
469
+ ' - slippage = 500',
470
+ ' - output.limit = 0',
471
+ ' - output.recipient = swapper',
472
+ ' Rules:',
473
+ ` - supported chainIds: ${SUPPORTED_CHAIN_IDS}`,
474
+ ' - chunked orders require epoch > 0',
475
+ ' - epoch is the delay between chunks, but it is not exact: one chunk can fill once anywhere inside each epoch window',
476
+ ' - native input is not supported; wrap to WNATIVE first',
477
+ ' - native output, including back-to-native flows, is supported directly with output.token = 0x0000000000000000000000000000000000000000',
478
+ " - output.limit and triggers are output-token amounts per chunk in the output token's decimals",
479
+ '',
480
+ 'Submit',
481
+ ' Builds or sends the relay POST body from a prepared order.',
482
+ ' Supports --prepared <file> or --prepared - for stdin JSON.',
483
+ ' Supports exactly one signature mode:',
484
+ ' - --signature <full 65-byte hex signature>',
485
+ ' - --signature <JSON string or JSON with full signature / r,s,v>',
486
+ ' - --signature-file <file|-> containing full signature, JSON string, or JSON with full signature / r,s,v',
487
+ ' - --r <0x...> --s <0x...> --v <0x..>',
488
+ " All signature inputs are normalized to the relay's r/s/v object format.",
489
+ '',
490
+ 'Query',
491
+ ' Builds or sends the relay GET request.',
492
+ ' Supports only:',
493
+ ' - --swapper <0x...>',
494
+ ' - --hash <0x...>',
495
+ '',
496
+ 'Watch',
497
+ ' Polls the relay query endpoint until the order reaches a terminal status.',
498
+ ' Retries transient network errors automatically.',
499
+ ' Defaults:',
500
+ ` - interval = ${String(DEF_WATCH_INTERVAL)} seconds`,
501
+ ` - timeout = ${String(DEF_WATCH_TIMEOUT)} seconds (0 = no timeout)`,
502
+ ' Supports only:',
503
+ ' - --swapper <0x...> when exactly one order matches',
504
+ ' - --hash <0x...> (recommended)',
505
+ ];
506
+ process.stdout.write(`${lines.join('\n')}\n`);
507
+ }
508
+
509
+ function loadSkeleton() {
510
+ if (!skeletonCache) {
511
+ skeletonCache = parseJson(fs.readFileSync(SKELETON, 'utf8'), 'typed data skeleton');
512
+ }
513
+ return JSON.parse(JSON.stringify(skeletonCache));
514
+ }
515
+
516
+ function buildTypedData(
517
+ chainId,
518
+ swapper,
519
+ nonce,
520
+ start,
521
+ deadline,
522
+ epoch,
523
+ slippage,
524
+ inputToken,
525
+ inputAmount,
526
+ inputMaxAmount,
527
+ outputToken,
528
+ outputLimit,
529
+ outputTriggerLower,
530
+ outputTriggerUpper,
531
+ outputRecipient,
532
+ ) {
533
+ const typedData = loadSkeleton();
534
+ typedData.domain.chainId = Number(chainId);
535
+ typedData.domain.verifyingContract = REPERMIT;
536
+ typedData.message.permitted.token = inputToken;
537
+ typedData.message.permitted.amount = inputMaxAmount;
538
+ typedData.message.spender = REACTOR;
539
+ typedData.message.nonce = nonce;
540
+ typedData.message.deadline = deadline;
541
+ typedData.message.witness.reactor = REACTOR;
542
+ typedData.message.witness.executor = EXECUTOR;
543
+ typedData.message.witness.exchange.adapter = resolveAdapter(chainId);
544
+ typedData.message.witness.exchange.ref = ZERO;
545
+ typedData.message.witness.exchange.share = REF_SHARE;
546
+ typedData.message.witness.exchange.data = '0x';
547
+ typedData.message.witness.swapper = swapper;
548
+ typedData.message.witness.nonce = nonce;
549
+ typedData.message.witness.start = start;
550
+ typedData.message.witness.deadline = deadline;
551
+ typedData.message.witness.chainid = Number(chainId);
552
+ typedData.message.witness.exclusivity = EXCLUSIVITY;
553
+ typedData.message.witness.epoch = Number(epoch);
554
+ typedData.message.witness.slippage = Number(slippage);
555
+ typedData.message.witness.freshness = FRESHNESS;
556
+ typedData.message.witness.input.token = inputToken;
557
+ typedData.message.witness.input.amount = inputAmount;
558
+ typedData.message.witness.input.maxAmount = inputMaxAmount;
559
+ typedData.message.witness.output.token = outputToken;
560
+ typedData.message.witness.output.limit = outputLimit;
561
+ typedData.message.witness.output.triggerLower = outputTriggerLower;
562
+ typedData.message.witness.output.triggerUpper = outputTriggerUpper;
563
+ typedData.message.witness.output.recipient = outputRecipient;
564
+ return typedData;
565
+ }
566
+
567
+ function signatureFieldsFromObject(parsed) {
568
+ if (typeof parsed.signature === 'string') {
569
+ return { payload: parsed.signature };
570
+ }
571
+ if (typeof parsed.full === 'string') {
572
+ return { payload: parsed.full };
573
+ }
574
+ const source = isPlainObject(parsed.signature) ? parsed.signature : parsed;
575
+ const r = source.r ?? '';
576
+ const s = source.s ?? '';
577
+ const v = source.v ?? '';
578
+ if (!r || !s || !v) {
579
+ die('signature JSON must contain a full signature string or r, s, v');
580
+ }
581
+ return { r, s, v, kind: 'rsv' };
582
+ }
583
+
584
+ function normalizeSignature(payloadInput) {
585
+ let payload = trim(payloadInput);
586
+ let r = '';
587
+ let s = '';
588
+ let v = '';
589
+ let full = '';
590
+ let kind = '';
591
+
592
+ if (!payload) {
593
+ die('signature input is empty');
594
+ }
595
+
596
+ try {
597
+ const parsed = JSON.parse(payload);
598
+ if (typeof parsed === 'string') {
599
+ payload = parsed;
600
+ } else if (isPlainObject(parsed)) {
601
+ ({ payload = payload, r = '', s = '', v = '', kind = '' } = signatureFieldsFromObject(parsed));
602
+ } else {
603
+ die('signature JSON must contain a full signature string or r, s, v');
604
+ }
605
+ } catch (error) {
606
+ if (error instanceof CliError) {
607
+ throw error;
608
+ }
609
+ }
610
+
611
+ if (r || s || v) {
612
+ r = normalizeSizedHex(r, 'signature.r', 64);
613
+ s = normalizeSizedHex(s, 'signature.s', 64);
614
+ v = normalizeSigV(v, 'signature.v');
615
+ full = `${r}${s.slice(2)}${v.slice(2)}`;
616
+ } else {
617
+ let signature = trim(payload);
618
+ if (!/^(?:0x)?[0-9a-fA-F]{130}$/.test(signature)) {
619
+ die('signature must be full hex, a JSON string, or r/s/v JSON');
620
+ }
621
+ if (!signature.startsWith('0x')) {
622
+ signature = `0x${signature}`;
623
+ }
624
+ full = signature;
625
+ r = `0x${signature.slice(2, 66)}`;
626
+ s = `0x${signature.slice(66, 130)}`;
627
+ v = normalizeSigV(`0x${signature.slice(130, 132)}`, 'signature.v');
628
+ if (!kind) {
629
+ kind = 'full';
630
+ }
631
+ }
632
+
633
+ return {
634
+ kind,
635
+ full,
636
+ signature: { r, s, v },
637
+ };
638
+ }
639
+
640
+ function prepare(args) {
641
+ loadRuntimeConfig();
642
+
643
+ warnings = [];
644
+
645
+ const { paramsSource, outFile } = parseOptions(
646
+ args,
647
+ { '--params': 'paramsSource', '--out': 'outFile' },
648
+ 'prepare',
649
+ );
650
+
651
+ const params = readJsonSource(paramsSource, 'params');
652
+ const input = objectOrEmpty(params.input);
653
+ const output = objectOrEmpty(params.output);
654
+ const nowTs = now();
655
+ const chainId = decimalString(firstDefined(params.chainId, params.chainID), 'chainId');
656
+ if (!hasSupportedChain(chainId)) {
657
+ unsupportedChain(chainId);
658
+ }
659
+
660
+ const swapper = parseAddress(firstDefined(params.swapper, params.account, params.signer), 'swapper');
661
+ const nonce = decimalString(firstDefined(params.nonce, nowTs), 'nonce');
662
+ const start = decimalString(firstDefined(params.start, nowTs), 'start');
663
+ const slippage = decimalString(firstDefined(params.slippage, DEF_SLIPPAGE.toString(10)), 'slippage');
664
+ const inputToken = parseAddress(firstDefined(input.token, params.inputToken), 'input.token');
665
+ const inputAmount = decimalString(firstDefined(input.amount, params.inputAmount), 'input.amount');
666
+ let inputMaxAmount = decimalString(
667
+ firstDefined(input.maxAmount, params.inputMaxAmount, inputAmount),
668
+ 'input.maxAmount',
669
+ );
670
+ const outputToken = parseAddress(firstDefined(output.token, params.outputToken), 'output.token', true);
671
+ const outputLimit = decimalString(firstDefined(output.limit, params.outputLimit, '0'), 'output.limit');
672
+ const outputTriggerLower = decimalString(
673
+ firstDefined(output.triggerLower, params.outputTriggerLower, '0'),
674
+ 'output.triggerLower',
675
+ );
676
+ const outputTriggerUpper = decimalString(
677
+ firstDefined(output.triggerUpper, params.outputTriggerUpper, '0'),
678
+ 'output.triggerUpper',
679
+ );
680
+ const recipient = parseAddress(firstDefined(output.recipient, params.recipient, swapper), 'output.recipient');
681
+
682
+ let epoch;
683
+ if (params.epoch !== undefined && params.epoch !== null) {
684
+ epoch = decimalString(params.epoch, 'epoch');
685
+ } else {
686
+ epoch = eq(inputAmount, inputMaxAmount) ? '0' : '60';
687
+ }
688
+
689
+ ensureU32(epoch, 'epoch');
690
+ ensureU32(slippage, 'slippage');
691
+
692
+ if (eq(start, '0')) {
693
+ die('start must be non-zero');
694
+ }
695
+ if (eq(inputAmount, '0')) {
696
+ die('input.amount must be non-zero');
697
+ }
698
+ if (gt(inputAmount, inputMaxAmount)) {
699
+ die('input.amount cannot exceed input.maxAmount');
700
+ }
701
+ if (lower(inputToken) === lower(outputToken)) {
702
+ die('input.token and output.token must differ');
703
+ }
704
+ if (!eq(outputTriggerUpper, '0') && gt(outputTriggerLower, outputTriggerUpper)) {
705
+ die('output.triggerLower cannot exceed output.triggerUpper');
706
+ }
707
+ if (gt(slippage, MAX_SLIPPAGE.toString(10))) {
708
+ die(`slippage cannot exceed ${MAX_SLIPPAGE.toString(10)}`);
709
+ }
710
+ if (!eq(epoch, '0') && compare(epoch, MIN_NON_ZERO_EPOCH.toString()) === -1) {
711
+ die(`non-zero epoch must be >= ${MIN_NON_ZERO_EPOCH.toString()} because helper freshness is ${String(FRESHNESS)}`);
712
+ }
713
+ if (!eq(epoch, '0') && compare(String(FRESHNESS), epoch) !== -1) {
714
+ die('freshness must be < epoch when epoch != 0');
715
+ }
716
+
717
+ const requestedInputMaxAmount = inputMaxAmount;
718
+ const chunking = divideAndRemainder(inputMaxAmount, inputAmount);
719
+ const chunkCount = chunking.quotient;
720
+ const remainder = chunking.remainder;
721
+
722
+ if (!eq(remainder, '0')) {
723
+ inputMaxAmount = subtract(inputMaxAmount, remainder);
724
+ warn(
725
+ `input.maxAmount is not divisible by input.amount; rounding down from ${requestedInputMaxAmount} to ${inputMaxAmount} to keep fixed chunk sizes`,
726
+ );
727
+ }
728
+
729
+ if (!eq(inputAmount, inputMaxAmount) && eq(epoch, '0')) {
730
+ die(`chunked orders require epoch >= ${MIN_NON_ZERO_EPOCH.toString()}`);
731
+ }
732
+
733
+ const kind = eq(inputAmount, inputMaxAmount) ? 'single' : 'chunked';
734
+ let deadline;
735
+ if (params.deadline !== undefined && params.deadline !== null) {
736
+ deadline = decimalString(params.deadline, 'deadline');
737
+ } else {
738
+ deadline = add(start, TTL.toString(10));
739
+ if (gt(epoch, '0')) {
740
+ deadline = add(deadline, multiply(chunkCount, epoch));
741
+ }
742
+ }
743
+
744
+ if (gt(start, nowTs)) {
745
+ if (!gt(deadline, start)) {
746
+ die('deadline must be after start');
747
+ }
748
+ } else if (!gt(deadline, nowTs)) {
749
+ die('deadline must be after current time');
750
+ }
751
+
752
+ if (compare(slippage, DEF_SLIPPAGE.toString(10)) === -1) {
753
+ warn(WARN_LOW_SLIPPAGE);
754
+ }
755
+ if (lower(recipient) !== lower(swapper)) {
756
+ warn(WARN_RECIPIENT);
757
+ }
758
+
759
+ const approvalAmount = MAX_APPROVAL.toString(10);
760
+ const approvalData = requireHex(approveCalldata(REPERMIT, approvalAmount), 'approval.tx.data');
761
+ const typedData = buildTypedData(
762
+ chainId,
763
+ swapper,
764
+ nonce,
765
+ start,
766
+ deadline,
767
+ epoch,
768
+ slippage,
769
+ inputToken,
770
+ inputAmount,
771
+ inputMaxAmount,
772
+ outputToken,
773
+ outputLimit,
774
+ outputTriggerLower,
775
+ outputTriggerUpper,
776
+ recipient,
777
+ );
778
+
779
+ const prepared = {
780
+ meta: {
781
+ preparedAt: iso(),
782
+ kind,
783
+ chunkCount,
784
+ chunkInputAmount: inputAmount,
785
+ start,
786
+ deadline,
787
+ epoch,
788
+ epochScheduling: NOTE_EPOCH,
789
+ limit: outputLimit,
790
+ oracleProtection: NOTE_ORACLE,
791
+ },
792
+ warnings,
793
+ approval: {
794
+ token: inputToken,
795
+ spender: REPERMIT,
796
+ amount: approvalAmount,
797
+ tx: {
798
+ to: inputToken,
799
+ data: approvalData,
800
+ value: '0x0',
801
+ },
802
+ },
803
+ typedData,
804
+ signing: {
805
+ signer: swapper,
806
+ note: NOTE_SIGN,
807
+ },
808
+ submit: {
809
+ url: CREATE_URL,
810
+ body: {
811
+ order: typedData.message,
812
+ signature: {
813
+ r: null,
814
+ s: null,
815
+ v: null,
816
+ },
817
+ status: 'pending',
818
+ },
819
+ },
820
+ query: {
821
+ url: QUERY_URL,
822
+ },
823
+ };
824
+
825
+ writeOutput(prepared, outFile);
826
+ return 0;
827
+ }
828
+
829
+ function selectOrderPayload(prepared) {
830
+ if (prepared && prepared.submit && prepared.submit.body && prepared.submit.body.order) {
831
+ return prepared.submit.body.order;
832
+ }
833
+ if (prepared && prepared.typedData && prepared.typedData.message) {
834
+ return prepared.typedData.message;
835
+ }
836
+ if (prepared && prepared.domain && prepared.types && prepared.message) {
837
+ return prepared.message;
838
+ }
839
+ die('missing order payload');
840
+ }
841
+
842
+ async function requestJson(url, options) {
843
+ let response;
844
+ try {
845
+ response = await fetch(url, options);
846
+ } catch (error) {
847
+ die(`request failed: ${formatError(error)}`);
848
+ }
849
+ const text = await response.text();
850
+ return {
851
+ ok: response.ok,
852
+ status: response.status,
853
+ response: jsonOrText(text),
854
+ };
855
+ }
856
+
857
+ function secondsOption(value, name, fallback) {
858
+ const selected = trim(value) ? value : fallback;
859
+ const parsed = decimalString(selected, name);
860
+ ensureU32(parsed, name);
861
+ return Number(parsed);
862
+ }
863
+
864
+ function buildQueryUrl(rawSwapper, rawHash) {
865
+ let swapper = trim(rawSwapper);
866
+ let hash = trim(rawHash);
867
+
868
+ if (!swapper && !hash) {
869
+ die('query needs --swapper or --hash');
870
+ }
871
+
872
+ let url = QUERY_URL;
873
+ if (swapper) {
874
+ swapper = parseAddress(swapper, 'swapper');
875
+ url = `${url}?swapper=${encodeURIComponent(swapper)}`;
876
+ }
877
+ if (hash) {
878
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hash)) {
879
+ die('hash must be 32-byte 0x hex');
880
+ }
881
+ url = url.includes('?') ? `${url}&hash=${encodeURIComponent(hash)}` : `${url}?hash=${encodeURIComponent(hash)}`;
882
+ }
883
+
884
+ return { swapper, hash, url };
885
+ }
886
+
887
+ function responseOrders(response) {
888
+ return isPlainObject(response) && Array.isArray(response.orders) ? response.orders : [];
889
+ }
890
+
891
+ function responseOrderHash(response) {
892
+ if (!isPlainObject(response)) {
893
+ return '';
894
+ }
895
+
896
+ const direct = trim(response.orderHash);
897
+ if (/^0x[0-9a-fA-F]{64}$/.test(direct)) {
898
+ return direct;
899
+ }
900
+
901
+ const signedOrder = objectOrEmpty(response.signedOrder);
902
+ const nested = trim(signedOrder.hash);
903
+ if (/^0x[0-9a-fA-F]{64}$/.test(nested)) {
904
+ return nested;
905
+ }
906
+
907
+ return '';
908
+ }
909
+
910
+ function submitOutput(result, request) {
911
+ const orderHash = result.ok ? responseOrderHash(result.response) : '';
912
+ const watch = orderHash
913
+ ? {
914
+ hash: orderHash,
915
+ command: `node scripts/order.js watch --hash ${orderHash}`,
916
+ url: `${QUERY_URL}?hash=${encodeURIComponent(orderHash)}`,
917
+ }
918
+ : null;
919
+
920
+ return {
921
+ ok: result.ok,
922
+ status: result.status,
923
+ url: request.url,
924
+ request,
925
+ response: result.response,
926
+ orderHash,
927
+ watch,
928
+ };
929
+ }
930
+
931
+ function watchSnapshot(response, { hash }) {
932
+ const orders = responseOrders(response);
933
+ if (!hash && orders.length > 1) {
934
+ die('watch with --swapper requires exactly one matching order; use --hash to disambiguate');
935
+ }
936
+
937
+ const order = isPlainObject(orders[0]) ? orders[0] : null;
938
+ const metadata = objectOrEmpty(order && order.metadata);
939
+ const status = trim(firstDefined(metadata.status, order && order.status));
940
+ const chunkStatuses = Array.isArray(metadata.chunks)
941
+ ? metadata.chunks.map((chunk) => trim(chunk && chunk.status)).filter(Boolean)
942
+ : [];
943
+
944
+ return {
945
+ count: orders.length,
946
+ status,
947
+ chunkStatuses,
948
+ };
949
+ }
950
+
951
+ function watchOutput(result, url, watchMeta) {
952
+ return {
953
+ ok: result.ok,
954
+ status: result.status,
955
+ url,
956
+ response: result.response,
957
+ watch: watchMeta,
958
+ };
959
+ }
960
+
961
+ function buildWatchMeta({
962
+ polls,
963
+ queryErrors,
964
+ intervalSeconds,
965
+ timeoutSeconds,
966
+ startedAt,
967
+ finalStatus,
968
+ chunkStatuses,
969
+ timedOut,
970
+ lastError,
971
+ }) {
972
+ return {
973
+ command: 'watch',
974
+ polls,
975
+ queryErrors,
976
+ intervalSeconds,
977
+ timeoutSeconds,
978
+ elapsedSeconds: Math.floor((Date.now() - startedAt) / 1000),
979
+ finalStatus,
980
+ chunkStatuses,
981
+ timedOut,
982
+ lastError,
983
+ };
984
+ }
985
+
986
+ async function submit(args) {
987
+ loadRuntimeConfig();
988
+
989
+ const { preparedSource, signatureInput, signatureFile, r, s, v, outFile } = parseOptions(
990
+ args,
991
+ {
992
+ '--prepared': 'preparedSource',
993
+ '--signature': 'signatureInput',
994
+ '--signature-file': 'signatureFile',
995
+ '--r': 'r',
996
+ '--s': 's',
997
+ '--v': 'v',
998
+ '--out': 'outFile',
999
+ },
1000
+ 'submit',
1001
+ );
1002
+
1003
+ if (preparedSource === '-' && signatureFile === '-') {
1004
+ die('submit supports only one stdin source');
1005
+ }
1006
+
1007
+ const prepared = readJsonSource(preparedSource, 'prepared');
1008
+ if (countPresent(signatureInput, signatureFile, r || s || v) !== 1) {
1009
+ die('submit needs exactly one of --signature, --signature-file, or --r/--s/--v');
1010
+ }
1011
+
1012
+ let normalizedSignature;
1013
+ if (signatureFile) {
1014
+ normalizedSignature = normalizeSignature(readSource(signatureFile, 'signature-file'));
1015
+ } else if (signatureInput) {
1016
+ normalizedSignature = normalizeSignature(signatureInput);
1017
+ } else {
1018
+ if (!r || !s || !v) {
1019
+ die('--r --s --v must be used together');
1020
+ }
1021
+ normalizedSignature = normalizeSignature(JSON.stringify({ r, s, v }));
1022
+ }
1023
+
1024
+ const request = {
1025
+ url: (prepared.submit && prepared.submit.url) || CREATE_URL,
1026
+ body: {
1027
+ order: selectOrderPayload(prepared),
1028
+ signature: normalizedSignature.signature,
1029
+ status: (prepared.submit && prepared.submit.body && prepared.submit.body.status) || 'pending',
1030
+ },
1031
+ signatureInput: normalizedSignature.kind,
1032
+ };
1033
+
1034
+ const result = await requestJson(request.url, {
1035
+ method: 'POST',
1036
+ headers: { 'content-type': 'application/json' },
1037
+ body: JSON.stringify(request.body),
1038
+ });
1039
+
1040
+ writeOutput(submitOutput(result, request), outFile);
1041
+
1042
+ return result.ok ? 0 : 1;
1043
+ }
1044
+
1045
+ async function query(args) {
1046
+ loadRuntimeConfig();
1047
+
1048
+ const { swapper: rawSwapper, hash: rawHash, outFile } = parseOptions(
1049
+ args,
1050
+ { '--swapper': 'swapper', '--hash': 'hash', '--out': 'outFile' },
1051
+ 'query',
1052
+ );
1053
+ const { url } = buildQueryUrl(rawSwapper, rawHash);
1054
+
1055
+ const result = await requestJson(url, { method: 'GET' });
1056
+ writeOutput(
1057
+ {
1058
+ ok: result.ok,
1059
+ status: result.status,
1060
+ url,
1061
+ response: result.response,
1062
+ },
1063
+ outFile,
1064
+ );
1065
+
1066
+ return result.ok ? 0 : 1;
1067
+ }
1068
+
1069
+ async function watchOrder(args) {
1070
+ loadRuntimeConfig();
1071
+ const watchCommand = 'watch';
1072
+
1073
+ const { swapper: rawSwapper, hash: rawHash, interval, timeout, outFile } = parseOptions(
1074
+ args,
1075
+ {
1076
+ '--swapper': 'swapper',
1077
+ '--hash': 'hash',
1078
+ '--interval': 'interval',
1079
+ '--timeout': 'timeout',
1080
+ '--out': 'outFile',
1081
+ },
1082
+ watchCommand,
1083
+ );
1084
+
1085
+ const intervalSeconds = secondsOption(interval, 'interval', String(DEF_WATCH_INTERVAL));
1086
+ const timeoutSeconds = secondsOption(timeout, 'timeout', String(DEF_WATCH_TIMEOUT));
1087
+ const { hash, url } = buildQueryUrl(rawSwapper, rawHash);
1088
+ const startedAt = Date.now();
1089
+ let polls = 0;
1090
+ let queryErrors = 0;
1091
+ let lastResult = {
1092
+ ok: false,
1093
+ status: 0,
1094
+ response: null,
1095
+ };
1096
+ let lastError = '';
1097
+
1098
+ while (true) {
1099
+ polls += 1;
1100
+ try {
1101
+ lastResult = await requestJson(url, { method: 'GET' });
1102
+ lastError = '';
1103
+ } catch (error) {
1104
+ queryErrors += 1;
1105
+ lastError = formatError(error);
1106
+ note(`${watchCommand} retry ${String(queryErrors)} after ${lastError}`);
1107
+ if (timeoutSeconds !== 0 && Date.now() - startedAt >= timeoutSeconds * 1000) {
1108
+ const waitMeta = buildWatchMeta({
1109
+ polls,
1110
+ queryErrors,
1111
+ intervalSeconds,
1112
+ timeoutSeconds,
1113
+ startedAt,
1114
+ finalStatus: '',
1115
+ chunkStatuses: [],
1116
+ timedOut: true,
1117
+ lastError,
1118
+ });
1119
+ writeOutput(watchOutput(lastResult, url, waitMeta), outFile);
1120
+ return 1;
1121
+ }
1122
+ await sleep(intervalSeconds * 1000);
1123
+ continue;
1124
+ }
1125
+
1126
+ if (!lastResult.ok) {
1127
+ queryErrors += 1;
1128
+ lastError = `query returned HTTP ${String(lastResult.status)}`;
1129
+ note(`${watchCommand} retry ${String(queryErrors)} after ${lastError}`);
1130
+ } else {
1131
+ const snapshot = watchSnapshot(lastResult.response, { hash });
1132
+ const finalStatus = snapshot.status || '';
1133
+ const chunkStatuses = snapshot.chunkStatuses;
1134
+ note(`watch status=${finalStatus || 'pending'} chunks=${chunkStatuses.join(',') || '-'}`);
1135
+
1136
+ if (TERMINAL_ORDER_STATUSES.has(lower(finalStatus))) {
1137
+ const waitMeta = buildWatchMeta({
1138
+ polls,
1139
+ queryErrors,
1140
+ intervalSeconds,
1141
+ timeoutSeconds,
1142
+ startedAt,
1143
+ finalStatus,
1144
+ chunkStatuses,
1145
+ timedOut: false,
1146
+ lastError,
1147
+ });
1148
+ writeOutput(watchOutput(lastResult, url, waitMeta), outFile);
1149
+ return 0;
1150
+ }
1151
+ }
1152
+
1153
+ if (timeoutSeconds !== 0 && Date.now() - startedAt >= timeoutSeconds * 1000) {
1154
+ const snapshot = lastResult.ok ? watchSnapshot(lastResult.response, { hash }) : { status: '', chunkStatuses: [] };
1155
+ const waitMeta = buildWatchMeta({
1156
+ polls,
1157
+ queryErrors,
1158
+ intervalSeconds,
1159
+ timeoutSeconds,
1160
+ startedAt,
1161
+ finalStatus: snapshot.status,
1162
+ chunkStatuses: snapshot.chunkStatuses,
1163
+ timedOut: true,
1164
+ lastError,
1165
+ });
1166
+ writeOutput(watchOutput(lastResult, url, waitMeta), outFile);
1167
+ return 1;
1168
+ }
1169
+
1170
+ await sleep(intervalSeconds * 1000);
1171
+ }
1172
+ }
1173
+
1174
+ async function run() {
1175
+ const args = process.argv.slice(2);
1176
+ const command = args[0] || '';
1177
+
1178
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
1179
+ usage();
1180
+ return command ? 0 : 1;
1181
+ }
1182
+
1183
+ switch (command) {
1184
+ case 'prepare':
1185
+ return prepare(args.slice(1));
1186
+ case 'submit':
1187
+ return submit(args.slice(1));
1188
+ case 'query':
1189
+ return query(args.slice(1));
1190
+ case 'watch':
1191
+ return watchOrder(args.slice(1));
1192
+ default:
1193
+ usage();
1194
+ return 1;
1195
+ }
1196
+ }
1197
+
1198
+ run()
1199
+ .then((code) => {
1200
+ process.exitCode = code;
1201
+ })
1202
+ .catch((error) => {
1203
+ if (error instanceof CliError) {
1204
+ process.stderr.write(`error: ${error.message}\n`);
1205
+ process.exitCode = 1;
1206
+ return;
1207
+ }
1208
+ throw error;
1209
+ });