@deeeed/metamask-harness 0.48.0 → 0.49.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 +6 -0
- package/dist/commands/help.js +17 -5
- package/dist/funding-execution-context.js +19 -4
- package/dist/metamask-action-validation.js +3 -2
- package/dist/mm-harness-cli.js +2 -1
- package/docs/RECIPES.md +2 -2
- package/library/actions/core/perps/_controller.mjs +14 -24
- package/library/actions/core/perps/assert_orders.mjs +3 -5
- package/library/actions/core/perps/assert_positions.mjs +3 -5
- package/library/actions/core/perps/close_orders.mjs +28 -4
- package/library/actions/core/perps/close_positions.mjs +53 -12
- package/library/actions/core/perps/edit_order.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +3 -4
- package/library/actions/core/perps/ensure_positions.mjs +4 -5
- package/library/actions/core/perps/place_order.mjs +7 -14
- package/library/actions/core/perps/read_account.mjs +2 -0
- package/library/actions/core/perps/read_orders.mjs +2 -0
- package/library/actions/core/perps/read_positions.mjs +3 -1
- package/library/actions/core/perps/read_snapshot.mjs +3 -0
- package/library/actions/core/perps/start_state.mjs +3 -14
- package/library/actions/core/perps/teardown_state.mjs +3 -14
- package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
- package/library/actions/core/wallet/list_accounts.mjs +3 -0
- package/library/actions/extension/analytics/consent.mjs +2 -0
- package/library/actions/extension/analytics/set_consent.mjs +2 -0
- package/library/actions/extension/assets/home-token-identity.mjs +1 -0
- package/library/actions/extension/assets/import_custom_token.mjs +2 -0
- package/library/actions/extension/assets/open_details.mjs +1 -0
- package/library/actions/extension/assets/prepare_send.mjs +1 -0
- package/library/actions/extension/assets/read_details.mjs +1 -0
- package/library/actions/extension/assets/read_visible_state.mjs +1 -0
- package/library/actions/extension/assets/review_send.mjs +1 -0
- package/library/actions/extension/assets/select_network_scope.mjs +1 -0
- package/library/actions/extension/assets/send.mjs +2 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +2 -0
- package/library/actions/extension/assets/verify_sorting.mjs +1 -0
- package/library/actions/extension/deeplink/open.mjs +1 -0
- package/library/actions/extension/networks/add_chainlist.mjs +2 -0
- package/library/actions/extension/networks/add_custom.mjs +2 -0
- package/library/actions/extension/networks/custom_network.mjs +2 -0
- package/library/actions/extension/networks/read_visible_state.mjs +1 -0
- package/library/actions/extension/networks/remove_custom.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +2 -0
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +2 -0
- package/library/actions/extension/performance/measure_detached_dom.mjs +2 -0
- package/library/actions/extension/performance/measure_navigation_memory.mjs +2 -0
- package/library/actions/extension/perps/assert_orders.mjs +1 -0
- package/library/actions/extension/perps/assert_positions.mjs +1 -0
- package/library/actions/extension/perps/assert_visible_consistency.mjs +2 -0
- package/library/actions/extension/perps/close_orders.mjs +4 -1
- package/library/actions/extension/perps/close_positions.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +159 -10
- package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
- package/library/actions/extension/perps/edit_margin.mjs +2 -0
- package/library/actions/extension/perps/ensure_orders.mjs +1 -0
- package/library/actions/extension/perps/ensure_positions.mjs +1 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
- package/library/actions/extension/perps/open_balance_action.mjs +1 -0
- package/library/actions/extension/perps/open_position_action.mjs +1 -0
- package/library/actions/extension/perps/perps.mjs +401 -25
- package/library/actions/extension/perps/place_order.mjs +1 -0
- package/library/actions/extension/perps/read_eligibility.mjs +1 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +2 -0
- package/library/actions/extension/perps/read_orders.mjs +1 -0
- package/library/actions/extension/perps/read_positions.mjs +1 -0
- package/library/actions/extension/perps/read_snapshot.mjs +2 -0
- package/library/actions/extension/perps/read_visible_state.mjs +2 -0
- package/library/actions/extension/perps/search_markets.mjs +1 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -0
- package/library/actions/extension/perps/select_market_filter.mjs +1 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +1 -0
- package/library/actions/extension/perps/state.mjs +1 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +2 -0
- package/library/actions/extension/perps/visible-mutation-identity.mjs +2 -0
- package/library/actions/extension/platform/cdp.mjs +29 -2
- package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
- package/library/actions/extension/swap_bridge/quote-state.mjs +2 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +1 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +1 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +1 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +1 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -0
- package/library/actions/extension/ui/locators.mjs +1 -0
- package/library/actions/extension/ui/navigate.mjs +2 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
- package/library/actions/extension/wallet/import.mjs +2 -0
- package/library/actions/extension/wallet/list_accounts.mjs +1 -0
- package/library/actions/extension/wallet/lock.mjs +2 -0
- package/library/actions/extension/wallet/read_state.mjs +1 -0
- package/library/actions/extension/wallet/reset.mjs +2 -0
- package/library/actions/extension/wallet/secret-input.mjs +2 -0
- package/library/actions/extension/wallet/select_account.mjs +2 -0
- package/library/actions/extension/wallet/setup.mjs +1 -0
- package/library/actions/extension/wallet/state.mjs +2 -0
- package/library/actions/extension/wallet/visible-session.mjs +2 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
- package/library/actions/mobile/analytics/set_consent.mjs +1 -0
- package/library/actions/mobile/app/network-control.mjs +2 -0
- package/library/actions/mobile/app/network.mjs +1 -0
- package/library/actions/mobile/app/network_assert.mjs +1 -0
- package/library/actions/mobile/app/network_capture.mjs +1 -0
- package/library/actions/mobile/assets/import_custom_token.mjs +2 -0
- package/library/actions/mobile/assets/open_details.mjs +1 -0
- package/library/actions/mobile/assets/read_details.mjs +1 -0
- package/library/actions/mobile/assets/read_visible_state.mjs +1 -0
- package/library/actions/mobile/assets/set_token_visibility.mjs +2 -0
- package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
- package/library/actions/mobile/deeplink/open.mjs +1 -0
- package/library/actions/mobile/networks/add_custom.mjs +1 -0
- package/library/actions/mobile/networks/network-management.mjs +1 -0
- package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
- package/library/actions/mobile/networks/remove_custom.mjs +1 -0
- package/library/actions/mobile/perps/assert_orders.mjs +1 -0
- package/library/actions/mobile/perps/assert_positions.mjs +1 -0
- package/library/actions/mobile/perps/capture_performance.mjs +1 -0
- package/library/actions/mobile/perps/close_orders.mjs +1 -0
- package/library/actions/mobile/perps/close_positions.mjs +1 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
- package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
- package/library/actions/mobile/perps/performance-capture.mjs +2 -0
- package/library/actions/mobile/perps/perps.mjs +6 -2
- package/library/actions/mobile/perps/place_order.mjs +1 -0
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +2 -0
- package/library/actions/mobile/perps/read_orders.mjs +1 -0
- package/library/actions/mobile/perps/read_positions.mjs +1 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
- package/library/actions/mobile/perps/search_markets.mjs +1 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +2 -0
- package/library/actions/mobile/platform/bridge.mjs +16 -4
- package/library/actions/mobile/platform/native-session-name.mjs +1 -0
- package/library/actions/mobile/platform/native-session.mjs +2 -0
- package/library/actions/mobile/platform/observe-ui.mjs +2 -0
- package/library/actions/mobile/platform/tool-paths.mjs +1 -0
- package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
- package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
- package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
- package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
- package/library/actions/mobile/ui/locators.mjs +1 -0
- package/library/actions/mobile/ui/native-navigation.mjs +2 -0
- package/library/actions/mobile/ui/navigate.mjs +2 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
- package/library/actions/mobile/wallet/home.mjs +1 -0
- package/library/actions/mobile/wallet/import.mjs +2 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
- package/library/actions/mobile/wallet/lock.mjs +1 -0
- package/library/actions/mobile/wallet/native-ui.mjs +2 -0
- package/library/actions/mobile/wallet/read_state.mjs +2 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
- package/library/actions/mobile/wallet/reset.mjs +1 -0
- package/library/actions/mobile/wallet/select_account.mjs +2 -0
- package/library/actions/mobile/wallet/setup.mjs +2 -0
- package/library/actions/shared/analytics/assert_events.mjs +3 -0
- package/library/actions/shared/analytics/consent.mjs +3 -0
- package/library/actions/shared/analytics/read_events.mjs +2 -0
- package/library/actions/shared/analytics/start_capture.mjs +3 -0
- package/library/actions/shared/app/network-artifact.mjs +2 -0
- package/library/actions/shared/app/network-assert.mjs +2 -0
- package/library/actions/shared/assets/visible-state.mjs +2 -0
- package/library/actions/shared/deeplink/url.mjs +3 -0
- package/library/actions/shared/networks/visible-state.mjs +2 -0
- package/library/actions/shared/perps/visible-state.mjs +2 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
- package/library/actions/shared/ui/locators.mjs +2 -0
- package/library/actions/shared/wallet/import-source.mjs +3 -0
- package/library/manifests/core.action-manifest.json +20 -0
- package/library/manifests/extension.action-manifest.json +2 -2
- package/library/manifests/mobile.action-manifest.json +3 -3
- package/library/recipes/mobile/app/{lifecycle.android-smoke.recipe.json → lifecycle-smoke.recipe.json} +12 -11
- package/package.json +1 -1
- package/site/assets/help-recipes.json +188 -35
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
// Restore selected testnet baselines, canceling orders before closing positions.
|
|
2
|
+
// Omitted domains default to empty; explicit false skips a domain.
|
|
3
|
+
|
|
1
4
|
import { isDirectRun, resolveNetwork, runAdapter } from './_controller.mjs';
|
|
2
5
|
import { ensureOrders } from './ensure_orders.mjs';
|
|
3
6
|
import { ensurePositions } from './ensure_positions.mjs';
|
|
4
7
|
|
|
5
|
-
// core Headless composite that restores the POSITION and ORDER baselines for the
|
|
6
|
-
// selected market(s) on HyperLiquid testnet after the proof window. Mirrors the
|
|
7
|
-
// extension perps-domain-dispatcher teardown_state semantics but SKIPS every
|
|
8
|
-
// navigation/page/HUD step — headless core has no UI surface.
|
|
9
|
-
//
|
|
10
|
-
// Params (extension schema, testnet-default): profile, market(s)/symbol(s),
|
|
11
|
-
// positions{state,...}, orders{state,...}, network. The UI-only `page`/`hud`
|
|
12
|
-
// fields are accepted by the schema but intentionally ignored here.
|
|
13
|
-
//
|
|
14
|
-
// Teardown defaults BOTH domains to state=none (leave the shared dev account
|
|
15
|
-
// FLAT) when a baseline block is omitted; pass a block explicitly to override, or
|
|
16
|
-
// `false` to skip a domain. Orders are canceled FIRST, then positions are closed,
|
|
17
|
-
// so resting orders never re-arm against a position mid-teardown.
|
|
18
|
-
|
|
19
8
|
const BASELINE_KEYS = [
|
|
20
9
|
'market',
|
|
21
10
|
'symbol',
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Manage full or partial TP/SL for a selected testnet position and verify the resulting triggers.
|
|
2
|
+
// Fractional sizes use live venue quantity precision.
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
configuredSymbols,
|
|
3
6
|
controllerRejection,
|
|
@@ -10,14 +13,6 @@ import {
|
|
|
10
13
|
selectedItems,
|
|
11
14
|
} from './_controller.mjs';
|
|
12
15
|
|
|
13
|
-
// core Set or replace the TP/SL attached to an existing Perps POSITION by driving
|
|
14
|
-
// the headless PerpsController.updatePositionTPSL() through the full
|
|
15
|
-
// signing/provider path. This is the position-bound half of the advanced-order
|
|
16
|
-
// contract (TAT-3511): it covers both whole-position TP/SL and PARTIAL
|
|
17
|
-
// (quantity-scoped) TP/SL, which the provider must express as standalone
|
|
18
|
-
// reduce-only trigger orders because a position-bound TP/SL always closes the
|
|
19
|
-
// whole position.
|
|
20
|
-
//
|
|
21
16
|
// Params use the controller's own provider-agnostic names: take_profit_price /
|
|
22
17
|
// take_profit_size / stop_loss_price / stop_loss_size (camelCase aliases
|
|
23
18
|
// accepted). Omitting a price removes that side; omitting a size covers the
|
|
@@ -129,9 +124,6 @@ export async function updatePositionTpsl(input) {
|
|
|
129
124
|
// Prices and sizes are resolved from live mid and the actual position, so a
|
|
130
125
|
// recipe never has to name a number that only holds for one market at one
|
|
131
126
|
// moment. An absolute value still wins when the caller gives one.
|
|
132
|
-
// Both derivations below need the market, and each used to fetch it
|
|
133
|
-
// separately. getMarketDataWithPrices reads every market, so doing it twice in
|
|
134
|
-
// one adapter is what pushed this past the node timeout.
|
|
135
127
|
let market;
|
|
136
128
|
const readMarket = async () => {
|
|
137
129
|
if (market === undefined) {
|
|
@@ -181,11 +173,16 @@ export async function updatePositionTpsl(input) {
|
|
|
181
173
|
|
|
182
174
|
if (takeProfitFraction !== undefined || stopLossFraction !== undefined) {
|
|
183
175
|
const absolutePositionSize = Math.abs(Number(position.size ?? position.szi ?? 0));
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
176
|
+
// MarketInfo carries quantity precision; formatted market prices do not.
|
|
177
|
+
const markets = await controller.getMarkets({ symbols: [symbol] });
|
|
178
|
+
const szDecimals = markets.find(
|
|
179
|
+
(item) => item.name.toUpperCase() === symbol.toUpperCase(),
|
|
180
|
+
)?.szDecimals;
|
|
181
|
+
if (!Number.isInteger(szDecimals) || szDecimals < 0) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`core updatePositionTPSL found no valid size precision for ${symbol}.\nNext: run mm-harness verify --adapter core`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
189
186
|
const fromFraction = (raw, name) => {
|
|
190
187
|
const fraction = Number(raw);
|
|
191
188
|
if (!Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {
|
|
@@ -194,9 +191,6 @@ export async function updatePositionTpsl(input) {
|
|
|
194
191
|
);
|
|
195
192
|
}
|
|
196
193
|
const exact = absolutePositionSize * fraction;
|
|
197
|
-
if (!Number.isFinite(szDecimals)) {
|
|
198
|
-
return String(exact);
|
|
199
|
-
}
|
|
200
194
|
const rounded = Number(exact.toFixed(szDecimals));
|
|
201
195
|
if (rounded <= 0) {
|
|
202
196
|
throw new Error(
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Derive public account identities from the checkout's wallet fixture and apply scope selection.
|
|
2
|
+
// Credential values are not included in the returned account list.
|
|
3
|
+
|
|
1
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
5
|
import { pathToFileURL } from 'node:url';
|
|
3
6
|
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Sets MetaMetrics and marketing consent through background controllers and checks settled state.
|
|
2
|
+
// Reloads with a temporary Chrome bridge when the debug request bridge is unavailable.
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import { consentParams } from '../../shared/analytics/consent.mjs';
|
|
3
5
|
import {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Imports an ERC-20 through token management and checks its Home row.
|
|
2
|
+
// Reusing a visible token requires a matching details-route identity and an unchanged account.
|
|
1
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import { readWalletStateExpression } from '../wallet/state.mjs';
|
|
3
5
|
import { verifyHomeIdentity } from './home-token-identity.mjs';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Opens Home and selects the all-networks token filter, then checks the label and closed menu.
|
|
1
2
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
3
|
import { openHome } from '../ui/navigate.mjs';
|
|
3
4
|
import { visible, waitFor } from './send.mjs';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Sets a token-management toggle and checks Home visibility, with optional exact ERC-20 identity.
|
|
2
|
+
// Can require a change or an already-matching preference.
|
|
1
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import { verifyHomeIdentity } from './home-token-identity.mjs';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Adds a missing network through ChainList and selects it in the Home network filter.
|
|
2
|
+
// Can reject existing networks so later cleanup does not delete prior configuration.
|
|
1
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
assertNetworkAdditionAllowed,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Adds a missing custom RPC network through the UI and selects it on Home.
|
|
2
|
+
// Can reject existing networks so later cleanup does not delete prior configuration.
|
|
1
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
assertNetworkAdditionAllowed,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Shares network-manager navigation, chain/name matching, and Home network selection.
|
|
2
|
+
// Button lookup marks DOM targets; network additions can reject existing configuration.
|
|
1
3
|
import { dataTestId } from '../platform/cdp.mjs';
|
|
2
4
|
|
|
3
5
|
async function visible(page, selector) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Runs navigation and idle memory measurements with forced GC, CDP metrics, and heap snapshots.
|
|
2
|
+
// Checks configured growth bounds and writes diagnostic artifacts for failed measurements.
|
|
1
3
|
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Compares post-GC listener growth across equal-duration idle and navigation rounds against a configured bound.
|
|
2
|
+
// Writes a diagnostic report when the measurement exceeds that bound.
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
compareIdleNavigation,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Checks detached-node growth between post-GC heap snapshots before and after repeated screen navigation.
|
|
2
|
+
// Writes a diagnostic report when growth exceeds the configured bound.
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
detachedDomBound,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Repeatedly navigates a screen and measures post-GC DOM-node, listener, and heap growth slopes.
|
|
2
|
+
// Enforces node and listener slope bounds and writes a diagnostic report when either fails.
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
measureNavigationSlope,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Sample changing Perps UI values or check recent activity containment across filters.
|
|
2
|
+
// Some modes navigate lists, select filters, or open watchlist market details.
|
|
1
3
|
import { pathToFileURL } from 'node:url';
|
|
2
4
|
|
|
3
5
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Cancel selected Perps orders through guarded mutation helpers and verify their absence.
|
|
1
2
|
import { runAdapter } from '../platform/cdp.mjs';
|
|
2
3
|
import { assertOrders, closeOrders } from './perps.mjs';
|
|
3
4
|
|
|
@@ -5,5 +6,7 @@ runAdapter(async (input) => {
|
|
|
5
6
|
const close = await closeOrders(input);
|
|
6
7
|
if (close.proofPath === 'visible-product-controls') return close;
|
|
7
8
|
const assertion = await assertOrders(input, false);
|
|
8
|
-
return
|
|
9
|
+
return close.proofPath === 'accepted-order-status'
|
|
10
|
+
? { ...assertion, ...close }
|
|
11
|
+
: { ...assertion, close };
|
|
9
12
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Close visible Perps positions with funding checks and receipt-bound identity.
|
|
2
|
+
// Accepted fills require lineage checks; full closes retain their own venue identity.
|
|
3
|
+
|
|
1
4
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
5
|
import {
|
|
3
6
|
assertTrustedExternalMutationTarget,
|
|
@@ -10,7 +13,7 @@ import {
|
|
|
10
13
|
trustedExternalMutationPlanIsExact,
|
|
11
14
|
withExternalMutationRecovery,
|
|
12
15
|
} from './mutation-receipt.mjs';
|
|
13
|
-
import { placementReceiptSelection, requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
|
|
16
|
+
import { observeHyperliquidSubmission, placementReceiptSelection, requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
|
|
14
17
|
import {
|
|
15
18
|
absoluteDelta,
|
|
16
19
|
parsePositionDetail,
|
|
@@ -58,6 +61,97 @@ async function readVisibleClosePosition(page, market) {
|
|
|
58
61
|
})()`);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
async function readAcceptedFill(page, placement, market, side, visibleSize) {
|
|
65
|
+
const accepted = placement.record?.acceptance;
|
|
66
|
+
if (!accepted?.venueOrderId) throw new Error('Position cleanup requires the exact accepted venue order ID.');
|
|
67
|
+
const deadline = Date.now() + 10_000;
|
|
68
|
+
let startTime = null;
|
|
69
|
+
// Explicit end windows read current fills instead of the controller's recent-history cache.
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
const result = await page.session.call('Runtime.evaluate', {
|
|
72
|
+
expression: `(async () => {
|
|
73
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
74
|
+
if (typeof request !== 'function') throw new Error('Read-only Perps fill API is unavailable.');
|
|
75
|
+
const recent = await request('perpsGetOrderFills', [{ aggregateByTime: false, endTime: ${JSON.stringify(Date.now())} }]);
|
|
76
|
+
if (!Array.isArray(recent) || recent.some(fill => !fill || typeof fill !== 'object'
|
|
77
|
+
|| Array.isArray(fill) || !['string', 'number'].includes(typeof fill.orderId))) {
|
|
78
|
+
throw new Error('Read-only Perps fill history is malformed.');
|
|
79
|
+
}
|
|
80
|
+
const owned = recent.filter(fill => String(fill?.orderId) === ${JSON.stringify(accepted.venueOrderId)});
|
|
81
|
+
if (owned.some(fill => !Number.isSafeInteger(fill.timestamp) || fill.timestamp <= 0)) {
|
|
82
|
+
throw new Error('Accepted order has malformed fill timestamps.');
|
|
83
|
+
}
|
|
84
|
+
return owned.length ? Math.min(...owned.map(fill => fill.timestamp)) : null;
|
|
85
|
+
})()`,
|
|
86
|
+
awaitPromise: true, returnByValue: true,
|
|
87
|
+
}, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
88
|
+
if (result.exceptionDetails) {
|
|
89
|
+
throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? 'Perps fill observation failed.');
|
|
90
|
+
}
|
|
91
|
+
startTime = result.result?.value;
|
|
92
|
+
if (startTime !== null) {
|
|
93
|
+
if (!Number.isSafeInteger(startTime) || startTime <= 0) throw new Error('Accepted fill observation returned an invalid timestamp.');
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(250, Math.max(1, deadline - Date.now()))));
|
|
97
|
+
}
|
|
98
|
+
if (startTime === null) throw new Error('Accepted order did not reach fill history before the cleanup observation deadline.');
|
|
99
|
+
const data = await page.evaluate(`(async () => {
|
|
100
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
101
|
+
if (typeof request !== 'function') throw new Error('Read-only Perps fill API is unavailable.');
|
|
102
|
+
const startTime = ${JSON.stringify(startTime)};
|
|
103
|
+
const [fills, orders] = await Promise.all([
|
|
104
|
+
request('perpsGetOrderFills', [{ aggregateByTime: false, startTime, endTime: ${JSON.stringify(Date.now())} }]),
|
|
105
|
+
request('perpsGetOpenOrders', []),
|
|
106
|
+
]);
|
|
107
|
+
if (!Array.isArray(fills) || !Array.isArray(orders) || fills.length >= 2000) {
|
|
108
|
+
throw new Error('Read-only Perps fill coverage is incomplete.');
|
|
109
|
+
}
|
|
110
|
+
const market = ${JSON.stringify(market)};
|
|
111
|
+
return {
|
|
112
|
+
openOrderCount: orders.filter(order => String(order.symbol ?? order.coin).toUpperCase() === market).length,
|
|
113
|
+
fills: fills.filter(fill => String(fill.symbol).toUpperCase() === market).map(fill => ({
|
|
114
|
+
orderId: String(fill.orderId), side: fill.side, size: fill.size,
|
|
115
|
+
startPosition: fill.startPosition, timestamp: fill.timestamp,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
})()`);
|
|
119
|
+
if (data.openOrderCount !== 0) throw new Error('Cancel the exact accepted order remainder before closing its filled position.');
|
|
120
|
+
const decimal = (value) => {
|
|
121
|
+
const match = /^(-?)(0|[1-9][0-9]{0,19})(?:\.([0-9]{1,8}))?$/u.exec(String(value));
|
|
122
|
+
if (!match) throw new Error('Perps fill size identity is invalid.');
|
|
123
|
+
return BigInt(`${match[1]}${match[2]}${String(match[3] ?? '').padEnd(8, '0')}`);
|
|
124
|
+
};
|
|
125
|
+
const owned = data.fills.filter((fill) => fill.orderId === accepted.venueOrderId);
|
|
126
|
+
if (!owned.length || data.fills.some((fill) => !Number.isSafeInteger(fill.timestamp) || fill.timestamp <= 0)) {
|
|
127
|
+
throw new Error('Accepted venue order has no independently observed complete fill identity.');
|
|
128
|
+
}
|
|
129
|
+
const firstTimestamp = Math.min(...owned.map((fill) => fill.timestamp));
|
|
130
|
+
if (data.fills.some((fill) => fill.timestamp >= firstTimestamp && fill.orderId !== accepted.venueOrderId)) {
|
|
131
|
+
throw new Error('Another order changed this market after the accepted fill; refusing shared-position cleanup.');
|
|
132
|
+
}
|
|
133
|
+
const sign = side === 'long' ? 1n : -1n;
|
|
134
|
+
owned.sort((left, right) => {
|
|
135
|
+
const difference = sign * (decimal(left.startPosition) - decimal(right.startPosition));
|
|
136
|
+
return difference < 0n ? -1 : difference > 0n ? 1 : 0;
|
|
137
|
+
});
|
|
138
|
+
let position = 0n;
|
|
139
|
+
for (const fill of owned) {
|
|
140
|
+
const size = decimal(fill.size);
|
|
141
|
+
if (fill.side !== (side === 'long' ? 'buy' : 'sell') || size <= 0n || decimal(fill.startPosition) !== position) {
|
|
142
|
+
throw new Error('Accepted order fills do not prove an exclusively owned position starting from zero.');
|
|
143
|
+
}
|
|
144
|
+
position += sign * size;
|
|
145
|
+
}
|
|
146
|
+
const sizeMatch = /^([0-9.,]+) ([A-Z0-9:_-]{2,20})$/u.exec(visibleSize);
|
|
147
|
+
if (!sizeMatch || sizeMatch[2] !== market || decimal(sizeMatch[1].replace(/,/gu, '')) !== sign * position
|
|
148
|
+
|| sign * position > decimal(accepted.size)) {
|
|
149
|
+
throw new Error('Visible position size does not equal the exact accepted-order fills.');
|
|
150
|
+
}
|
|
151
|
+
return { venueOrderId: accepted.venueOrderId, fillCount: owned.length, size: visibleSize,
|
|
152
|
+
firstFillTimestamp: firstTimestamp, lastFillTimestamp: Math.max(...owned.map((fill) => fill.timestamp)) };
|
|
153
|
+
}
|
|
154
|
+
|
|
61
155
|
async function prepareCloseModal(page, partialAmount) {
|
|
62
156
|
let confirmation;
|
|
63
157
|
for (let attempt = 0; attempt < 3 && !confirmation; attempt += 1) {
|
|
@@ -75,6 +169,7 @@ async function prepareCloseModal(page, partialAmount) {
|
|
|
75
169
|
return {
|
|
76
170
|
amount: String(document.querySelector('[data-testid="close-amount-value"] input')?.value || read('close-amount-value')),
|
|
77
171
|
fullSizeSelected: Boolean(document.querySelector('[data-testid="close-amount-slider-pct-100"]')),
|
|
172
|
+
marketOrder: !modal.querySelector('[data-testid="limit-price-input"]'),
|
|
78
173
|
margin: read('perps-close-summary-margin-value'),
|
|
79
174
|
fees: read('perps-close-summary-fees-value'),
|
|
80
175
|
receive: read('perps-close-summary-receive-value'),
|
|
@@ -129,6 +224,20 @@ async function proveGlobalCleanup(page, balanceBeforeBaseUnits, toleranceBaseUni
|
|
|
129
224
|
};
|
|
130
225
|
}
|
|
131
226
|
|
|
227
|
+
function requireCloseAcceptance(receipt, maximumBaseUnits) {
|
|
228
|
+
const accepted = receipt?.acceptance;
|
|
229
|
+
const size = receipt?.facts?.closeSizing?.size;
|
|
230
|
+
if (!accepted?.venueOrderId || accepted.status !== 'filled' || Number(accepted.filledSize) !== Number(size)
|
|
231
|
+
|| accepted.venueOrderId === receipt?.facts?.acceptedFill?.venueOrderId) {
|
|
232
|
+
throw new Error('Full close has no distinct accepted full-fill identity.');
|
|
233
|
+
}
|
|
234
|
+
const notional = Math.ceil(Number(accepted.filledSize) * Number(accepted.averagePrice) * 1_000_000);
|
|
235
|
+
if (!Number.isSafeInteger(notional) || notional <= 0 || BigInt(notional) > BigInt(maximumBaseUnits)) {
|
|
236
|
+
throw new Error('Accepted close fill exceeds the trusted operation cap.');
|
|
237
|
+
}
|
|
238
|
+
return { venueOrderId: accepted.venueOrderId, filledSize: accepted.filledSize, averagePrice: accepted.averagePrice };
|
|
239
|
+
}
|
|
240
|
+
|
|
132
241
|
export async function closeVisiblePosition(input) {
|
|
133
242
|
return withExtensionPage(input, async (page) => {
|
|
134
243
|
const market = marketSymbol(input.node?.market);
|
|
@@ -176,6 +285,7 @@ export async function closeVisiblePosition(input) {
|
|
|
176
285
|
roundingToleranceBaseUnits,
|
|
177
286
|
...(cleanupOnly ? { positionSize: fundingPositionSize, cleanupOnly: true } : {}),
|
|
178
287
|
});
|
|
288
|
+
const closeBudget = trustedExternalMutationBudget(input, 'perps-close-position');
|
|
179
289
|
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
180
290
|
if (existingReceipt) {
|
|
181
291
|
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
@@ -187,7 +297,8 @@ export async function closeVisiblePosition(input) {
|
|
|
187
297
|
dispatch: async () => {
|
|
188
298
|
throw new Error('resumed close must not dispatch');
|
|
189
299
|
},
|
|
190
|
-
observe: async () => {
|
|
300
|
+
observe: async ({ receipt }) => {
|
|
301
|
+
const acceptedClose = partialAmount == null ? requireCloseAcceptance(receipt, closeBudget.maxBaseUnits) : null;
|
|
191
302
|
const deadline = Date.now() + 45_000;
|
|
192
303
|
let finalState;
|
|
193
304
|
let absentSince = null;
|
|
@@ -218,6 +329,7 @@ export async function closeVisiblePosition(input) {
|
|
|
218
329
|
);
|
|
219
330
|
return {
|
|
220
331
|
success: true,
|
|
332
|
+
...acceptedClose,
|
|
221
333
|
finalState,
|
|
222
334
|
cleanup,
|
|
223
335
|
};
|
|
@@ -238,6 +350,8 @@ export async function closeVisiblePosition(input) {
|
|
|
238
350
|
receipt: mutation.receipt,
|
|
239
351
|
resumed: true,
|
|
240
352
|
recovery: mutation.recovery,
|
|
353
|
+
...(partialAmount == null ? { venueOrderId: mutation.observation.venueOrderId,
|
|
354
|
+
filledSize: mutation.observation.filledSize, averagePrice: mutation.observation.averagePrice } : {}),
|
|
241
355
|
finalState: mutation.observation.finalState,
|
|
242
356
|
cleanup: mutation.observation.cleanup ?? null,
|
|
243
357
|
proofPath: 'trusted-pointer-visible-ui',
|
|
@@ -275,20 +389,30 @@ export async function closeVisiblePosition(input) {
|
|
|
275
389
|
const observedNotionalBaseUnits = sizeMatch && sizeMatch[2] === market && priceBaseUnits != null
|
|
276
390
|
? `${Math.round(Number(sizeMatch[1]) * Number(priceBaseUnits))}`
|
|
277
391
|
: null;
|
|
278
|
-
const closeBudget = trustedExternalMutationBudget(input, 'perps-close-position');
|
|
279
392
|
if (!/^[1-9][0-9]{0,29}$/u.test(String(observedNotionalBaseUnits ?? ''))
|
|
280
393
|
|| BigInt(observedNotionalBaseUnits) > BigInt(closeBudget.maxBaseUnits)) {
|
|
281
394
|
throw new Error('Visible Perps position notional is unavailable or exceeds the authorized close cap.');
|
|
282
395
|
}
|
|
283
|
-
|
|
284
|
-
await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order', placementReceiptSelection(input));
|
|
396
|
+
const placement = cleanupOnly ? null
|
|
397
|
+
: await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order', placementReceiptSelection(input));
|
|
398
|
+
let acceptedFill = null;
|
|
399
|
+
if (['limit', 'market'].includes(placement?.record?.identity?.orderType)) {
|
|
400
|
+
if (partialAmount != null) throw new Error('Accepted-fill cleanup requires a full owned-size close.');
|
|
401
|
+
acceptedFill = await readAcceptedFill(page, placement, market, fundingSide, exactBefore.size);
|
|
285
402
|
}
|
|
286
|
-
const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
|
|
403
|
+
const balanceBeforeBaseUnits = String(placement?.record?.facts?.balanceBeforeBaseUnits ?? fundingBalance.observedBaseUnits);
|
|
287
404
|
if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceBeforeBaseUnits)) {
|
|
288
405
|
throw new Error('Prior order receipt has no valid visible pre-order balance.');
|
|
289
406
|
}
|
|
290
407
|
const confirmation = await prepareCloseModal(page, partialAmount);
|
|
291
|
-
|
|
408
|
+
if (partialAmount == null && (!confirmation.fullSizeSelected || !confirmation.marketOrder)) {
|
|
409
|
+
throw new Error('Receipt-bound full close requires the visible 100% market-close form.');
|
|
410
|
+
}
|
|
411
|
+
const closeSizing = partialAmount == null ? { size: sizeMatch[1], side: fundingSide === 'long' ? 'short' : 'long' } : null;
|
|
412
|
+
let submissionObserver = null;
|
|
413
|
+
let mutation;
|
|
414
|
+
try {
|
|
415
|
+
mutation = await runGuardedExternalMutation({
|
|
292
416
|
input,
|
|
293
417
|
identity,
|
|
294
418
|
publicationFacts: {
|
|
@@ -298,6 +422,8 @@ export async function closeVisiblePosition(input) {
|
|
|
298
422
|
cleanupOnly,
|
|
299
423
|
fundingBalance,
|
|
300
424
|
tradingContext,
|
|
425
|
+
...(acceptedFill ? { acceptedFill } : {}),
|
|
426
|
+
...(closeSizing ? { closeSizing } : {}),
|
|
301
427
|
},
|
|
302
428
|
dispatch: async () => {
|
|
303
429
|
let dispatchTarget;
|
|
@@ -312,7 +438,20 @@ export async function closeVisiblePosition(input) {
|
|
|
312
438
|
}) || currentPosition.size !== exactBefore.size) {
|
|
313
439
|
throw new Error('Visible Perps position changed before close dispatch.');
|
|
314
440
|
}
|
|
315
|
-
await
|
|
441
|
+
if (acceptedFill && JSON.stringify(await readAcceptedFill(page, placement, market, fundingSide, currentPosition.size))
|
|
442
|
+
!== JSON.stringify(acceptedFill)) {
|
|
443
|
+
throw new Error('Accepted-order fill identity changed before close dispatch.');
|
|
444
|
+
}
|
|
445
|
+
const currentConfirmation = await prepareCloseModal(page, partialAmount);
|
|
446
|
+
if (closeSizing) {
|
|
447
|
+
if (!currentConfirmation.fullSizeSelected || !currentConfirmation.marketOrder) {
|
|
448
|
+
throw new Error('Full market-close selection changed before dispatch.');
|
|
449
|
+
}
|
|
450
|
+
submissionObserver = await observeHyperliquidSubmission(page, { ...tradingContext, market,
|
|
451
|
+
side: closeSizing.side, size: closeSizing.size, orderType: 'market', reduceOnly: true,
|
|
452
|
+
leverage, maximumNotionalBaseUnits: closeBudget.maxBaseUnits });
|
|
453
|
+
await submissionObserver.checkSizing();
|
|
454
|
+
}
|
|
316
455
|
dispatchTarget = await page.evaluate(`(() => {
|
|
317
456
|
const element = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
318
457
|
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
@@ -326,6 +465,7 @@ export async function closeVisiblePosition(input) {
|
|
|
326
465
|
} catch (error) {
|
|
327
466
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
328
467
|
}
|
|
468
|
+
submissionObserver?.arm();
|
|
329
469
|
await page.session.call('Input.dispatchMouseEvent', {
|
|
330
470
|
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
331
471
|
button: 'left', buttons: 1, clickCount: 1,
|
|
@@ -334,8 +474,10 @@ export async function closeVisiblePosition(input) {
|
|
|
334
474
|
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
335
475
|
button: 'left', buttons: 0, clickCount: 1,
|
|
336
476
|
});
|
|
477
|
+
return submissionObserver ? submissionObserver.accepted(timeoutMs) : undefined;
|
|
337
478
|
},
|
|
338
|
-
observe: async () => {
|
|
479
|
+
observe: async ({ receipt }) => {
|
|
480
|
+
const acceptedClose = closeSizing ? requireCloseAcceptance(receipt, closeBudget.maxBaseUnits) : null;
|
|
339
481
|
const closeDeadline = Date.now() + 45_000;
|
|
340
482
|
let finalState;
|
|
341
483
|
while (Date.now() <= closeDeadline) {
|
|
@@ -364,6 +506,7 @@ export async function closeVisiblePosition(input) {
|
|
|
364
506
|
);
|
|
365
507
|
return {
|
|
366
508
|
success: true,
|
|
509
|
+
...acceptedClose,
|
|
367
510
|
finalState,
|
|
368
511
|
cleanup,
|
|
369
512
|
};
|
|
@@ -373,7 +516,10 @@ export async function closeVisiblePosition(input) {
|
|
|
373
516
|
}
|
|
374
517
|
throw new Error('matching closed-position postcondition was not visible before timeout');
|
|
375
518
|
},
|
|
376
|
-
|
|
519
|
+
});
|
|
520
|
+
} finally {
|
|
521
|
+
submissionObserver?.close();
|
|
522
|
+
}
|
|
377
523
|
return {
|
|
378
524
|
action: input.action,
|
|
379
525
|
market,
|
|
@@ -387,6 +533,9 @@ export async function closeVisiblePosition(input) {
|
|
|
387
533
|
receipt: mutation.receipt,
|
|
388
534
|
resumed: mutation.resumed,
|
|
389
535
|
recovery: mutation.recovery,
|
|
536
|
+
...(closeSizing ? { venueOrderId: mutation.observation.venueOrderId,
|
|
537
|
+
filledSize: mutation.observation.filledSize, averagePrice: mutation.observation.averagePrice,
|
|
538
|
+
closeSizing: submissionObserver?.sizing } : {}),
|
|
390
539
|
proofPath: 'trusted-pointer-visible-ui',
|
|
391
540
|
};
|
|
392
541
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Fetch production Hyperliquid data and compare it with the opened market's displayed values.
|
|
2
|
+
// Also check the funding countdown and nonblank chart canvases.
|
|
1
3
|
import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
|
|
3
5
|
function numeric(value) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Submit live margin changes through visible controls with mutation receipts and position identity checks.
|
|
2
|
+
// Observe the resulting margin and balance; removal requires a prior successful add receipt.
|
|
1
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
assertTrustedExternalMutationTarget,
|