@deeeed/metamask-harness 0.93.0 → 0.94.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 +23 -0
- package/library/actions/core/perps/_controller.mjs +3 -1
- package/library/actions/core/perps/ensure_orders.mjs +19 -3
- package/library/actions/core/perps/ensure_positions.mjs +7 -1
- package/library/actions/core/perps/place_order.mjs +23 -0
- package/library/actions/mobile/perps/perps.mjs +38 -62
- package/library/actions/mobile/platform/observe-ui.mjs +17 -2
- package/library/actions/mobile/ui/locators.mjs +2 -0
- package/library/actions/shared/perps/margin-mode.mjs +53 -0
- package/library/actions/shared/ui/locators.mjs +30 -6
- package/library/manifests/core.action-manifest.json +26 -1
- package/library/manifests/mobile.action-manifest.json +19 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.94.0 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Core `metamask.perps.place_order`, `ensure_positions` and `ensure_orders` take `margin_mode` (`isolated` or `cross`) with the same contract as Mobile: the controller order carries it, and the action proves the opened position's mode or, for a resting order, the venue lock (`getMarginModeLock`). It refuses a resting order before dispatch when the lock can't be read, and refuses anything but isolated on mainnet. `ensure_orders` proves reused orders per market and lists them in `marginModeLocks`; `margin_mode` applies only to `state=open`.
|
|
10
|
+
- Mobile and Core `metamask.perps.start_state` pass `margin_mode` through their `positions` and `orders` objects to the `ensure_*` step that creates the state.
|
|
11
|
+
- Mobile and Core positions report `side` from their signed size, and Core positions also report `marginMode`, like Mobile.
|
|
12
|
+
- On Android, an unlabeled test-ID container (for example a list row whose copy sits on child text views) reports its visible descendants' text as `descendant_text` (up to 500 characters), and `ui.locators` `byTestId` includes it in `description`.
|
|
13
|
+
- Mobile `ui.locators` takes `expect_enabled`, a map of requested test IDs to `true` or `false`, and re-reads until they reach that state or `timeout_ms` passes. The Pro margin chooser keeps Cross disabled until its venue lock read answers, so a single read can see a transient state. The last read is returned either way.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- The margin-mode request parsing and venue proofs live in one shared module (`library/actions/shared/perps/margin-mode.mjs`) used by Mobile and Core.
|
|
18
|
+
- Extension order actions don't take `margin_mode`: the Extension product has no Cross margin control to drive yet.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Mobile `place_order` with `margin_mode` retries the account-guarded position read on a transient client error instead of failing right after a fill. An account or network change is still never retried. A failed lock check now says the order may still be resting, since the order may have filled or been rejected. The lock mode is compared case-insensitively, and the returned top-level `marginMode` is lowercase like position reads; the `marginModeLock` objects keep the venue's value.
|
|
23
|
+
- Mobile `ui.locators` `test_ids` returns after one read when given no finite deadline instead of polling forever, and its description says to prove presence from `byTestId`.
|
|
24
|
+
- Mobile `ensure_orders` `margin_mode` documents `marginModeLocks`, one lock per selected market.
|
|
25
|
+
- Mobile Perps reads (positions, open orders, margin lock) retry a bridge read the app left unanswered (`CDP_TIMEOUT`, a busy JS thread) until their deadline, as they already did for `CLIENT_NOT_INITIALIZED`. A lost target or closed socket, as after a reload, still fails. Before, one busy moment right after a signed order failed `assert_positions` although the position opened.
|
|
26
|
+
- Mobile `start_state` with `page=false` keeps the current route even when `market` or `symbol` scopes the trading state. Before, it navigated to a `false` target before cleaning, so a cleanup could fail on navigation and leave the position open.
|
|
27
|
+
|
|
5
28
|
## 0.93.0 - 2026-10-21
|
|
6
29
|
|
|
7
30
|
### Added
|
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
9
9
|
import { normalizeSide, sideForItem } from '../../shared/perps/item-side.mjs';
|
|
10
|
+
import { marginModeOf } from '../../shared/perps/margin-mode.mjs';
|
|
10
11
|
import { readFixtureAccounts } from '../../shared/wallet/fixture-accounts.mjs';
|
|
11
12
|
import { createRewardsClient, rewardsApiUrl } from './_rewards.mjs';
|
|
12
13
|
|
|
@@ -655,8 +656,9 @@ export function redactPosition(position) {
|
|
|
655
656
|
return {
|
|
656
657
|
coin: position.coin ?? position.symbol ?? null,
|
|
657
658
|
size: position.size ?? position.szi ?? null,
|
|
658
|
-
side: position.side
|
|
659
|
+
side: sideForItem(position, 'position') || normalizeSide(position.side) || null,
|
|
659
660
|
entryPrice: position.entryPrice ?? position.entryPx ?? null,
|
|
661
|
+
marginMode: marginModeOf(position),
|
|
660
662
|
};
|
|
661
663
|
}
|
|
662
664
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Converge selected testnet orders to open or absent state, then assert the result.
|
|
2
2
|
// Reuse an existing matching order instead of placing another.
|
|
3
3
|
|
|
4
|
-
import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
4
|
+
import { getCoreController, getCoreControllerWithSigner, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
5
5
|
import { assertOrders } from './assert_orders.mjs';
|
|
6
6
|
import { closeOrders } from './close_orders.mjs';
|
|
7
7
|
import { placeOrder } from './place_order.mjs';
|
|
8
|
+
import { refuseMarginModeForAbsentState, requireLockMarginMode, resolveMarginMode } from '../../shared/perps/margin-mode.mjs';
|
|
8
9
|
|
|
9
10
|
export async function ensureOrders(input) {
|
|
10
11
|
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
@@ -12,6 +13,7 @@ export async function ensureOrders(input) {
|
|
|
12
13
|
const state = String(input.node.state).toLowerCase();
|
|
13
14
|
|
|
14
15
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
16
|
+
refuseMarginModeForAbsentState(input.node, input.action);
|
|
15
17
|
const close = await closeOrders(input);
|
|
16
18
|
const assertion = await assertOrders(input, false);
|
|
17
19
|
return { ...assertion, ensured: 'none', close };
|
|
@@ -28,11 +30,25 @@ export async function ensureOrders(input) {
|
|
|
28
30
|
userAddress: accountAddress,
|
|
29
31
|
});
|
|
30
32
|
let order = null;
|
|
31
|
-
|
|
33
|
+
const reused = selectedItems(input, orders);
|
|
34
|
+
if (reused.length === 0) {
|
|
32
35
|
order = await placeOrder({ ...input, node: { ...input.node, order_type: 'limit' } });
|
|
33
36
|
}
|
|
34
37
|
const assertion = await assertOrders(input, true);
|
|
35
|
-
|
|
38
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
39
|
+
if (!marginMode) return { ...assertion, ensured: 'open', order };
|
|
40
|
+
// A placed order was already proved inside placeOrder; reused orders are proved per market.
|
|
41
|
+
const locks = {};
|
|
42
|
+
if (order) {
|
|
43
|
+
locks[order.market] = order.marginModeLock;
|
|
44
|
+
} else {
|
|
45
|
+
// The lock is read for the controller's own account, which only the signer setup binds.
|
|
46
|
+
const { controller: signed } = await getCoreControllerWithSigner(input);
|
|
47
|
+
for (const symbol of [...new Set(reused.map((item) => item.coin ?? item.symbol))]) {
|
|
48
|
+
locks[symbol] = requireLockMarginMode(input.action, symbol, await signed.getMarginModeLock({ symbol }), marginMode);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { ...assertion, ensured: 'open', order, marginModeLock: Object.values(locks)[0], marginModeLocks: locks };
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
@@ -5,6 +5,7 @@ import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, s
|
|
|
5
5
|
import { assertPositions } from './assert_positions.mjs';
|
|
6
6
|
import { closePositions } from './close_positions.mjs';
|
|
7
7
|
import { placeOrder } from './place_order.mjs';
|
|
8
|
+
import { refuseMarginModeForAbsentState, requirePositionMarginMode, resolveMarginMode } from '../../shared/perps/margin-mode.mjs';
|
|
8
9
|
|
|
9
10
|
export async function ensurePositions(input) {
|
|
10
11
|
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
@@ -12,6 +13,7 @@ export async function ensurePositions(input) {
|
|
|
12
13
|
const state = String(input.node.state).toLowerCase();
|
|
13
14
|
|
|
14
15
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
16
|
+
refuseMarginModeForAbsentState(input.node, input.action);
|
|
15
17
|
const close = await closePositions(input);
|
|
16
18
|
const assertion = await assertPositions(input, false);
|
|
17
19
|
return { ...assertion, ensured: 'none', close };
|
|
@@ -25,7 +27,11 @@ export async function ensurePositions(input) {
|
|
|
25
27
|
userAddress: accountAddress,
|
|
26
28
|
});
|
|
27
29
|
let order = null;
|
|
28
|
-
|
|
30
|
+
const existing = selectedItems(input, positions, 'position');
|
|
31
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
32
|
+
// The venue cannot change an open position's mode, so a reused one must already hold it.
|
|
33
|
+
if (existing.length > 0 && marginMode) requirePositionMarginMode(input.action, existing, marginMode);
|
|
34
|
+
if (existing.length === 0) {
|
|
29
35
|
order = await placeOrder(input);
|
|
30
36
|
}
|
|
31
37
|
const assertion = await assertPositions(input, true);
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
symbolForItem,
|
|
17
17
|
} from './_controller.mjs';
|
|
18
18
|
import { waitForPositionsAbsent } from './close_positions.mjs';
|
|
19
|
+
import {
|
|
20
|
+
requireLockMarginMode,
|
|
21
|
+
requireLockReadable,
|
|
22
|
+
requirePositionMarginMode,
|
|
23
|
+
resolveMarginMode,
|
|
24
|
+
} from '../../shared/perps/margin-mode.mjs';
|
|
19
25
|
import {
|
|
20
26
|
assertFlatBeforeRoundTrip,
|
|
21
27
|
assertHouseLimitPosition,
|
|
@@ -376,6 +382,7 @@ export async function placeOrder(input) {
|
|
|
376
382
|
}
|
|
377
383
|
const isBuy = side === 'long';
|
|
378
384
|
const orderType = resolveOrderType(input);
|
|
385
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
379
386
|
const leverage = Number(optionalParam(input.node ?? {}, 'leverage') ?? 3);
|
|
380
387
|
const maxSlippageBps = Number(
|
|
381
388
|
optionalParam(
|
|
@@ -437,6 +444,7 @@ export async function placeOrder(input) {
|
|
|
437
444
|
leverage,
|
|
438
445
|
orderType,
|
|
439
446
|
closeImmediately,
|
|
447
|
+
marginMode,
|
|
440
448
|
projectRoot: input.context?.projectRoot,
|
|
441
449
|
selectedAddress: signerAddress,
|
|
442
450
|
});
|
|
@@ -489,6 +497,7 @@ export async function placeOrder(input) {
|
|
|
489
497
|
const strayTimeInForce =
|
|
490
498
|
orderType === 'limit' ? undefined : requestedTimeInForce;
|
|
491
499
|
const forwardedFields = {
|
|
500
|
+
...(marginMode ? { marginMode } : {}),
|
|
492
501
|
...(strayTriggerPrice === undefined
|
|
493
502
|
? {}
|
|
494
503
|
: { triggerPrice: strayTriggerPrice }),
|
|
@@ -566,6 +575,11 @@ export async function placeOrder(input) {
|
|
|
566
575
|
// close_immediately started from a flat market (checked above), so a position
|
|
567
576
|
// in it after a failed placement is this order's: close it rather than leave it open.
|
|
568
577
|
const restsOnBook = orderType === 'limit' || isTriggerOrderType(orderType);
|
|
578
|
+
if (marginMode && restsOnBook) {
|
|
579
|
+
requireLockReadable(input.action, symbol, await controller.getMarginModeLock({ symbol }));
|
|
580
|
+
}
|
|
581
|
+
let marginModeLock;
|
|
582
|
+
let observedMarginMode;
|
|
569
583
|
const placeAndVerify = async () => {
|
|
570
584
|
const result = await controller.placeOrder(orderParams);
|
|
571
585
|
if (!result || result.success !== true) {
|
|
@@ -614,6 +628,13 @@ export async function placeOrder(input) {
|
|
|
614
628
|
`core placeOrder (market) for ${symbol} reported success but no matching position is visible (orderId=${result.orderId ?? 'null'}).`,
|
|
615
629
|
);
|
|
616
630
|
}
|
|
631
|
+
if (marginMode && restsOnBook) {
|
|
632
|
+
const resting = ` The ${symbol} order it placed (${result.orderId ?? 'unknown id'}) may still be resting; cancel it with ensure_orders state=none.`;
|
|
633
|
+
marginModeLock = requireLockMarginMode(input.action, symbol, await controller.getMarginModeLock({ symbol }), marginMode, resting);
|
|
634
|
+
observedMarginMode = String(marginModeLock.marginMode).toLowerCase();
|
|
635
|
+
} else if (marginMode) {
|
|
636
|
+
observedMarginMode = requirePositionMarginMode(input.action, matchingPositions, marginMode);
|
|
637
|
+
}
|
|
617
638
|
return { result, matchingOrders, matchingPositions };
|
|
618
639
|
};
|
|
619
640
|
let placed;
|
|
@@ -649,6 +670,8 @@ export async function placeOrder(input) {
|
|
|
649
670
|
orderType,
|
|
650
671
|
notional: usdAmount,
|
|
651
672
|
leverage,
|
|
673
|
+
...(observedMarginMode ? { marginMode: observedMarginMode } : {}),
|
|
674
|
+
...(marginModeLock ? { marginModeLock } : {}),
|
|
652
675
|
timeInForce,
|
|
653
676
|
size: orderParams.size,
|
|
654
677
|
currentPrice,
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
// Implement Mobile Perps navigation, trading mutations, assertions, and setup or cleanup through the bridge.
|
|
2
2
|
// Strategy getters used during cleanup can settle tracked collateral.
|
|
3
|
-
import { sideForItem } from '../../shared/perps/item-side.mjs';
|
|
3
|
+
import { normalizeSide, sideForItem } from '../../shared/perps/item-side.mjs';
|
|
4
|
+
import {
|
|
5
|
+
marginModeOf,
|
|
6
|
+
refuseMarginModeForAbsentState,
|
|
7
|
+
requireLockMarginMode,
|
|
8
|
+
requireLockReadable,
|
|
9
|
+
requirePositionMarginMode,
|
|
10
|
+
resolveMarginMode,
|
|
11
|
+
} from '../../shared/perps/margin-mode.mjs';
|
|
4
12
|
import { pathToFileURL } from 'node:url';
|
|
5
13
|
import {
|
|
14
|
+
MOBILE_BRIDGE_ERROR_CODES,
|
|
6
15
|
MOBILE_BRIDGE_RESULT_ERROR_CODES,
|
|
7
16
|
bridgeCommand,
|
|
8
17
|
createSetupBudget,
|
|
@@ -251,8 +260,10 @@ export async function ensureMode(input) {
|
|
|
251
260
|
throw new Error(`Perps did not reach ${requestedMode} mode before timeout.`);
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
// Retries the client-not-initialized race
|
|
255
|
-
// the
|
|
263
|
+
// Retries the client-not-initialized race and a CDP_TIMEOUT (the app's JS
|
|
264
|
+
// thread busy, e.g. signing on a dev build) until the deadline, and a 429 with
|
|
265
|
+
// the shared rate-limit backoff. Only reads are wrapped, so a retry cannot
|
|
266
|
+
// repeat a write. A lost target or closed socket (a reload) still throws.
|
|
256
267
|
export async function retryPerpsClientRead(
|
|
257
268
|
read,
|
|
258
269
|
{
|
|
@@ -274,7 +285,8 @@ export async function retryPerpsClientRead(
|
|
|
274
285
|
try {
|
|
275
286
|
return await withRateLimitRetry(call, read, { deadline, sleep: wait, random });
|
|
276
287
|
} catch (error) {
|
|
277
|
-
if (!/CLIENT_NOT_INITIALIZED/u.test(String(error?.message ?? error))
|
|
288
|
+
if (!/CLIENT_NOT_INITIALIZED/u.test(String(error?.message ?? error))
|
|
289
|
+
&& error?.code !== MOBILE_BRIDGE_ERROR_CODES.CDP_TIMEOUT) {
|
|
278
290
|
throw error;
|
|
279
291
|
}
|
|
280
292
|
const remainingMs = deadline - Date.now();
|
|
@@ -349,17 +361,6 @@ function resolveOrderType(input) {
|
|
|
349
361
|
return value;
|
|
350
362
|
}
|
|
351
363
|
|
|
352
|
-
// Omitted keeps the venue default: the open position's mode, else isolated.
|
|
353
|
-
function resolveMarginMode(input) {
|
|
354
|
-
const raw = input.node?.margin_mode ?? input.node?.marginMode;
|
|
355
|
-
if (raw == null || raw === '') return undefined;
|
|
356
|
-
const value = String(raw).toLowerCase();
|
|
357
|
-
if (value !== 'isolated' && value !== 'cross') {
|
|
358
|
-
throw new Error(`${input.action} received unsupported margin_mode: ${value}.`);
|
|
359
|
-
}
|
|
360
|
-
return value;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
364
|
function resolveLimitPrice(input, isBuy, currentPrice) {
|
|
364
365
|
const explicit = input.node?.limit_price ?? input.node?.limitPrice ?? input.node?.price;
|
|
365
366
|
if (explicit != null && String(explicit).length > 0) {
|
|
@@ -458,30 +459,7 @@ async function readMarginModeLock(input, symbol) {
|
|
|
458
459
|
}
|
|
459
460
|
|
|
460
461
|
async function requireOrderMarginMode(input, symbol, marginMode, leftOver = '') {
|
|
461
|
-
|
|
462
|
-
if (lock?.status !== 'locked' || lock.marginMode !== marginMode) {
|
|
463
|
-
throw new Error(`${input.action} requested margin_mode=${marginMode} for ${symbol}, but the venue reports ${JSON.stringify(lock)}.${leftOver}`);
|
|
464
|
-
}
|
|
465
|
-
return lock;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// A resting order is only provable through the venue lock, so refuse before
|
|
469
|
-
// dispatch when this build or provider cannot answer it.
|
|
470
|
-
async function requireMarginModeLockReadable(input, symbol) {
|
|
471
|
-
const lock = await readMarginModeLock(input, symbol);
|
|
472
|
-
if (lock?.status !== 'locked' && lock?.status !== 'unlocked') {
|
|
473
|
-
throw new Error(`${input.action} cannot verify margin_mode for ${symbol} before placing a resting order: the venue lock reports ${JSON.stringify(lock)}.`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function requirePositionMarginMode(input, positions, marginMode) {
|
|
478
|
-
if (positions.length === 0) {
|
|
479
|
-
throw new Error(`${input.action} requested margin_mode=${marginMode}, but no matching position was observed to prove it.`);
|
|
480
|
-
}
|
|
481
|
-
const mismatched = positions.filter((position) => position.marginMode !== marginMode);
|
|
482
|
-
if (mismatched.length > 0) {
|
|
483
|
-
throw new Error(`${input.action} requested margin_mode=${marginMode}, but ${mismatched.map((p) => `${p.symbol} is ${p.marginMode}`).join(', ')}; the venue cannot change an open position's mode, so converge with state=none first.`);
|
|
484
|
-
}
|
|
462
|
+
return requireLockMarginMode(input.action, symbol, await readMarginModeLock(input, symbol), marginMode, leftOver);
|
|
485
463
|
}
|
|
486
464
|
|
|
487
465
|
const TWAP_NOT_ADVERTISED = 'provider-does-not-advertise-twap';
|
|
@@ -885,10 +863,9 @@ function redactPosition(position) {
|
|
|
885
863
|
return {
|
|
886
864
|
symbol: position.symbol ?? position.coin ?? null,
|
|
887
865
|
size: position.size ?? position.szi ?? null,
|
|
888
|
-
side: position.side
|
|
866
|
+
side: sideForItem(position, 'position') || normalizeSide(position.side) || null,
|
|
889
867
|
entryPrice: position.entryPrice ?? position.entryPx ?? null,
|
|
890
|
-
|
|
891
|
-
marginMode: String(position.leverage?.type ?? position.marginType ?? '').toLowerCase() || null,
|
|
868
|
+
marginMode: marginModeOf(position),
|
|
892
869
|
};
|
|
893
870
|
}
|
|
894
871
|
|
|
@@ -1585,7 +1562,7 @@ export async function placeOrder(input, { ensure = false } = {}) {
|
|
|
1585
1562
|
const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
|
|
1586
1563
|
const isBuy = orderSideIsBuy(side);
|
|
1587
1564
|
const orderType = resolveOrderType(input);
|
|
1588
|
-
const marginMode = resolveMarginMode(input);
|
|
1565
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
1589
1566
|
const closeImmediately = closeImmediatelyRequested(input.node);
|
|
1590
1567
|
const runtime = await readPerpsRuntimeState(input);
|
|
1591
1568
|
const network = orderNetwork({ declared: input.node?.network, liveIsTestnet: runtime?.isTestnet });
|
|
@@ -1660,7 +1637,7 @@ export async function placeOrder(input, { ensure = false } = {}) {
|
|
|
1660
1637
|
maxSlippageBps,
|
|
1661
1638
|
...(marginMode ? { marginMode } : {}),
|
|
1662
1639
|
};
|
|
1663
|
-
if (marginMode && orderType === 'limit') await
|
|
1640
|
+
if (marginMode && orderType === 'limit') requireLockReadable(input.action, symbol, await readMarginModeLock(input, symbol));
|
|
1664
1641
|
if (await tradingContextChanged(input, bound)) {
|
|
1665
1642
|
throw new Error('Perps order not dispatched: the selected account or network changed after the order rule allowed it.');
|
|
1666
1643
|
}
|
|
@@ -1712,13 +1689,16 @@ export async function placeOrder(input, { ensure = false } = {}) {
|
|
|
1712
1689
|
await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
1713
1690
|
}
|
|
1714
1691
|
if (marginMode && orderType === 'limit') {
|
|
1715
|
-
const resting = ` The ${symbol} order it placed (${result?.orderId ?? 'unknown id'})
|
|
1692
|
+
const resting = ` The ${symbol} order it placed (${result?.orderId ?? 'unknown id'}) may still be resting; ${ledgerId ? "the run's teardown cancels it" : 'cancel it with ensure_orders state=none'}.`;
|
|
1716
1693
|
marginModeLock = await requireOrderMarginMode(input, symbol, marginMode, resting);
|
|
1717
|
-
observedMarginMode = marginModeLock.marginMode;
|
|
1694
|
+
observedMarginMode = String(marginModeLock.marginMode).toLowerCase();
|
|
1718
1695
|
} else if (marginMode) {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1696
|
+
const positions = await retryPerpsClientRead(
|
|
1697
|
+
() => readBoundPositions(input, bound),
|
|
1698
|
+
{ deadlineEpochMs: inheritedDeadline(input), call: 'PerpsController.getPositions' },
|
|
1699
|
+
);
|
|
1700
|
+
const opened = positions.filter((position) => symbolForItem(position) === symbol);
|
|
1701
|
+
observedMarginMode = requirePositionMarginMode(input.action, opened, marginMode);
|
|
1722
1702
|
}
|
|
1723
1703
|
} catch (error) {
|
|
1724
1704
|
if (ledgerId && error?.knownNotDispatched) await dropMainnetOrder(input.context.projectRoot, ledgerId);
|
|
@@ -1894,7 +1874,7 @@ export async function ensurePositions(input) {
|
|
|
1894
1874
|
requireExplicitSelection(input);
|
|
1895
1875
|
const state = String(input.node.state).toLowerCase();
|
|
1896
1876
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
1897
|
-
refuseMarginModeForAbsentState(input);
|
|
1877
|
+
refuseMarginModeForAbsentState(input.node, input.action);
|
|
1898
1878
|
const close = await closePositions(input);
|
|
1899
1879
|
return { ...(await assertPositions(input, false)), close };
|
|
1900
1880
|
}
|
|
@@ -1902,8 +1882,8 @@ export async function ensurePositions(input) {
|
|
|
1902
1882
|
const existing = await readPositions(input);
|
|
1903
1883
|
if (selectedItems(input, existing).length === 0) await placeOrder(input, { ensure: true });
|
|
1904
1884
|
const result = await assertPositions(input, true);
|
|
1905
|
-
const marginMode = resolveMarginMode(input);
|
|
1906
|
-
if (marginMode) requirePositionMarginMode(input, result.positions, marginMode);
|
|
1885
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
1886
|
+
if (marginMode) requirePositionMarginMode(input.action, result.positions, marginMode);
|
|
1907
1887
|
return result;
|
|
1908
1888
|
}
|
|
1909
1889
|
throw new Error(`metamask.perps.ensure_positions received unsupported state: ${state}`);
|
|
@@ -1914,7 +1894,7 @@ export async function ensureOrders(input) {
|
|
|
1914
1894
|
requireExplicitSelection(input);
|
|
1915
1895
|
const state = String(input.node.state).toLowerCase();
|
|
1916
1896
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
1917
|
-
refuseMarginModeForAbsentState(input);
|
|
1897
|
+
refuseMarginModeForAbsentState(input.node, input.action);
|
|
1918
1898
|
const close = await closeOrders(input);
|
|
1919
1899
|
return { ...(await assertOrders(input, false)), close };
|
|
1920
1900
|
}
|
|
@@ -1925,7 +1905,7 @@ export async function ensureOrders(input) {
|
|
|
1925
1905
|
order = await placeOrder({ ...input, node: { ...input.node, order_type: 'limit' } }, { ensure: true });
|
|
1926
1906
|
}
|
|
1927
1907
|
const result = await assertOrders(input, true);
|
|
1928
|
-
const marginMode = resolveMarginMode(input);
|
|
1908
|
+
const marginMode = resolveMarginMode(input.node, input.action);
|
|
1929
1909
|
if (!marginMode) return { ...result, order };
|
|
1930
1910
|
// A placed order was already proved inside placeOrder; reused orders are proved per market.
|
|
1931
1911
|
const symbols = order ? [marketSymbol(input)] : [...new Set(result.orders.map((item) => item.symbol))];
|
|
@@ -1939,12 +1919,6 @@ export async function ensureOrders(input) {
|
|
|
1939
1919
|
}
|
|
1940
1920
|
|
|
1941
1921
|
|
|
1942
|
-
function refuseMarginModeForAbsentState(input) {
|
|
1943
|
-
if (resolveMarginMode(input)) {
|
|
1944
|
-
throw new Error(`${input.action} margin_mode applies only to state=open; drop it for state=none.`);
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
1922
|
function paramsForState(input) {
|
|
1949
1923
|
const nested = input.node?.params && typeof input.node.params === 'object' ? input.node.params : {};
|
|
1950
1924
|
return { ...nested, ...input.node };
|
|
@@ -2122,8 +2096,10 @@ async function applyStrategiesState(input, config) {
|
|
|
2122
2096
|
return closeStrategyOrders(childInput(input, node));
|
|
2123
2097
|
}
|
|
2124
2098
|
|
|
2125
|
-
|
|
2126
|
-
|
|
2099
|
+
// page=false preserves the current route even when market/symbol scopes the
|
|
2100
|
+
// cleanup, so a teardown cannot fail on navigation before it cleans.
|
|
2101
|
+
export async function applyStateNavigation(input, config) {
|
|
2102
|
+
if (config.page === false || (!config.page && !config.market && !config.symbol)) return { skipped: true };
|
|
2127
2103
|
const target = config.page ?? defaultNavigationTarget(config);
|
|
2128
2104
|
return navigatePerps(childInput(input, { target, market: config.market, symbol: config.symbol }));
|
|
2129
2105
|
}
|
|
@@ -503,18 +503,26 @@ function flattenIosHierarchy(value, textTestIds = [], hudIntent) {
|
|
|
503
503
|
return { visible, hidden, nodeCount, screenName };
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
|
|
506
|
+
// Unlabeled test-ID containers (a Pressable around TextViews) also report the
|
|
507
|
+
// visible text of their descendants as descendant_text, capped so a container
|
|
508
|
+
// such as a scroll view does not carry the whole screen.
|
|
509
|
+
const DESCENDANT_TEXT_LIMIT = 500;
|
|
510
|
+
|
|
511
|
+
export function parseAndroidHierarchy(output, hudIntent) {
|
|
507
512
|
const xmlStart = Math.max(output.indexOf('<?xml'), output.indexOf('<hierarchy'));
|
|
508
513
|
const xml = xmlStart >= 0 ? output.slice(xmlStart) : output;
|
|
509
514
|
const visible = [];
|
|
510
515
|
const hidden = [];
|
|
511
516
|
const ancestorVisibility = [];
|
|
517
|
+
const openItems = [];
|
|
512
518
|
let nodeCount = 0;
|
|
513
519
|
let screenName;
|
|
514
520
|
for (const match of xml.matchAll(/<\/?node\b[^>]*>/gu)) {
|
|
515
521
|
const tag = match[0];
|
|
516
522
|
if (tag.startsWith('</')) {
|
|
517
523
|
ancestorVisibility.pop();
|
|
524
|
+
const closed = openItems.pop();
|
|
525
|
+
if (closed?.texts.length) closed.item.descendant_text = closed.texts.join(' ').slice(0, DESCENDANT_TEXT_LIMIT);
|
|
518
526
|
continue;
|
|
519
527
|
}
|
|
520
528
|
const attributes = {};
|
|
@@ -528,6 +536,7 @@ function parseAndroidHierarchy(output, hudIntent) {
|
|
|
528
536
|
const label = text(attributes['content-desc']) ??
|
|
529
537
|
(isEditable ? undefined : text(attributes.text));
|
|
530
538
|
if (!screenName && label && !isEditable) screenName = label;
|
|
539
|
+
const ownText = isEditable ? undefined : text(attributes.text) ?? text(attributes['content-desc']);
|
|
531
540
|
const item = compactItem({
|
|
532
541
|
role,
|
|
533
542
|
label,
|
|
@@ -540,6 +549,9 @@ function parseAndroidHierarchy(output, hudIntent) {
|
|
|
540
549
|
const isVisible =
|
|
541
550
|
ancestorVisibility.every(Boolean) &&
|
|
542
551
|
attributes['visible-to-user'] !== 'false';
|
|
552
|
+
if (ownText && isVisible) {
|
|
553
|
+
for (const open of openItems) if (open) open.texts.push(ownText);
|
|
554
|
+
}
|
|
543
555
|
const isActionable =
|
|
544
556
|
isActionableRole(role) ||
|
|
545
557
|
attributes.clickable === 'true' ||
|
|
@@ -551,7 +563,10 @@ function parseAndroidHierarchy(output, hudIntent) {
|
|
|
551
563
|
hidden.push({ ...item, reason: 'hidden' });
|
|
552
564
|
}
|
|
553
565
|
}
|
|
554
|
-
if (!tag.endsWith('/>'))
|
|
566
|
+
if (!tag.endsWith('/>')) {
|
|
567
|
+
ancestorVisibility.push(isVisible);
|
|
568
|
+
openItems.push(item.test_id && !item.label ? { item, texts: [] } : null);
|
|
569
|
+
}
|
|
555
570
|
}
|
|
556
571
|
return { visible, hidden, nodeCount, screenName };
|
|
557
572
|
}
|
|
@@ -4,6 +4,7 @@ import { earliestDeadline, runAdapter } from '../platform/bridge.mjs';
|
|
|
4
4
|
import {
|
|
5
5
|
locatorsFromObservation,
|
|
6
6
|
observeRequestedTestIds,
|
|
7
|
+
requestedEnabledStates,
|
|
7
8
|
requestedTestIds,
|
|
8
9
|
requestedTimeoutMs,
|
|
9
10
|
requireVisibleObservation,
|
|
@@ -18,6 +19,7 @@ runAdapter(async (input) => {
|
|
|
18
19
|
)),
|
|
19
20
|
testIds,
|
|
20
21
|
earliestDeadline(input, Date.now() + requestedTimeoutMs(input.node)),
|
|
22
|
+
requestedEnabledStates(input.node, testIds),
|
|
21
23
|
);
|
|
22
24
|
return {
|
|
23
25
|
action: input.action,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Margin-mode requests and venue proofs shared by the Mobile and Core Perps order actions.
|
|
2
|
+
|
|
3
|
+
export const MARGIN_MODES = Object.freeze(['isolated', 'cross']);
|
|
4
|
+
|
|
5
|
+
// Omitted keeps the venue default: the open position's mode, else isolated.
|
|
6
|
+
export function resolveMarginMode(node, action) {
|
|
7
|
+
const raw = node?.margin_mode ?? node?.marginMode;
|
|
8
|
+
if (raw == null || raw === '') return undefined;
|
|
9
|
+
const value = String(raw).toLowerCase();
|
|
10
|
+
if (!MARGIN_MODES.includes(value)) {
|
|
11
|
+
throw new Error(`${action} received unsupported margin_mode: ${value}.`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Same source as the mainnet house-limit check, normalized to isolated|cross.
|
|
17
|
+
// Accepts a raw controller position or one already redacted with marginMode.
|
|
18
|
+
export function marginModeOf(position) {
|
|
19
|
+
return String(position?.marginMode ?? position?.leverage?.type ?? position?.marginType ?? '').toLowerCase() || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function refuseMarginModeForAbsentState(node, action) {
|
|
23
|
+
if (resolveMarginMode(node, action)) {
|
|
24
|
+
throw new Error(`${action} margin_mode applies only to state=open; drop it for state=none.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requirePositionMarginMode(action, positions, marginMode) {
|
|
29
|
+
if (positions.length === 0) {
|
|
30
|
+
throw new Error(`${action} requested margin_mode=${marginMode}, but no matching position was observed to prove it.`);
|
|
31
|
+
}
|
|
32
|
+
const mismatched = positions.filter((position) => marginModeOf(position) !== marginMode);
|
|
33
|
+
if (mismatched.length > 0) {
|
|
34
|
+
const found = mismatched.map((position) => `${position.symbol ?? position.coin} is ${marginModeOf(position)}`).join(', ');
|
|
35
|
+
throw new Error(`${action} requested margin_mode=${marginMode}, but ${found}; the venue cannot change an open position's mode, so converge with state=none first.`);
|
|
36
|
+
}
|
|
37
|
+
return marginModeOf(positions[0]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A resting order is only provable through the venue lock, so refuse before
|
|
41
|
+
// dispatch when the build or provider cannot answer it.
|
|
42
|
+
export function requireLockReadable(action, symbol, lock) {
|
|
43
|
+
if (lock?.status !== 'locked' && lock?.status !== 'unlocked') {
|
|
44
|
+
throw new Error(`${action} cannot verify margin_mode for ${symbol} before placing a resting order: the venue lock reports ${JSON.stringify(lock)}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function requireLockMarginMode(action, symbol, lock, marginMode, leftOver = '') {
|
|
49
|
+
if (lock?.status !== 'locked' || String(lock.marginMode).toLowerCase() !== marginMode) {
|
|
50
|
+
throw new Error(`${action} requested margin_mode=${marginMode} for ${symbol}, but the venue reports ${JSON.stringify(lock)}.${leftOver}`);
|
|
51
|
+
}
|
|
52
|
+
return lock;
|
|
53
|
+
}
|
|
@@ -77,15 +77,20 @@ function visibleStateByTestId(items, testIds, truncated) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// The native accessibility tree can lag the React tree while a sheet animates
|
|
80
|
-
// in,
|
|
81
|
-
|
|
80
|
+
// in, and a control can stay disabled until its data loads, so requested test
|
|
81
|
+
// IDs are re-read until observed (and in their expected enabled state) or the
|
|
82
|
+
// bound expires. The caller asserts on whatever the last read saw.
|
|
83
|
+
export async function observeRequestedTestIds(observe, testIds, deadlineEpochMs, expectEnabled = {}) {
|
|
82
84
|
const pollMs = 250;
|
|
83
85
|
while (true) {
|
|
84
86
|
const observation = await observe();
|
|
85
|
-
if (testIds.length === 0) return observation;
|
|
86
|
-
const items = Array.isArray(observation?.items) ? observation.items : [];
|
|
87
|
-
const
|
|
88
|
-
|
|
87
|
+
if (testIds.length === 0 || !Number.isFinite(deadlineEpochMs)) return observation;
|
|
88
|
+
const items = (Array.isArray(observation?.items) ? observation.items : []).map(record);
|
|
89
|
+
const settled = testIds.every((testId) => {
|
|
90
|
+
const item = items.find((candidate) => text(candidate.test_id) === testId);
|
|
91
|
+
return item && (!(testId in expectEnabled) || item.enabled === expectEnabled[testId]);
|
|
92
|
+
});
|
|
93
|
+
if (settled) return observation;
|
|
89
94
|
if (Date.now() + pollMs >= deadlineEpochMs) return observation;
|
|
90
95
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
91
96
|
}
|
|
@@ -100,6 +105,24 @@ export function requestedTestIds(node) {
|
|
|
100
105
|
return value.map((id) => id.trim());
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
// Booleans may arrive as rendered template strings ("true"/"false").
|
|
109
|
+
export function requestedEnabledStates(node, testIds) {
|
|
110
|
+
const value = node?.expect_enabled;
|
|
111
|
+
if (value == null) return {};
|
|
112
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
113
|
+
throw new Error('ui.locators expect_enabled must map requested test IDs to true or false.');
|
|
114
|
+
}
|
|
115
|
+
const states = Object.create(null);
|
|
116
|
+
for (const [testId, raw] of Object.entries(value)) {
|
|
117
|
+
const enabled = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
|
|
118
|
+
if (!testIds.includes(testId) || enabled === null) {
|
|
119
|
+
throw new Error(`ui.locators expect_enabled must map requested test IDs to true or false; got ${testId}: ${JSON.stringify(raw)}.`);
|
|
120
|
+
}
|
|
121
|
+
states[testId] = enabled;
|
|
122
|
+
}
|
|
123
|
+
return states;
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
export function requestedTimeoutMs(node) {
|
|
104
127
|
const value = node?.timeout_ms;
|
|
105
128
|
if (value == null) return 5000;
|
|
@@ -189,6 +212,7 @@ function supportsTextTarget(role) {
|
|
|
189
212
|
function describe(item) {
|
|
190
213
|
const parts = [text(item.role) ?? 'element'];
|
|
191
214
|
if (text(item.label)) parts.push(`label=${JSON.stringify(text(item.label))}`);
|
|
215
|
+
if (text(item.descendant_text)) parts.push(`text=${JSON.stringify(text(item.descendant_text))}`);
|
|
192
216
|
if (text(item.test_id)) parts.push(`test_id=${JSON.stringify(text(item.test_id))}`);
|
|
193
217
|
return parts.join(' ');
|
|
194
218
|
}
|
|
@@ -572,7 +572,7 @@
|
|
|
572
572
|
"execution_capabilities": []
|
|
573
573
|
},
|
|
574
574
|
"metamask.perps.read_positions": {
|
|
575
|
-
"description": "core Read live Perps positions directly from the Perps controller; without a selector, return all live positions.",
|
|
575
|
+
"description": "core Read live Perps positions directly from the Perps controller; without a selector, return all live positions. Each position reports side (from its signed size) and marginMode (isolated or cross) when the controller exposes it.",
|
|
576
576
|
"schema": {
|
|
577
577
|
"type": "object",
|
|
578
578
|
"properties": {
|
|
@@ -1098,6 +1098,11 @@
|
|
|
1098
1098
|
"type": "number",
|
|
1099
1099
|
"default": 3
|
|
1100
1100
|
},
|
|
1101
|
+
"margin_mode": {
|
|
1102
|
+
"type": "string",
|
|
1103
|
+
"enum": ["isolated", "cross"],
|
|
1104
|
+
"description": "Collateral mode for this order (Hyperliquid). The action proves the opened position or, for a resting order, the venue lock carries it and fails otherwise. Mainnet refuses anything but isolated. Omitted keeps the venue default: the open position mode, else isolated."
|
|
1105
|
+
},
|
|
1101
1106
|
"close_immediately": {
|
|
1102
1107
|
"type": "boolean",
|
|
1103
1108
|
"description": "Close the opened position again inside this action and prove the market flat (market orders only; the market must start flat). Required on mainnet, with leverage 1 and notional at most $15 from a fixture account."
|
|
@@ -1701,6 +1706,11 @@
|
|
|
1701
1706
|
],
|
|
1702
1707
|
"description": "USD notional alias."
|
|
1703
1708
|
},
|
|
1709
|
+
"margin_mode": {
|
|
1710
|
+
"type": "string",
|
|
1711
|
+
"enum": ["isolated", "cross"],
|
|
1712
|
+
"description": "Collateral mode for a created position. A reused open position must already hold it, since the venue cannot change it while open. Applies only to state=open."
|
|
1713
|
+
},
|
|
1704
1714
|
"leverage": {
|
|
1705
1715
|
"type": "number"
|
|
1706
1716
|
},
|
|
@@ -2188,6 +2198,11 @@
|
|
|
2188
2198
|
"type": "number",
|
|
2189
2199
|
"description": "Leverage used only when creating a missing open order."
|
|
2190
2200
|
},
|
|
2201
|
+
"margin_mode": {
|
|
2202
|
+
"type": "string",
|
|
2203
|
+
"enum": ["isolated", "cross"],
|
|
2204
|
+
"description": "Collateral mode for a created resting order. Also proves the venue lock binds this mode on every selected market, including orders that already rested; the result lists each lock in marginModeLocks. Applies only to state=open."
|
|
2205
|
+
},
|
|
2191
2206
|
"state": {
|
|
2192
2207
|
"type": "string",
|
|
2193
2208
|
"enum": [
|
|
@@ -2347,6 +2362,11 @@
|
|
|
2347
2362
|
"leverage": {
|
|
2348
2363
|
"type": "number"
|
|
2349
2364
|
},
|
|
2365
|
+
"margin_mode": {
|
|
2366
|
+
"type": "string",
|
|
2367
|
+
"enum": ["isolated", "cross"],
|
|
2368
|
+
"description": "Collateral mode passed to the ensure_* step that creates this state (isolated or cross); see metamask.perps.ensure_positions / ensure_orders."
|
|
2369
|
+
},
|
|
2350
2370
|
"order_type": {
|
|
2351
2371
|
"type": "string"
|
|
2352
2372
|
},
|
|
@@ -2458,6 +2478,11 @@
|
|
|
2458
2478
|
"leverage": {
|
|
2459
2479
|
"type": "number"
|
|
2460
2480
|
},
|
|
2481
|
+
"margin_mode": {
|
|
2482
|
+
"type": "string",
|
|
2483
|
+
"enum": ["isolated", "cross"],
|
|
2484
|
+
"description": "Collateral mode passed to the ensure_* step that creates this state (isolated or cross); see metamask.perps.ensure_positions / ensure_orders."
|
|
2485
|
+
},
|
|
2461
2486
|
"order_type": {
|
|
2462
2487
|
"type": "string"
|
|
2463
2488
|
},
|
|
@@ -1009,12 +1009,16 @@
|
|
|
1009
1009
|
"test_ids": {
|
|
1010
1010
|
"type": "array",
|
|
1011
1011
|
"items": { "type": "string" },
|
|
1012
|
-
"description": "Also report byTestId: the role, enabled state and description of each visible element with these test IDs, including disabled and non-interactive ones (enabled is null when unreported). Absent IDs are listed in missingTestIds, or in unobservedTestIds when the observation was truncated and cannot prove absence. Use it to assert control state; locators lists only press targets."
|
|
1012
|
+
"description": "Also report byTestId: the role, enabled state and description of each visible element with these test IDs, including disabled and non-interactive ones (enabled is null when unreported). Absent IDs are listed in missingTestIds, or in unobservedTestIds when the accessibility observation itself was truncated (not the 20-entry ranked list) and cannot prove absence. Prove presence from byTestId, not from an empty missingTestIds. Use it to assert control state; locators lists only press targets."
|
|
1013
|
+
},
|
|
1014
|
+
"expect_enabled": {
|
|
1015
|
+
"type": "object",
|
|
1016
|
+
"description": "Map of requested test IDs to the enabled state (true or false) to wait for, for controls that stay disabled until their data loads. The last read is returned either way; assert on byTestId."
|
|
1013
1017
|
},
|
|
1014
1018
|
"timeout_ms": {
|
|
1015
1019
|
"type": "number",
|
|
1016
1020
|
"minimum": 0,
|
|
1017
|
-
"description": "With test_ids, re-read the observation until every requested test ID is present or this many milliseconds pass (default 5000)."
|
|
1021
|
+
"description": "With test_ids, re-read the observation until every requested test ID is present (and matches expect_enabled) or this many milliseconds pass (default 5000)."
|
|
1018
1022
|
}
|
|
1019
1023
|
},
|
|
1020
1024
|
"additionalProperties": false
|
|
@@ -2374,7 +2378,7 @@
|
|
|
2374
2378
|
"execution_capabilities": ["app-mutation"]
|
|
2375
2379
|
},
|
|
2376
2380
|
"metamask.perps.read_positions": {
|
|
2377
|
-
"description": "mobile Read live Perps positions; without a selector, return all live positions. Each position reports marginMode (isolated or cross) when the product exposes it.",
|
|
2381
|
+
"description": "mobile Read live Perps positions; without a selector, return all live positions. Each position reports side (from its signed size) and marginMode (isolated or cross) when the product exposes it.",
|
|
2378
2382
|
"schema": {
|
|
2379
2383
|
"type": "object",
|
|
2380
2384
|
"properties": {
|
|
@@ -3273,7 +3277,7 @@
|
|
|
3273
3277
|
"margin_mode": {
|
|
3274
3278
|
"type": "string",
|
|
3275
3279
|
"enum": ["isolated", "cross"],
|
|
3276
|
-
"description": "Collateral mode for a created resting order. Also proves the venue lock binds this mode, including
|
|
3280
|
+
"description": "Collateral mode for a created resting order. Also proves the venue lock binds this mode on every selected market, including orders that already rested; the result lists each lock in marginModeLocks, and marginModeLock is the first market's. Requires a build with getMarginModeLock."
|
|
3277
3281
|
},
|
|
3278
3282
|
"state": {
|
|
3279
3283
|
"type": "string",
|
|
@@ -3582,7 +3586,7 @@
|
|
|
3582
3586
|
}
|
|
3583
3587
|
},
|
|
3584
3588
|
"page": {
|
|
3585
|
-
"description": "Initial Perps page target; omit it to use the profile or market default, or set false
|
|
3589
|
+
"description": "Initial Perps page target; omit it to use the profile or market default, or set false to preserve the current route (market/symbol then only scope the trading state). Without navigation, Perps must already have been opened in this app session.",
|
|
3586
3590
|
"type": ["string", "boolean"],
|
|
3587
3591
|
"enum": [false, "home", "perps", "perps_home", "market", "market_details"]
|
|
3588
3592
|
},
|
|
@@ -3642,6 +3646,11 @@
|
|
|
3642
3646
|
"leverage": {
|
|
3643
3647
|
"type": "number"
|
|
3644
3648
|
},
|
|
3649
|
+
"margin_mode": {
|
|
3650
|
+
"type": "string",
|
|
3651
|
+
"enum": ["isolated", "cross"],
|
|
3652
|
+
"description": "Collateral mode passed to the ensure_* step that creates this state (isolated or cross); see metamask.perps.ensure_positions / ensure_orders."
|
|
3653
|
+
},
|
|
3645
3654
|
"order_type": {
|
|
3646
3655
|
"type": "string"
|
|
3647
3656
|
},
|
|
@@ -3730,6 +3739,11 @@
|
|
|
3730
3739
|
"leverage": {
|
|
3731
3740
|
"type": "number"
|
|
3732
3741
|
},
|
|
3742
|
+
"margin_mode": {
|
|
3743
|
+
"type": "string",
|
|
3744
|
+
"enum": ["isolated", "cross"],
|
|
3745
|
+
"description": "Collateral mode passed to the ensure_* step that creates this state (isolated or cross); see metamask.perps.ensure_positions / ensure_orders."
|
|
3746
|
+
},
|
|
3733
3747
|
"order_type": {
|
|
3734
3748
|
"type": "string"
|
|
3735
3749
|
},
|