@deeeed/metamask-harness 0.45.1 → 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 +11 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +12 -3
- package/library/actions/mobile/perps/perps.mjs +749 -34
- 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
|
}
|
|
@@ -872,7 +1430,8 @@ function paramsForState(input) {
|
|
|
872
1430
|
}
|
|
873
1431
|
|
|
874
1432
|
function profileDefaults(profile) {
|
|
875
|
-
const selected = String(profile ?? '
|
|
1433
|
+
const selected = String(profile ?? '').trim();
|
|
1434
|
+
if (!selected) return {};
|
|
876
1435
|
if (selected === 'clean_market_testnet') {
|
|
877
1436
|
return {
|
|
878
1437
|
provider: 'hyperliquid',
|
|
@@ -880,6 +1439,7 @@ function profileDefaults(profile) {
|
|
|
880
1439
|
page: 'market',
|
|
881
1440
|
positions: { state: 'none', mode: 'matching' },
|
|
882
1441
|
orders: { state: 'none', mode: 'matching' },
|
|
1442
|
+
strategies: { state: 'none', mode: 'matching' },
|
|
883
1443
|
};
|
|
884
1444
|
}
|
|
885
1445
|
if (selected === 'open_position_testnet') {
|
|
@@ -927,9 +1487,40 @@ function mergeStateConfig(defaults, params) {
|
|
|
927
1487
|
...params,
|
|
928
1488
|
positions: mergeNested(defaults.positions, params.positions),
|
|
929
1489
|
orders: mergeNested(defaults.orders, params.orders),
|
|
1490
|
+
strategies: mergeNested(defaults.strategies, params.strategies),
|
|
930
1491
|
};
|
|
931
1492
|
}
|
|
932
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
|
+
|
|
933
1524
|
function mergeNested(defaultValue, overrideValue) {
|
|
934
1525
|
if (overrideValue === false) return false;
|
|
935
1526
|
if (overrideValue === undefined) return defaultValue;
|
|
@@ -939,34 +1530,66 @@ function mergeNested(defaultValue, overrideValue) {
|
|
|
939
1530
|
return overrideValue;
|
|
940
1531
|
}
|
|
941
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
|
+
|
|
942
1548
|
function childInput(input, node) {
|
|
943
1549
|
const params = paramsForState(input);
|
|
1550
|
+
const deadline = inheritedDeadline(input);
|
|
944
1551
|
return {
|
|
945
1552
|
...input,
|
|
946
1553
|
node: {
|
|
947
1554
|
...params,
|
|
948
1555
|
...node,
|
|
1556
|
+
...(deadline === null ? {} : { _action_deadline_epoch_ms: deadline }),
|
|
949
1557
|
market: node.market ?? node.symbol ?? params.market ?? params.symbol,
|
|
950
1558
|
markets: node.markets ?? params.markets,
|
|
951
1559
|
symbols: node.symbols ?? params.symbols,
|
|
952
1560
|
side: node.side ?? params.side,
|
|
953
|
-
timeout_ms: node
|
|
1561
|
+
timeout_ms: resolveChildTimeoutMs(input, node, params),
|
|
954
1562
|
},
|
|
955
1563
|
};
|
|
956
1564
|
}
|
|
957
1565
|
|
|
958
1566
|
async function applyOrdersState(input, config) {
|
|
959
|
-
if (config === false) return { skipped: true };
|
|
1567
|
+
if (config === false || config === undefined) return { skipped: true };
|
|
960
1568
|
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
961
1569
|
return ensureOrders(childInput(input, node));
|
|
962
1570
|
}
|
|
963
1571
|
|
|
964
1572
|
async function applyPositionsState(input, config) {
|
|
965
|
-
if (config === false) return { skipped: true };
|
|
1573
|
+
if (config === false || config === undefined) return { skipped: true };
|
|
966
1574
|
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
967
1575
|
return ensurePositions(childInput(input, node));
|
|
968
1576
|
}
|
|
969
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
|
+
|
|
970
1593
|
async function applyStateNavigation(input, config) {
|
|
971
1594
|
if (!config.page && !config.market && !config.symbol) return { skipped: true };
|
|
972
1595
|
const target = config.page ?? defaultNavigationTarget(config);
|
|
@@ -984,6 +1607,28 @@ async function readPerpsRuntimeState(input) {
|
|
|
984
1607
|
`);
|
|
985
1608
|
}
|
|
986
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
|
+
|
|
987
1632
|
async function ensureProvider(input, config) {
|
|
988
1633
|
if (!config.provider) return { skipped: true };
|
|
989
1634
|
const expected = String(config.provider).toLowerCase();
|
|
@@ -1018,12 +1663,12 @@ async function ensureNetwork(input, config) {
|
|
|
1018
1663
|
}
|
|
1019
1664
|
|
|
1020
1665
|
async function waitForNetworkState(input, expectedTestnet, timeoutMs) {
|
|
1021
|
-
const deadline =
|
|
1666
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1022
1667
|
let last = await readPerpsRuntimeState(input);
|
|
1023
1668
|
while (Date.now() < deadline) {
|
|
1024
1669
|
last = await readPerpsRuntimeState(input);
|
|
1025
1670
|
if (Boolean(last.isTestnet) === expectedTestnet) return last;
|
|
1026
|
-
await
|
|
1671
|
+
await sleepBeforeDeadline(deadline, 500);
|
|
1027
1672
|
}
|
|
1028
1673
|
return last;
|
|
1029
1674
|
}
|
|
@@ -1078,11 +1723,18 @@ async function applyTutorialState(input, config) {
|
|
|
1078
1723
|
|
|
1079
1724
|
export async function startState(input) {
|
|
1080
1725
|
const params = paramsForState(input);
|
|
1081
|
-
const config =
|
|
1726
|
+
const config = applyTradingStatePolicy(
|
|
1727
|
+
mergeStateConfig(profileDefaults(params.profile), params),
|
|
1728
|
+
);
|
|
1082
1729
|
const timeoutMs = Math.max(1, Number(input.node?.timeout_ms ?? 30_000));
|
|
1083
|
-
const deadline =
|
|
1730
|
+
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
1084
1731
|
const stateInput = () => {
|
|
1085
|
-
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
|
+
}
|
|
1086
1738
|
const configuredTargetMs = Number(input.node?.target_timeout_ms);
|
|
1087
1739
|
const targetTimeoutMs = Math.min(
|
|
1088
1740
|
remainingMs,
|
|
@@ -1101,6 +1753,7 @@ export async function startState(input) {
|
|
|
1101
1753
|
...input,
|
|
1102
1754
|
node: {
|
|
1103
1755
|
...input.node,
|
|
1756
|
+
_action_deadline_epoch_ms: deadline,
|
|
1104
1757
|
timeout_ms: remainingMs,
|
|
1105
1758
|
bridge_timeout_ms: remainingMs,
|
|
1106
1759
|
cdp_timeout_ms: remainingMs,
|
|
@@ -1136,45 +1789,107 @@ export async function startState(input) {
|
|
|
1136
1789
|
const navigation = await applyStateNavigation(stateInput(), config);
|
|
1137
1790
|
const provider = await ensureProvider(stateInput(), config);
|
|
1138
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 };
|
|
1139
1807
|
const tutorial = await applyTutorialState(stateInput(), config);
|
|
1140
1808
|
const readyToTrade = await assertReadyToTrade(stateInput(), config);
|
|
1141
1809
|
const balance = await assertBalance(stateInput(), config);
|
|
1810
|
+
const strategies = await applyStrategiesState(
|
|
1811
|
+
stateInput(),
|
|
1812
|
+
config.strategies,
|
|
1813
|
+
);
|
|
1142
1814
|
const orders = await applyOrdersState(stateInput(), config.orders);
|
|
1143
1815
|
const positions = await applyPositionsState(stateInput(), config.positions);
|
|
1144
1816
|
return {
|
|
1145
1817
|
action: input.action,
|
|
1146
|
-
profile: config.profile ?? params.profile ??
|
|
1818
|
+
profile: config.profile ?? params.profile ?? null,
|
|
1147
1819
|
phase: 'start_state',
|
|
1148
1820
|
wallet,
|
|
1149
1821
|
account,
|
|
1150
1822
|
provider,
|
|
1151
1823
|
network,
|
|
1824
|
+
marketMode,
|
|
1152
1825
|
tutorial,
|
|
1153
1826
|
readyToTrade,
|
|
1154
1827
|
balance,
|
|
1155
|
-
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,
|
|
1156
1836
|
orders,
|
|
1157
1837
|
positions,
|
|
1158
|
-
navigation,
|
|
1838
|
+
navigation: resolvedNavigation,
|
|
1159
1839
|
proofPath: 'metamask-perps-start-state',
|
|
1160
1840
|
};
|
|
1161
1841
|
}
|
|
1162
1842
|
|
|
1163
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
|
+
};
|
|
1164
1854
|
const params = paramsForState(input);
|
|
1165
1855
|
const defaults = {
|
|
1856
|
+
provider: 'hyperliquid',
|
|
1857
|
+
network: 'testnet',
|
|
1166
1858
|
page: 'home',
|
|
1167
1859
|
positions: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1168
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,
|
|
1169
1862
|
};
|
|
1170
1863
|
const config = mergeStateConfig(defaults, params);
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
+
);
|
|
1174
1887
|
return {
|
|
1175
1888
|
action: input.action,
|
|
1176
1889
|
phase: 'teardown',
|
|
1177
1890
|
market: config.market ?? config.symbol ?? null,
|
|
1891
|
+
environment,
|
|
1892
|
+
strategies,
|
|
1178
1893
|
orders,
|
|
1179
1894
|
positions,
|
|
1180
1895
|
navigation,
|