@deeeed/metamask-harness 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +12 -3
- package/dist/commands/call.js +2 -9
- package/dist/commands/run-engine.js +7 -71
- package/dist/commands/run.js +2 -7
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +13 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +3 -8
- package/library/actions/mobile/perps/perps.mjs +750 -39
- package/library/manifests/mobile.action-manifest.json +101 -6
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +110 -0
- package/library/recipes/mobile/perps/chase-place.recipe.json +145 -0
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +98 -0
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +111 -0
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +53 -0
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +126 -0
- package/library/recipes/mobile/perps/scale-place.recipe.json +186 -0
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +97 -0
- package/library/recipes/mobile/perps/twap-place.recipe.json +146 -0
- package/package.json +1 -1
- package/site/perps-advanced-orders-qa.html +96 -0
- package/site/perps.html +11 -0
|
@@ -4,10 +4,13 @@ import {
|
|
|
4
4
|
evalAsync,
|
|
5
5
|
isTargetTransition,
|
|
6
6
|
navigate,
|
|
7
|
+
routeSatisfiesNavigationTarget,
|
|
7
8
|
runAdapter,
|
|
9
|
+
waitForRoute,
|
|
8
10
|
} from '../platform/bridge.mjs';
|
|
9
11
|
import { ensureUnlocked } from '../wallet/ensure_unlocked.mjs';
|
|
10
12
|
import { selectAccount } from '../wallet/select_account.mjs';
|
|
13
|
+
import { observeNativeUi } from '../platform/observe-ui.mjs';
|
|
11
14
|
|
|
12
15
|
function sleep(ms) {
|
|
13
16
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -80,7 +83,31 @@ const MODE_ROOT_TEST_IDS = {
|
|
|
80
83
|
};
|
|
81
84
|
const READ_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
82
85
|
|
|
83
|
-
function
|
|
86
|
+
function inheritedDeadline(input) {
|
|
87
|
+
const deadline = Number(input.node?._action_deadline_epoch_ms);
|
|
88
|
+
return Number.isFinite(deadline) ? deadline : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function earliestDeadline(input, deadline) {
|
|
92
|
+
const inherited = inheritedDeadline(input);
|
|
93
|
+
return inherited === null ? deadline : Math.min(deadline, inherited);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function deadlineFromTimeout(input, timeoutMs) {
|
|
97
|
+
return earliestDeadline(
|
|
98
|
+
input,
|
|
99
|
+
Date.now() + Math.max(0, Number(timeoutMs)),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sleepBeforeDeadline(deadline, maximumMs, wait = sleep) {
|
|
104
|
+
const remainingMs = deadline - Date.now();
|
|
105
|
+
if (remainingMs <= 0) return;
|
|
106
|
+
await wait(Math.min(maximumMs, remainingMs));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function withRemainingDeadline(input, requestedDeadline, attemptTimeoutMs) {
|
|
110
|
+
const deadline = earliestDeadline(input, requestedDeadline);
|
|
84
111
|
const remainingMs = Math.max(1, deadline - Date.now());
|
|
85
112
|
const boundedTimeout = (configured) =>
|
|
86
113
|
Math.min(
|
|
@@ -93,6 +120,7 @@ function withRemainingDeadline(input, deadline, attemptTimeoutMs) {
|
|
|
93
120
|
node: {
|
|
94
121
|
...input.node,
|
|
95
122
|
_action_deadline_epoch_ms: deadline,
|
|
123
|
+
timeout_ms: boundedTimeout(input.node?.timeout_ms),
|
|
96
124
|
bridge_timeout_ms: boundedTimeout(input.node?.bridge_timeout_ms),
|
|
97
125
|
cdp_timeout_ms: boundedTimeout(input.node?.cdp_timeout_ms),
|
|
98
126
|
controller_ready_timeout_ms: boundedTimeout(
|
|
@@ -147,7 +175,10 @@ export async function ensureMode(input) {
|
|
|
147
175
|
throw new Error('metamask.perps.ensure_mode requires mode=lite|pro.');
|
|
148
176
|
}
|
|
149
177
|
const otherMode = requestedMode === 'lite' ? 'pro' : 'lite';
|
|
150
|
-
const deadline =
|
|
178
|
+
const deadline = deadlineFromTimeout(
|
|
179
|
+
input,
|
|
180
|
+
Number(input.node?.timeout_ms ?? 30_000),
|
|
181
|
+
);
|
|
151
182
|
let switched = false;
|
|
152
183
|
|
|
153
184
|
while (Date.now() < deadline) {
|
|
@@ -184,7 +215,7 @@ export async function ensureMode(input) {
|
|
|
184
215
|
switched = true;
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
|
-
await
|
|
218
|
+
await sleepBeforeDeadline(deadline, 250);
|
|
188
219
|
}
|
|
189
220
|
|
|
190
221
|
throw new Error(`Perps did not reach ${requestedMode} mode before timeout.`);
|
|
@@ -192,9 +223,19 @@ export async function ensureMode(input) {
|
|
|
192
223
|
|
|
193
224
|
export async function retryPerpsClientRead(
|
|
194
225
|
read,
|
|
195
|
-
{
|
|
226
|
+
{
|
|
227
|
+
timeoutMs = 20000,
|
|
228
|
+
intervalMs = 500,
|
|
229
|
+
sleep: wait = sleep,
|
|
230
|
+
deadlineEpochMs,
|
|
231
|
+
} = {},
|
|
196
232
|
) {
|
|
197
|
-
const
|
|
233
|
+
const localDeadline = Date.now() + Math.max(0, timeoutMs);
|
|
234
|
+
const configuredDeadline = Number(deadlineEpochMs);
|
|
235
|
+
const deadline =
|
|
236
|
+
deadlineEpochMs == null || !Number.isFinite(configuredDeadline)
|
|
237
|
+
? localDeadline
|
|
238
|
+
: Math.min(localDeadline, configuredDeadline);
|
|
198
239
|
while (true) {
|
|
199
240
|
try {
|
|
200
241
|
return await read();
|
|
@@ -204,7 +245,7 @@ export async function retryPerpsClientRead(
|
|
|
204
245
|
}
|
|
205
246
|
const remainingMs = deadline - Date.now();
|
|
206
247
|
if (remainingMs <= 0) throw error;
|
|
207
|
-
await
|
|
248
|
+
await sleepBeforeDeadline(deadline, intervalMs, wait);
|
|
208
249
|
}
|
|
209
250
|
}
|
|
210
251
|
}
|
|
@@ -329,6 +370,7 @@ async function readPositions(input) {
|
|
|
329
370
|
}
|
|
330
371
|
return positions;
|
|
331
372
|
},
|
|
373
|
+
{ deadlineEpochMs: inheritedDeadline(input) },
|
|
332
374
|
);
|
|
333
375
|
}
|
|
334
376
|
|
|
@@ -355,9 +397,227 @@ async function readOpenOrders(input) {
|
|
|
355
397
|
}
|
|
356
398
|
return orders;
|
|
357
399
|
},
|
|
400
|
+
{ deadlineEpochMs: inheritedDeadline(input) },
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function readStrategyOrders(input) {
|
|
405
|
+
return retryPerpsClientRead(
|
|
406
|
+
async () => {
|
|
407
|
+
const orders = await evalAsync(
|
|
408
|
+
input,
|
|
409
|
+
`(function(){
|
|
410
|
+
var controller = Engine && Engine.context && Engine.context.PerpsController;
|
|
411
|
+
if (!controller || typeof controller.getTwapOrders !== 'function' || typeof controller.getChaseOrders !== 'function') {
|
|
412
|
+
throw new Error('PerpsController strategy readers are unavailable; cannot clean live TWAP and Chase orders.');
|
|
413
|
+
}
|
|
414
|
+
return Promise.all([
|
|
415
|
+
controller.getTwapOrders(),
|
|
416
|
+
controller.getChaseOrders()
|
|
417
|
+
]).then(function(values){
|
|
418
|
+
if (!Array.isArray(values[0]) || !Array.isArray(values[1])) {
|
|
419
|
+
throw new Error('PerpsController strategy readers returned a non-array result.');
|
|
420
|
+
}
|
|
421
|
+
return JSON.stringify({ twap: values[0], chase: values[1] });
|
|
422
|
+
});
|
|
423
|
+
})()`,
|
|
424
|
+
);
|
|
425
|
+
if (!Array.isArray(orders?.twap) || !Array.isArray(orders?.chase)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
'PerpsController strategy readers returned an invalid result.',
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return orders;
|
|
431
|
+
},
|
|
432
|
+
{ deadlineEpochMs: inheritedDeadline(input) },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function selectedActiveTwapOrders(input, orders) {
|
|
437
|
+
return selectedItems(input, orders).filter(
|
|
438
|
+
(order) => String(order?.status ?? '').toLowerCase() === 'active',
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const ACTIVE_CHASE_STATUSES = new Set([
|
|
443
|
+
'active',
|
|
444
|
+
'backgrounded',
|
|
445
|
+
'termination_pending',
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
function selectedActiveChaseOrders(input, orders) {
|
|
449
|
+
return selectedItems(input, orders).filter((order) =>
|
|
450
|
+
ACTIVE_CHASE_STATUSES.has(String(order?.status ?? '').toLowerCase()),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function waitForStrategyOrdersAbsent(
|
|
455
|
+
input,
|
|
456
|
+
targetKeys,
|
|
457
|
+
timeoutMs = 30000,
|
|
458
|
+
) {
|
|
459
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
460
|
+
let activeTwapOrders = [];
|
|
461
|
+
let activeChaseOrders = [];
|
|
462
|
+
while (true) {
|
|
463
|
+
const orders = await readStrategyOrders(input);
|
|
464
|
+
activeTwapOrders = selectedActiveTwapOrders(
|
|
465
|
+
input,
|
|
466
|
+
orders.twap,
|
|
467
|
+
).filter((order) =>
|
|
468
|
+
targetKeys.has(
|
|
469
|
+
strategyTargetKey({
|
|
470
|
+
orderId: order.orderId,
|
|
471
|
+
orderType: 'twap',
|
|
472
|
+
providerId: order.providerId,
|
|
473
|
+
}),
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
activeChaseOrders = selectedActiveChaseOrders(
|
|
477
|
+
input,
|
|
478
|
+
orders.chase,
|
|
479
|
+
).filter((order) =>
|
|
480
|
+
targetKeys.has(
|
|
481
|
+
strategyTargetKey({
|
|
482
|
+
orderId: order.handle,
|
|
483
|
+
orderType: 'chase',
|
|
484
|
+
providerId: order.providerId,
|
|
485
|
+
}),
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
if (activeTwapOrders.length === 0 && activeChaseOrders.length === 0) {
|
|
489
|
+
return { activeTwapOrders, activeChaseOrders };
|
|
490
|
+
}
|
|
491
|
+
if (Date.now() >= deadline) {
|
|
492
|
+
return { activeTwapOrders, activeChaseOrders };
|
|
493
|
+
}
|
|
494
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function strategyTargetKey(target) {
|
|
499
|
+
return [
|
|
500
|
+
target.orderType,
|
|
501
|
+
target.providerId ?? '',
|
|
502
|
+
String(target.orderId),
|
|
503
|
+
].join(':');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isAlreadyAbsentStrategyResult(entry) {
|
|
507
|
+
return (
|
|
508
|
+
entry.success !== true &&
|
|
509
|
+
/never placed, already canceled, or filled/iu.test(
|
|
510
|
+
String(entry.result?.error ?? entry.error ?? ''),
|
|
511
|
+
)
|
|
358
512
|
);
|
|
359
513
|
}
|
|
360
514
|
|
|
515
|
+
async function closeStrategyOrders(input) {
|
|
516
|
+
requireExplicitSelection(input);
|
|
517
|
+
const orders = await readStrategyOrders(input);
|
|
518
|
+
const twapOrders = selectedActiveTwapOrders(input, orders.twap);
|
|
519
|
+
const chaseOrders = selectedActiveChaseOrders(input, orders.chase);
|
|
520
|
+
const targets = [
|
|
521
|
+
...twapOrders.map((order) => ({
|
|
522
|
+
orderId: String(order.orderId ?? ''),
|
|
523
|
+
symbol: symbolForItem(order),
|
|
524
|
+
orderType: 'twap',
|
|
525
|
+
providerId: order.providerId,
|
|
526
|
+
})),
|
|
527
|
+
...chaseOrders.map((order) => ({
|
|
528
|
+
orderId: String(order.handle ?? ''),
|
|
529
|
+
symbol: symbolForItem(order),
|
|
530
|
+
orderType: 'chase',
|
|
531
|
+
providerId: order.providerId,
|
|
532
|
+
})),
|
|
533
|
+
];
|
|
534
|
+
const invalid = targets.find((target) => !target.orderId || !target.symbol);
|
|
535
|
+
if (invalid) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Cannot cancel selected Perps strategy without an order ID and symbol: ${JSON.stringify(invalid)}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (targets.length === 0) {
|
|
541
|
+
return {
|
|
542
|
+
action: input.action,
|
|
543
|
+
changed: false,
|
|
544
|
+
canceled: false,
|
|
545
|
+
matchingCount: 0,
|
|
546
|
+
twapCount: 0,
|
|
547
|
+
chaseCount: 0,
|
|
548
|
+
acceptedCount: 0,
|
|
549
|
+
alreadyAbsentCount: 0,
|
|
550
|
+
proofPath: 'mobile-perps-controller-cancel-strategies',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const result = await evalAsync(
|
|
555
|
+
input,
|
|
556
|
+
`(function(){
|
|
557
|
+
var controller = Engine && Engine.context && Engine.context.PerpsController;
|
|
558
|
+
var targets = ${JSON.stringify(targets)};
|
|
559
|
+
if (!controller || typeof controller.cancelOrder !== 'function') {
|
|
560
|
+
throw new Error('Engine.context.PerpsController.cancelOrder is unavailable; cannot cancel live Perps strategies.');
|
|
561
|
+
}
|
|
562
|
+
return targets.reduce(function(chain, target){
|
|
563
|
+
return chain.then(function(results){
|
|
564
|
+
return controller.cancelOrder(target).then(function(value){
|
|
565
|
+
results.push({ target: target, success: !value || value.success !== false, result: value });
|
|
566
|
+
return results;
|
|
567
|
+
}, function(error){
|
|
568
|
+
results.push({ target: target, success: false, error: String(error && error.message || error) });
|
|
569
|
+
return results;
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
}, Promise.resolve([])).then(function(results){
|
|
573
|
+
return JSON.stringify(results);
|
|
574
|
+
});
|
|
575
|
+
})()`,
|
|
576
|
+
);
|
|
577
|
+
const failed = result.filter(
|
|
578
|
+
(entry) =>
|
|
579
|
+
entry.success !== true && !isAlreadyAbsentStrategyResult(entry),
|
|
580
|
+
);
|
|
581
|
+
if (failed.length > 0) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`Failed to cancel ${failed.length} selected Perps strategies: ${JSON.stringify(failed)}`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const confirmedTargetKeys = new Set(
|
|
588
|
+
result
|
|
589
|
+
.filter((entry) => entry.success === true)
|
|
590
|
+
.map((entry) => strategyTargetKey(entry.target)),
|
|
591
|
+
);
|
|
592
|
+
const remaining =
|
|
593
|
+
confirmedTargetKeys.size === 0
|
|
594
|
+
? { activeTwapOrders: [], activeChaseOrders: [] }
|
|
595
|
+
: await waitForStrategyOrdersAbsent(
|
|
596
|
+
input,
|
|
597
|
+
confirmedTargetKeys,
|
|
598
|
+
Number(input.node?.timeout_ms ?? 30000),
|
|
599
|
+
);
|
|
600
|
+
if (
|
|
601
|
+
remaining.activeTwapOrders.length > 0 ||
|
|
602
|
+
remaining.activeChaseOrders.length > 0
|
|
603
|
+
) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Expected selected Perps strategies to cancel, but ${remaining.activeTwapOrders.length} TWAP and ${remaining.activeChaseOrders.length} Chase orders remain active.`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
action: input.action,
|
|
610
|
+
changed: result.some((entry) => entry.success === true),
|
|
611
|
+
canceled: result.some((entry) => entry.success === true),
|
|
612
|
+
matchingCount: targets.length,
|
|
613
|
+
twapCount: twapOrders.length,
|
|
614
|
+
chaseCount: chaseOrders.length,
|
|
615
|
+
acceptedCount: result.filter((entry) => entry.success === true).length,
|
|
616
|
+
alreadyAbsentCount: result.filter(isAlreadyAbsentStrategyResult).length,
|
|
617
|
+
proofPath: 'mobile-perps-controller-cancel-strategies',
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
361
621
|
export function clearPerformanceCachesExpression() {
|
|
362
622
|
return `(function(){
|
|
363
623
|
var metroRequire = globalThis.__r;
|
|
@@ -459,43 +719,43 @@ async function clearPerformanceCaches(input) {
|
|
|
459
719
|
}
|
|
460
720
|
|
|
461
721
|
async function waitForPositionsAbsent(input, symbols, timeoutMs = 30000) {
|
|
462
|
-
const deadline =
|
|
722
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
463
723
|
let last = [];
|
|
464
724
|
while (Date.now() < deadline) {
|
|
465
725
|
last = await readPositions(input);
|
|
466
726
|
const remaining = last.filter((position) => symbols.includes(symbolForItem(position)));
|
|
467
727
|
if (remaining.length === 0) return last;
|
|
468
|
-
await
|
|
728
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
469
729
|
}
|
|
470
730
|
return last;
|
|
471
731
|
}
|
|
472
732
|
|
|
473
733
|
async function waitForOrdersAbsent(input, symbols, timeoutMs = 30000) {
|
|
474
|
-
const deadline =
|
|
734
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
475
735
|
let last = [];
|
|
476
736
|
while (Date.now() < deadline) {
|
|
477
737
|
last = await readOpenOrders(input);
|
|
478
738
|
const remaining = last.filter((order) => symbols.includes(symbolForItem(order)));
|
|
479
739
|
if (remaining.length === 0) return last;
|
|
480
|
-
await
|
|
740
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
481
741
|
}
|
|
482
742
|
return last;
|
|
483
743
|
}
|
|
484
744
|
|
|
485
745
|
async function waitForPositionPresent(input, symbol, timeoutMs = 30000) {
|
|
486
|
-
const deadline =
|
|
746
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
487
747
|
let last = [];
|
|
488
748
|
while (Date.now() < deadline) {
|
|
489
749
|
last = await readPositions(input);
|
|
490
750
|
const matching = last.filter((position) => symbolForItem(position) === symbol);
|
|
491
751
|
if (matching.length > 0) return last;
|
|
492
|
-
await
|
|
752
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
493
753
|
}
|
|
494
754
|
return last;
|
|
495
755
|
}
|
|
496
756
|
|
|
497
757
|
async function waitForSelectedState(input, readItems, expectedOpen, timeoutMs) {
|
|
498
|
-
const deadline =
|
|
758
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
499
759
|
let items;
|
|
500
760
|
while (true) {
|
|
501
761
|
items = await readItems(input);
|
|
@@ -504,7 +764,7 @@ async function waitForSelectedState(input, readItems, expectedOpen, timeoutMs) {
|
|
|
504
764
|
return matching;
|
|
505
765
|
}
|
|
506
766
|
if (Date.now() >= deadline) return matching;
|
|
507
|
-
await
|
|
767
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
508
768
|
}
|
|
509
769
|
}
|
|
510
770
|
|
|
@@ -605,18 +865,311 @@ function isTransientClosePriceError(result) {
|
|
|
605
865
|
return /IOC_CANCEL|Slippage/i.test(message);
|
|
606
866
|
}
|
|
607
867
|
|
|
868
|
+
function isPerpsMarketSelectorRoute(route) {
|
|
869
|
+
return ['PerpsMarketListView', 'PerpsTrendingView'].includes(route?.name);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function marketSymbolFromRoute(route) {
|
|
873
|
+
const symbol = route?.params?.market?.symbol ?? route?.params?.symbol;
|
|
874
|
+
return String(symbol ?? '').trim() ? normalizeMarketSymbol(symbol) : null;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const PERPS_MARKET_DETAILS_BACK_TEST_IDS = [
|
|
878
|
+
'perps-pro-market-header-back-button',
|
|
879
|
+
'perps-market-header-back-button',
|
|
880
|
+
];
|
|
881
|
+
const PERPS_MARKET_DETAILS_SCROLL_TEST_IDS = [
|
|
882
|
+
'perps-pro-market-scroll-view',
|
|
883
|
+
'perps-market-details-view',
|
|
884
|
+
];
|
|
885
|
+
|
|
886
|
+
async function readCurrentRoute(input) {
|
|
887
|
+
try {
|
|
888
|
+
return await bridgeCommand(input, ['get-route']);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (!isTargetTransition(error)) throw error;
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function readVisiblePerpsTestIds(input, deadline) {
|
|
896
|
+
const boundedInput = withRemainingDeadline(
|
|
897
|
+
input,
|
|
898
|
+
deadline,
|
|
899
|
+
READ_ATTEMPT_TIMEOUT_MS,
|
|
900
|
+
);
|
|
901
|
+
const observed = await observeNativeUi(
|
|
902
|
+
{ refs: ['ui.visible'], node: boundedInput.node },
|
|
903
|
+
input.context,
|
|
904
|
+
);
|
|
905
|
+
const visible = observed.observations?.['ui.visible'];
|
|
906
|
+
if (!visible) {
|
|
907
|
+
const detail = observed.warnings
|
|
908
|
+
?.map((warning) => warning.message)
|
|
909
|
+
.filter(Boolean)
|
|
910
|
+
.join('; ');
|
|
911
|
+
throw new Error(
|
|
912
|
+
`Visible Perps market navigation could not read native accessibility${detail ? `: ${detail}` : '.'}`,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return new Set(visible.items.map((item) => String(item.test_id ?? '')));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function waitForPerpsUiTarget(input, testId, deadline) {
|
|
919
|
+
let lastTestIds = [];
|
|
920
|
+
while (Date.now() < deadline) {
|
|
921
|
+
const testIds = await readVisiblePerpsTestIds(input, deadline);
|
|
922
|
+
if (testIds.has(testId)) return testIds;
|
|
923
|
+
lastTestIds = [...testIds].filter(Boolean).slice(0, 20);
|
|
924
|
+
await sleepBeforeDeadline(deadline, 250);
|
|
925
|
+
}
|
|
926
|
+
throw new Error(
|
|
927
|
+
`Perps control ${testId} did not become visible before timeout. Observed: ${lastTestIds.join(', ') || 'none'}.`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
932
|
+
const timeoutMs = Number(input.node?.navigation_timeout_ms ?? 15_000);
|
|
933
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
934
|
+
const params = { market: { symbol } };
|
|
935
|
+
const rowTestId = `perps-market-row-item-${symbol}`;
|
|
936
|
+
const searchTestId = 'perps-market-list-search-bar';
|
|
937
|
+
let visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
938
|
+
let usedSearch = false;
|
|
939
|
+
|
|
940
|
+
if (!visibleTestIds.has(rowTestId)) {
|
|
941
|
+
if (!visibleTestIds.has(searchTestId)) {
|
|
942
|
+
if (!visibleTestIds.has('perps-home-search-toggle')) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
'Perps Home has no visible market row, search field, or search control.',
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
948
|
+
'press-test-id',
|
|
949
|
+
'perps-home-search-toggle',
|
|
950
|
+
]);
|
|
951
|
+
visibleTestIds = await waitForPerpsUiTarget(
|
|
952
|
+
input,
|
|
953
|
+
searchTestId,
|
|
954
|
+
deadline,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
958
|
+
'set-input',
|
|
959
|
+
searchTestId,
|
|
960
|
+
symbol,
|
|
961
|
+
]);
|
|
962
|
+
visibleTestIds = await waitForPerpsUiTarget(
|
|
963
|
+
input,
|
|
964
|
+
rowTestId,
|
|
965
|
+
deadline,
|
|
966
|
+
);
|
|
967
|
+
usedSearch = true;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
971
|
+
'press-test-id',
|
|
972
|
+
rowTestId,
|
|
973
|
+
]);
|
|
974
|
+
const currentRoute = await waitForRoute(
|
|
975
|
+
withRemainingDeadline(input, deadline),
|
|
976
|
+
'PerpsMarketDetails',
|
|
977
|
+
Math.max(1, deadline - Date.now()),
|
|
978
|
+
params,
|
|
979
|
+
);
|
|
980
|
+
return {
|
|
981
|
+
navigated: 'PerpsMarketDetails',
|
|
982
|
+
params,
|
|
983
|
+
previousRoute,
|
|
984
|
+
currentRoute,
|
|
985
|
+
verifiedRoute: 'PerpsMarketDetails',
|
|
986
|
+
visibleMarketSelection: { usedSearch },
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function waitForRouteChange(input, previousRoute, deadline) {
|
|
991
|
+
let currentRoute = previousRoute;
|
|
992
|
+
while (Date.now() < deadline) {
|
|
993
|
+
currentRoute = await readCurrentRoute(
|
|
994
|
+
withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
|
|
995
|
+
);
|
|
996
|
+
if (
|
|
997
|
+
currentRoute &&
|
|
998
|
+
(currentRoute.name !== previousRoute?.name ||
|
|
999
|
+
currentRoute.key !== previousRoute?.key)
|
|
1000
|
+
) {
|
|
1001
|
+
return currentRoute;
|
|
1002
|
+
}
|
|
1003
|
+
await sleepBeforeDeadline(deadline, 250);
|
|
1004
|
+
}
|
|
1005
|
+
throw new Error('Perps back control did not change the current route.');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function navigateFromMarketDetailsToHome(input, previousRoute) {
|
|
1009
|
+
const timeoutMs = Number(input.node?.navigation_timeout_ms ?? 15_000);
|
|
1010
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1011
|
+
let visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
1012
|
+
let backTestId = PERPS_MARKET_DETAILS_BACK_TEST_IDS.find((testId) =>
|
|
1013
|
+
visibleTestIds.has(testId),
|
|
1014
|
+
);
|
|
1015
|
+
if (!backTestId) {
|
|
1016
|
+
for (const [index, scrollTestId] of
|
|
1017
|
+
PERPS_MARKET_DETAILS_SCROLL_TEST_IDS.entries()) {
|
|
1018
|
+
try {
|
|
1019
|
+
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
1020
|
+
'scroll-view',
|
|
1021
|
+
'--test-id',
|
|
1022
|
+
scrollTestId,
|
|
1023
|
+
'--offset',
|
|
1024
|
+
'0',
|
|
1025
|
+
'--no-animated',
|
|
1026
|
+
]);
|
|
1027
|
+
const expectedBackTestId = PERPS_MARKET_DETAILS_BACK_TEST_IDS[index];
|
|
1028
|
+
visibleTestIds = await waitForPerpsUiTarget(
|
|
1029
|
+
input,
|
|
1030
|
+
expectedBackTestId,
|
|
1031
|
+
deadline,
|
|
1032
|
+
);
|
|
1033
|
+
backTestId = expectedBackTestId;
|
|
1034
|
+
break;
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (!/No scrollable near testID=/u.test(String(error))) throw error;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (!backTestId) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
'Perps market details has no visible back control for returning home.',
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
const pressed = await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
1046
|
+
'press-test-id',
|
|
1047
|
+
backTestId,
|
|
1048
|
+
]);
|
|
1049
|
+
if (pressed?.ok === false) {
|
|
1050
|
+
throw new Error(String(pressed.error || 'Perps back control press failed.'));
|
|
1051
|
+
}
|
|
1052
|
+
const intermediateRoute = await waitForRouteChange(
|
|
1053
|
+
input,
|
|
1054
|
+
previousRoute,
|
|
1055
|
+
deadline,
|
|
1056
|
+
);
|
|
1057
|
+
if (
|
|
1058
|
+
routeSatisfiesNavigationTarget(intermediateRoute, 'PerpsMarketListView')
|
|
1059
|
+
) {
|
|
1060
|
+
return {
|
|
1061
|
+
navigated: 'PerpsMarketListView',
|
|
1062
|
+
params: {},
|
|
1063
|
+
previousRoute,
|
|
1064
|
+
currentRoute: intermediateRoute,
|
|
1065
|
+
verifiedRoute: 'PerpsMarketListView',
|
|
1066
|
+
visibleBackNavigation: { testId: backTestId },
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const navigation = await navigate(
|
|
1070
|
+
withRemainingDeadline(input, deadline),
|
|
1071
|
+
'PerpsMarketListView',
|
|
1072
|
+
{},
|
|
1073
|
+
);
|
|
1074
|
+
return {
|
|
1075
|
+
...navigation,
|
|
1076
|
+
previousRoute,
|
|
1077
|
+
intermediateRoute,
|
|
1078
|
+
visibleBackNavigation: { testId: backTestId },
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
608
1082
|
async function navigatePerps(input) {
|
|
609
1083
|
const selected = String(
|
|
610
1084
|
input.node?.target ?? input.node?.destination ?? defaultNavigationTarget(input.node),
|
|
611
1085
|
).toLowerCase();
|
|
612
1086
|
if (selected === 'home' || selected === 'perps' || selected === 'perps_home') {
|
|
613
|
-
const
|
|
614
|
-
|
|
1087
|
+
const currentRoute = await readCurrentRoute(input);
|
|
1088
|
+
if (routeSatisfiesNavigationTarget(currentRoute, 'PerpsMarketListView')) {
|
|
1089
|
+
return {
|
|
1090
|
+
action: input.action,
|
|
1091
|
+
target: selected,
|
|
1092
|
+
navigation: {
|
|
1093
|
+
navigated: 'PerpsMarketListView',
|
|
1094
|
+
params: {},
|
|
1095
|
+
previousRoute: currentRoute,
|
|
1096
|
+
currentRoute,
|
|
1097
|
+
verifiedRoute: 'PerpsMarketListView',
|
|
1098
|
+
alreadyAtRoute: true,
|
|
1099
|
+
},
|
|
1100
|
+
proofPath: 'agentic-navigation',
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const navigation = currentRoute?.name === 'PerpsMarketDetails'
|
|
1104
|
+
? await navigateFromMarketDetailsToHome(input, currentRoute)
|
|
1105
|
+
: await navigate(input, 'PerpsMarketListView', {});
|
|
1106
|
+
return {
|
|
1107
|
+
action: input.action,
|
|
1108
|
+
target: selected,
|
|
1109
|
+
navigation,
|
|
1110
|
+
proofPath: navigation.visibleBackNavigation
|
|
1111
|
+
? 'visible-native-back-navigation'
|
|
1112
|
+
: 'agentic-navigation',
|
|
1113
|
+
};
|
|
615
1114
|
}
|
|
616
1115
|
if (selected === 'market' || selected === 'market_details') {
|
|
1116
|
+
const currentRoute = await readCurrentRoute(input);
|
|
1117
|
+
const requestedMarket = input.node?.market ?? input.node?.symbol;
|
|
1118
|
+
if (
|
|
1119
|
+
!String(requestedMarket ?? '').trim() &&
|
|
1120
|
+
routeSatisfiesNavigationTarget(currentRoute, 'PerpsMarketDetails')
|
|
1121
|
+
) {
|
|
1122
|
+
return {
|
|
1123
|
+
action: input.action,
|
|
1124
|
+
target: selected,
|
|
1125
|
+
market: marketSymbolFromRoute(currentRoute),
|
|
1126
|
+
navigation: {
|
|
1127
|
+
navigated: 'PerpsMarketDetails',
|
|
1128
|
+
params: currentRoute?.params ?? {},
|
|
1129
|
+
previousRoute: currentRoute,
|
|
1130
|
+
currentRoute,
|
|
1131
|
+
verifiedRoute: 'PerpsMarketDetails',
|
|
1132
|
+
alreadyAtRoute: true,
|
|
1133
|
+
},
|
|
1134
|
+
proofPath: 'agentic-navigation',
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
617
1137
|
const symbol = marketSymbol(input);
|
|
618
|
-
const
|
|
619
|
-
|
|
1138
|
+
const params = { market: { symbol } };
|
|
1139
|
+
if (
|
|
1140
|
+
routeSatisfiesNavigationTarget(
|
|
1141
|
+
currentRoute,
|
|
1142
|
+
'PerpsMarketDetails',
|
|
1143
|
+
params,
|
|
1144
|
+
)
|
|
1145
|
+
) {
|
|
1146
|
+
return {
|
|
1147
|
+
action: input.action,
|
|
1148
|
+
target: selected,
|
|
1149
|
+
market: symbol,
|
|
1150
|
+
navigation: {
|
|
1151
|
+
navigated: 'PerpsMarketDetails',
|
|
1152
|
+
params,
|
|
1153
|
+
previousRoute: currentRoute,
|
|
1154
|
+
currentRoute,
|
|
1155
|
+
verifiedRoute: 'PerpsMarketDetails',
|
|
1156
|
+
alreadyAtRoute: true,
|
|
1157
|
+
},
|
|
1158
|
+
proofPath: 'agentic-navigation',
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const navigation = isPerpsMarketSelectorRoute(currentRoute)
|
|
1162
|
+
? await selectPerpsMarketFromHome(input, symbol, currentRoute)
|
|
1163
|
+
: await navigate(input, 'PerpsMarketDetails', params);
|
|
1164
|
+
return {
|
|
1165
|
+
action: input.action,
|
|
1166
|
+
target: selected,
|
|
1167
|
+
market: symbol,
|
|
1168
|
+
navigation,
|
|
1169
|
+
proofPath: navigation.visibleMarketSelection
|
|
1170
|
+
? 'visible-native-market-selection'
|
|
1171
|
+
: 'agentic-navigation',
|
|
1172
|
+
};
|
|
620
1173
|
}
|
|
621
1174
|
throw new Error(`Unsupported mobile Perps navigation target: ${selected}`);
|
|
622
1175
|
}
|
|
@@ -681,10 +1234,15 @@ function closePositionParams(input, position) {
|
|
|
681
1234
|
async function closePositionItem(input, position) {
|
|
682
1235
|
const maxAttempts = Number(input.node?.close_attempts ?? 3);
|
|
683
1236
|
const retryDelayMs = Number(input.node?.close_retry_delay_ms ?? 1000);
|
|
1237
|
+
const deadline = deadlineFromTimeout(
|
|
1238
|
+
input,
|
|
1239
|
+
Number(input.node?.timeout_ms ?? 30_000),
|
|
1240
|
+
);
|
|
684
1241
|
const baseParams = closePositionParams(input, position);
|
|
685
1242
|
const attempts = [];
|
|
686
1243
|
let lastResult = null;
|
|
687
1244
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1245
|
+
if (Date.now() >= deadline) break;
|
|
688
1246
|
const currentPositions = await readPositions(input);
|
|
689
1247
|
const currentPosition = currentPositions.find((candidate) => symbolForItem(candidate) === baseParams.symbol);
|
|
690
1248
|
if (!currentPosition) return { symbol: baseParams.symbol, result: { success: true, alreadyClosed: true }, attempts };
|
|
@@ -699,7 +1257,7 @@ async function closePositionItem(input, position) {
|
|
|
699
1257
|
lastResult = result;
|
|
700
1258
|
if (result?.success === false) {
|
|
701
1259
|
if (isTransientClosePriceError(result)) {
|
|
702
|
-
await
|
|
1260
|
+
await sleepBeforeDeadline(deadline, retryDelayMs);
|
|
703
1261
|
} else {
|
|
704
1262
|
break;
|
|
705
1263
|
}
|
|
@@ -822,16 +1380,12 @@ export async function placeOrder(input) {
|
|
|
822
1380
|
if (result?.success === false || result == null) {
|
|
823
1381
|
throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
|
|
824
1382
|
}
|
|
825
|
-
const refresh = await evalAsync(
|
|
826
|
-
input,
|
|
827
|
-
'globalThis.__AGENTIC__ && globalThis.__AGENTIC__.refreshPerpsStreams ? globalThis.__AGENTIC__.refreshPerpsStreams().then(function(r){return JSON.stringify(r)}) : Promise.resolve(JSON.stringify({ ok: false, reason: "refreshPerpsStreams unavailable" }))',
|
|
828
|
-
);
|
|
829
1383
|
if (orderType === 'limit') {
|
|
830
1384
|
await waitForSelectedState(input, readOpenOrders, true, Number(input.node?.timeout_ms ?? 30000));
|
|
831
1385
|
} else {
|
|
832
1386
|
await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
833
1387
|
}
|
|
834
|
-
return { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result,
|
|
1388
|
+
return { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result, proofPath: 'mobile-perps-controller-place-order' };
|
|
835
1389
|
}
|
|
836
1390
|
|
|
837
1391
|
export async function ensurePositions(input) {
|
|
@@ -876,7 +1430,8 @@ function paramsForState(input) {
|
|
|
876
1430
|
}
|
|
877
1431
|
|
|
878
1432
|
function profileDefaults(profile) {
|
|
879
|
-
const selected = String(profile ?? '
|
|
1433
|
+
const selected = String(profile ?? '').trim();
|
|
1434
|
+
if (!selected) return {};
|
|
880
1435
|
if (selected === 'clean_market_testnet') {
|
|
881
1436
|
return {
|
|
882
1437
|
provider: 'hyperliquid',
|
|
@@ -884,6 +1439,7 @@ function profileDefaults(profile) {
|
|
|
884
1439
|
page: 'market',
|
|
885
1440
|
positions: { state: 'none', mode: 'matching' },
|
|
886
1441
|
orders: { state: 'none', mode: 'matching' },
|
|
1442
|
+
strategies: { state: 'none', mode: 'matching' },
|
|
887
1443
|
};
|
|
888
1444
|
}
|
|
889
1445
|
if (selected === 'open_position_testnet') {
|
|
@@ -931,9 +1487,40 @@ function mergeStateConfig(defaults, params) {
|
|
|
931
1487
|
...params,
|
|
932
1488
|
positions: mergeNested(defaults.positions, params.positions),
|
|
933
1489
|
orders: mergeNested(defaults.orders, params.orders),
|
|
1490
|
+
strategies: mergeNested(defaults.strategies, params.strategies),
|
|
934
1491
|
};
|
|
935
1492
|
}
|
|
936
1493
|
|
|
1494
|
+
export function applyTradingStatePolicy(config) {
|
|
1495
|
+
const policy = config.trading_state;
|
|
1496
|
+
if (policy === undefined) return config;
|
|
1497
|
+
if (policy === 'preserve') {
|
|
1498
|
+
return { ...config, positions: false, orders: false, strategies: false };
|
|
1499
|
+
}
|
|
1500
|
+
if (policy === 'clean_selected_testnet_market') {
|
|
1501
|
+
if (!config.market && !config.symbol) {
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
'metamask.perps.start_state trading_state=clean_selected_testnet_market requires market or symbol.',
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const selectedMarketState = {
|
|
1507
|
+
state: 'none',
|
|
1508
|
+
mode: 'matching',
|
|
1509
|
+
};
|
|
1510
|
+
return String(config.network).toLowerCase() === 'testnet'
|
|
1511
|
+
? {
|
|
1512
|
+
...config,
|
|
1513
|
+
positions: selectedMarketState,
|
|
1514
|
+
orders: selectedMarketState,
|
|
1515
|
+
strategies: selectedMarketState,
|
|
1516
|
+
}
|
|
1517
|
+
: { ...config, positions: false, orders: false, strategies: false };
|
|
1518
|
+
}
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`metamask.perps.start_state received unsupported trading_state: ${String(policy)}`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
937
1524
|
function mergeNested(defaultValue, overrideValue) {
|
|
938
1525
|
if (overrideValue === false) return false;
|
|
939
1526
|
if (overrideValue === undefined) return defaultValue;
|
|
@@ -943,34 +1530,66 @@ function mergeNested(defaultValue, overrideValue) {
|
|
|
943
1530
|
return overrideValue;
|
|
944
1531
|
}
|
|
945
1532
|
|
|
1533
|
+
function resolveChildTimeoutMs(input, node, params) {
|
|
1534
|
+
const deadline = Number(input.node?._action_deadline_epoch_ms);
|
|
1535
|
+
const limits = [
|
|
1536
|
+
Number(node.timeout_ms ?? params.timeout_ms),
|
|
1537
|
+
Number(input.node?.timeout_ms),
|
|
1538
|
+
];
|
|
1539
|
+
if (Number.isFinite(deadline)) {
|
|
1540
|
+
limits.push(deadline - Date.now());
|
|
1541
|
+
}
|
|
1542
|
+
const finiteLimits = limits.filter(Number.isFinite);
|
|
1543
|
+
return finiteLimits.length > 0
|
|
1544
|
+
? Math.max(1, Math.min(...finiteLimits))
|
|
1545
|
+
: undefined;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
946
1548
|
function childInput(input, node) {
|
|
947
1549
|
const params = paramsForState(input);
|
|
1550
|
+
const deadline = inheritedDeadline(input);
|
|
948
1551
|
return {
|
|
949
1552
|
...input,
|
|
950
1553
|
node: {
|
|
951
1554
|
...params,
|
|
952
1555
|
...node,
|
|
1556
|
+
...(deadline === null ? {} : { _action_deadline_epoch_ms: deadline }),
|
|
953
1557
|
market: node.market ?? node.symbol ?? params.market ?? params.symbol,
|
|
954
1558
|
markets: node.markets ?? params.markets,
|
|
955
1559
|
symbols: node.symbols ?? params.symbols,
|
|
956
1560
|
side: node.side ?? params.side,
|
|
957
|
-
timeout_ms: node
|
|
1561
|
+
timeout_ms: resolveChildTimeoutMs(input, node, params),
|
|
958
1562
|
},
|
|
959
1563
|
};
|
|
960
1564
|
}
|
|
961
1565
|
|
|
962
1566
|
async function applyOrdersState(input, config) {
|
|
963
|
-
if (config === false) return { skipped: true };
|
|
1567
|
+
if (config === false || config === undefined) return { skipped: true };
|
|
964
1568
|
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
965
1569
|
return ensureOrders(childInput(input, node));
|
|
966
1570
|
}
|
|
967
1571
|
|
|
968
1572
|
async function applyPositionsState(input, config) {
|
|
969
|
-
if (config === false) return { skipped: true };
|
|
1573
|
+
if (config === false || config === undefined) return { skipped: true };
|
|
970
1574
|
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
971
1575
|
return ensurePositions(childInput(input, node));
|
|
972
1576
|
}
|
|
973
1577
|
|
|
1578
|
+
async function applyStrategiesState(input, config) {
|
|
1579
|
+
if (config === false || config === undefined) return { skipped: true };
|
|
1580
|
+
const node =
|
|
1581
|
+
config && typeof config === 'object'
|
|
1582
|
+
? config
|
|
1583
|
+
: { state: String(config ?? 'none') };
|
|
1584
|
+
const state = String(node.state ?? 'none').toLowerCase();
|
|
1585
|
+
if (!['none', 'closed', 'absent'].includes(state)) {
|
|
1586
|
+
throw new Error(
|
|
1587
|
+
`metamask.perps.start_state received unsupported strategy state: ${state}`,
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return closeStrategyOrders(childInput(input, node));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
974
1593
|
async function applyStateNavigation(input, config) {
|
|
975
1594
|
if (!config.page && !config.market && !config.symbol) return { skipped: true };
|
|
976
1595
|
const target = config.page ?? defaultNavigationTarget(config);
|
|
@@ -988,6 +1607,28 @@ async function readPerpsRuntimeState(input) {
|
|
|
988
1607
|
`);
|
|
989
1608
|
}
|
|
990
1609
|
|
|
1610
|
+
async function assertRequestedRuntimeState(input, config) {
|
|
1611
|
+
const state = await readPerpsRuntimeState(input);
|
|
1612
|
+
if (
|
|
1613
|
+
config.provider &&
|
|
1614
|
+
String(state.activeProvider ?? '').toLowerCase() !==
|
|
1615
|
+
String(config.provider).toLowerCase()
|
|
1616
|
+
) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`Refusing Perps teardown on provider ${state.activeProvider ?? 'unknown'}; expected ${config.provider}.`,
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (config.network) {
|
|
1622
|
+
const expectedTestnet = String(config.network).toLowerCase() === 'testnet';
|
|
1623
|
+
if (Boolean(state.isTestnet) !== expectedTestnet) {
|
|
1624
|
+
throw new Error(
|
|
1625
|
+
`Refusing Perps teardown on ${state.isTestnet ? 'testnet' : 'mainnet'}; expected ${config.network}.`,
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return state;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
991
1632
|
async function ensureProvider(input, config) {
|
|
992
1633
|
if (!config.provider) return { skipped: true };
|
|
993
1634
|
const expected = String(config.provider).toLowerCase();
|
|
@@ -1022,12 +1663,12 @@ async function ensureNetwork(input, config) {
|
|
|
1022
1663
|
}
|
|
1023
1664
|
|
|
1024
1665
|
async function waitForNetworkState(input, expectedTestnet, timeoutMs) {
|
|
1025
|
-
const deadline =
|
|
1666
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1026
1667
|
let last = await readPerpsRuntimeState(input);
|
|
1027
1668
|
while (Date.now() < deadline) {
|
|
1028
1669
|
last = await readPerpsRuntimeState(input);
|
|
1029
1670
|
if (Boolean(last.isTestnet) === expectedTestnet) return last;
|
|
1030
|
-
await
|
|
1671
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
1031
1672
|
}
|
|
1032
1673
|
return last;
|
|
1033
1674
|
}
|
|
@@ -1082,11 +1723,18 @@ async function applyTutorialState(input, config) {
|
|
|
1082
1723
|
|
|
1083
1724
|
export async function startState(input) {
|
|
1084
1725
|
const params = paramsForState(input);
|
|
1085
|
-
const config =
|
|
1726
|
+
const config = applyTradingStatePolicy(
|
|
1727
|
+
mergeStateConfig(profileDefaults(params.profile), params),
|
|
1728
|
+
);
|
|
1086
1729
|
const timeoutMs = Math.max(1, Number(input.node?.timeout_ms ?? 30_000));
|
|
1087
|
-
const deadline =
|
|
1730
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1088
1731
|
const stateInput = () => {
|
|
1089
|
-
const remainingMs =
|
|
1732
|
+
const remainingMs = deadline - Date.now();
|
|
1733
|
+
if (remainingMs <= 0) {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
'metamask.perps.start_state timed out before completing all phases.',
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1090
1738
|
const configuredTargetMs = Number(input.node?.target_timeout_ms);
|
|
1091
1739
|
const targetTimeoutMs = Math.min(
|
|
1092
1740
|
remainingMs,
|
|
@@ -1105,6 +1753,7 @@ export async function startState(input) {
|
|
|
1105
1753
|
...input,
|
|
1106
1754
|
node: {
|
|
1107
1755
|
...input.node,
|
|
1756
|
+
_action_deadline_epoch_ms: deadline,
|
|
1108
1757
|
timeout_ms: remainingMs,
|
|
1109
1758
|
bridge_timeout_ms: remainingMs,
|
|
1110
1759
|
cdp_timeout_ms: remainingMs,
|
|
@@ -1140,45 +1789,107 @@ export async function startState(input) {
|
|
|
1140
1789
|
const navigation = await applyStateNavigation(stateInput(), config);
|
|
1141
1790
|
const provider = await ensureProvider(stateInput(), config);
|
|
1142
1791
|
const network = await ensureNetwork(stateInput(), config);
|
|
1792
|
+
const resolvedNavigation = provider.changed || network.changed
|
|
1793
|
+
? await applyStateNavigation(stateInput(), config)
|
|
1794
|
+
: navigation;
|
|
1795
|
+
const modeInput = stateInput();
|
|
1796
|
+
const marketMode = config.market_mode
|
|
1797
|
+
? await ensureMode({
|
|
1798
|
+
...modeInput,
|
|
1799
|
+
action: 'metamask.perps.ensure_mode',
|
|
1800
|
+
node: {
|
|
1801
|
+
...modeInput.node,
|
|
1802
|
+
action: 'metamask.perps.ensure_mode',
|
|
1803
|
+
mode: config.market_mode,
|
|
1804
|
+
},
|
|
1805
|
+
})
|
|
1806
|
+
: { skipped: true };
|
|
1143
1807
|
const tutorial = await applyTutorialState(stateInput(), config);
|
|
1144
1808
|
const readyToTrade = await assertReadyToTrade(stateInput(), config);
|
|
1145
1809
|
const balance = await assertBalance(stateInput(), config);
|
|
1810
|
+
const strategies = await applyStrategiesState(
|
|
1811
|
+
stateInput(),
|
|
1812
|
+
config.strategies,
|
|
1813
|
+
);
|
|
1146
1814
|
const orders = await applyOrdersState(stateInput(), config.orders);
|
|
1147
1815
|
const positions = await applyPositionsState(stateInput(), config.positions);
|
|
1148
1816
|
return {
|
|
1149
1817
|
action: input.action,
|
|
1150
|
-
profile: config.profile ?? params.profile ??
|
|
1818
|
+
profile: config.profile ?? params.profile ?? null,
|
|
1151
1819
|
phase: 'start_state',
|
|
1152
1820
|
wallet,
|
|
1153
1821
|
account,
|
|
1154
1822
|
provider,
|
|
1155
1823
|
network,
|
|
1824
|
+
marketMode,
|
|
1156
1825
|
tutorial,
|
|
1157
1826
|
readyToTrade,
|
|
1158
1827
|
balance,
|
|
1159
|
-
market:
|
|
1828
|
+
market:
|
|
1829
|
+
config.market ??
|
|
1830
|
+
config.symbol ??
|
|
1831
|
+
resolvedNavigation.market ??
|
|
1832
|
+
navigation.market ??
|
|
1833
|
+
null,
|
|
1834
|
+
tradingState: config.trading_state ?? null,
|
|
1835
|
+
strategies,
|
|
1160
1836
|
orders,
|
|
1161
1837
|
positions,
|
|
1162
|
-
navigation,
|
|
1838
|
+
navigation: resolvedNavigation,
|
|
1163
1839
|
proofPath: 'metamask-perps-start-state',
|
|
1164
1840
|
};
|
|
1165
1841
|
}
|
|
1166
1842
|
|
|
1167
1843
|
export async function teardownState(input) {
|
|
1844
|
+
const timeoutMs = Math.max(1, Number(input.node?.timeout_ms ?? 30_000));
|
|
1845
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1846
|
+
const stateInput = (phase) => {
|
|
1847
|
+
if (Date.now() >= deadline) {
|
|
1848
|
+
throw new Error(
|
|
1849
|
+
`metamask.perps.teardown_state timed out before ${phase}.`,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
return withRemainingDeadline(input, deadline);
|
|
1853
|
+
};
|
|
1168
1854
|
const params = paramsForState(input);
|
|
1169
1855
|
const defaults = {
|
|
1856
|
+
provider: 'hyperliquid',
|
|
1857
|
+
network: 'testnet',
|
|
1170
1858
|
page: 'home',
|
|
1171
1859
|
positions: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1172
1860
|
orders: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1861
|
+
strategies: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1173
1862
|
};
|
|
1174
1863
|
const config = mergeStateConfig(defaults, params);
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1864
|
+
const environment = await assertRequestedRuntimeState(
|
|
1865
|
+
stateInput('checking the requested environment'),
|
|
1866
|
+
config,
|
|
1867
|
+
);
|
|
1868
|
+
const strategies = await applyStrategiesState(
|
|
1869
|
+
stateInput('cleaning strategies'),
|
|
1870
|
+
config.strategies,
|
|
1871
|
+
);
|
|
1872
|
+
const orders = await applyOrdersState(
|
|
1873
|
+
stateInput('cleaning orders'),
|
|
1874
|
+
config.orders,
|
|
1875
|
+
);
|
|
1876
|
+
const positions = await applyPositionsState(
|
|
1877
|
+
stateInput('closing positions'),
|
|
1878
|
+
config.positions,
|
|
1879
|
+
);
|
|
1880
|
+
const navigation =
|
|
1881
|
+
config.page === false
|
|
1882
|
+
? { skipped: true }
|
|
1883
|
+
: await applyStateNavigation(
|
|
1884
|
+
stateInput('restoring the requested page'),
|
|
1885
|
+
config,
|
|
1886
|
+
);
|
|
1178
1887
|
return {
|
|
1179
1888
|
action: input.action,
|
|
1180
1889
|
phase: 'teardown',
|
|
1181
1890
|
market: config.market ?? config.symbol ?? null,
|
|
1891
|
+
environment,
|
|
1892
|
+
strategies,
|
|
1182
1893
|
orders,
|
|
1183
1894
|
positions,
|
|
1184
1895
|
navigation,
|