@deeeed/metamask-harness 0.47.4 → 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 +12 -0
- package/README.md +3 -2
- package/adapters/extension/lib/macos-focus.cjs +2 -2
- package/adapters/extension/live.sh +6 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/shared/ensure-runner-deps.sh +6 -0
- package/dist/adapters/extension/runtime-decision.js +18 -1
- package/dist/adapters/extension/runtime.js +5 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/app-lifecycle.js +3 -1
- package/dist/command-contract.js +2 -0
- package/dist/commands/call.js +1 -8
- package/dist/commands/device-target.js +12 -3
- package/dist/commands/doctor.js +7 -5
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/help.js +17 -5
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/manifest.js +3 -1
- package/dist/commands/mobile-device-view.js +13 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/run-engine.js +29 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status.js +8 -4
- package/dist/devices.js +31 -9
- package/dist/execution-provenance.js +8 -7
- package/dist/funding-execution-context.js +62 -17
- package/dist/heal-bounds.js +3 -3
- package/dist/live-adapter-contract.js +4 -0
- package/dist/manifest.js +2 -13
- package/dist/metamask-action-validation.js +3 -2
- package/dist/mm-harness-cli.js +4 -2
- package/dist/runner.js +14 -3
- package/docs/RECIPES.md +135 -24
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
- package/library/actions/core/perps/_controller.mjs +24 -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 +39 -19
- 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 +31 -0
- package/library/actions/extension/assets/import_custom_token.mjs +63 -79
- package/library/actions/extension/assets/open_details.mjs +19 -7
- package/library/actions/extension/assets/prepare_send.mjs +1 -0
- package/library/actions/extension/assets/read_details.mjs +62 -29
- package/library/actions/extension/assets/read_visible_state.mjs +5 -1
- package/library/actions/extension/assets/review_send.mjs +1 -0
- package/library/actions/extension/assets/select_network_scope.mjs +17 -9
- 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 +86 -17
- package/library/actions/extension/assets/verify_sorting.mjs +63 -14
- 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 +21 -17
- package/library/actions/extension/networks/custom_network.mjs +10 -3
- package/library/actions/extension/networks/read_visible_state.mjs +2 -0
- package/library/actions/extension/networks/remove_custom.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +37 -6
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +5 -2
- package/library/actions/extension/performance/measure_detached_dom.mjs +5 -2
- package/library/actions/extension/performance/measure_navigation_memory.mjs +5 -2
- 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 +11 -4
- 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 +160 -11
- package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
- package/library/actions/extension/perps/edit_margin.mjs +3 -1
- 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 +17 -7
- package/library/actions/extension/perps/open_position_action.mjs +1 -0
- package/library/actions/extension/perps/perps.mjs +550 -69
- 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 +10 -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 +4 -1
- package/library/actions/extension/perps/read_visible_state.mjs +12 -6
- package/library/actions/extension/perps/search_markets.mjs +8 -2
- package/library/actions/extension/perps/select_activity_filter.mjs +2 -1
- package/library/actions/extension/perps/select_market_filter.mjs +75 -45
- package/library/actions/extension/perps/set_market_favorite.mjs +7 -3
- package/library/actions/extension/perps/state.mjs +2 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +3 -1
- package/library/actions/extension/perps/visible-mutation-identity.mjs +6 -2
- package/library/actions/extension/platform/cdp.mjs +50 -142
- package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
- package/library/actions/extension/swap_bridge/quote-state.mjs +56 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +5 -1
- package/library/actions/extension/swap_bridge/select_assets.mjs +12 -1
- package/library/actions/extension/swap_bridge/set_amount.mjs +11 -8
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +18 -18
- package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -9
- package/library/actions/extension/ui/locators.mjs +1 -0
- package/library/actions/extension/ui/navigate.mjs +60 -14
- 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 +4 -1
- 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 +3 -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 +4 -1
- package/library/actions/mobile/assets/set_token_visibility.mjs +15 -9
- 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 +23 -15
- package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
- package/library/actions/mobile/networks/remove_custom.mjs +7 -2
- 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 +8 -1
- 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 +3 -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 +17 -19
- 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 +28 -17
- package/library/actions/mobile/platform/observe-ui.mjs +4 -2
- 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 +3 -1
- 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 +16 -3
- 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 +34 -2
- package/library/manifests/extension.action-manifest.json +50 -17
- package/library/manifests/mobile.action-manifest.json +11 -10
- package/library/recipes/core/perps/smoke.recipe.json +23 -0
- package/library/recipes/core/perps/snapshot.recipe.json +26 -1
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
- package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
- package/library/recipes/mobile/app/lifecycle-smoke.recipe.json +89 -0
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
- package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
- package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
- package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
- package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
- package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
- package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
- package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
- package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
- package/package.json +3 -3
- package/site/assets/help-recipes.json +220 -27
- package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
- package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
- package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
- package/library/recipes/core/perps/read-markets.recipe.json +0 -36
- package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
- package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
- package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
- package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
- package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
- package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
- package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +0 -69
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
- package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
- package/library/recipes/mobile/perps/performance.recipe.json +0 -722
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
- package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
- package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
- package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
- package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
- package/library/recipes/perps/lifecycle.recipe.json +0 -144
- package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
- package/library/recipes/perps/release-watchlist.recipe.json +0 -111
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
- /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
// Implement Extension Perps state reads, UI setup and funded trade operations.
|
|
2
|
+
// Funded paths check runtime identity and retain mutation receipts for cleanup.
|
|
3
|
+
|
|
1
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
2
6
|
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
7
|
import {
|
|
4
8
|
assertTrustedExternalMutationTarget,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
} from './mutation-receipt.mjs';
|
|
15
19
|
import { readSnapshot } from './read_snapshot.mjs';
|
|
16
20
|
import { readPerpsRuntimeStateExpression } from './state.mjs';
|
|
21
|
+
import { readWalletStateExpression } from '../wallet/state.mjs';
|
|
17
22
|
import {
|
|
18
23
|
orderIdentityMatches,
|
|
19
24
|
parseOrderCard,
|
|
@@ -25,6 +30,8 @@ import {
|
|
|
25
30
|
waitForStableGlobalPerpsHome,
|
|
26
31
|
} from './visible-mutation-identity.mjs';
|
|
27
32
|
|
|
33
|
+
const ORDER_ENTRY = '[data-testid="parent-selector-perps-order-entry"], [data-testid="perps-order-entry-page"]';
|
|
34
|
+
|
|
28
35
|
function sleep(ms) {
|
|
29
36
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
37
|
}
|
|
@@ -117,6 +124,17 @@ export function positionSelectionState(input, positions) {
|
|
|
117
124
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
export function placementReceiptSelection(input) {
|
|
128
|
+
const reference = input.node?.placement_node;
|
|
129
|
+
if (reference == null) return {};
|
|
130
|
+
if (typeof reference !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/u.test(reference)
|
|
131
|
+
|| reference.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
132
|
+
throw new Error('placement_node must reference a placement node in the current recipe call.');
|
|
133
|
+
}
|
|
134
|
+
const current = String(input.context?.nodeId ?? '');
|
|
135
|
+
return { nodeId: current.slice(0, current.lastIndexOf('/') + 1) + reference };
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
async function navigatePerps(input) {
|
|
121
139
|
const selected = String(input.node?.target ?? input.node?.destination ?? (input.node?.market || input.node?.symbol ? 'market' : 'home')).toLowerCase();
|
|
122
140
|
return withExtensionPage(input, async (page) => {
|
|
@@ -135,7 +153,7 @@ async function navigatePerps(input) {
|
|
|
135
153
|
if (selected === 'trade' || selected === 'order') {
|
|
136
154
|
const side = String(input.node?.side ?? 'long').toLowerCase();
|
|
137
155
|
const navigation = await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
138
|
-
await page.waitForSelector(
|
|
156
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20000 });
|
|
139
157
|
return { action: input.action, target: selected, market: symbol, side, navigation, proofPath: 'ui-navigation' };
|
|
140
158
|
}
|
|
141
159
|
throw new Error(`Unsupported extension Perps navigation target: ${selected}`);
|
|
@@ -144,18 +162,20 @@ async function navigatePerps(input) {
|
|
|
144
162
|
|
|
145
163
|
export async function readPositions(input) {
|
|
146
164
|
return withExtensionPage(input, async (page) => {
|
|
165
|
+
const network = await assertNetwork(page, input.node?.network);
|
|
147
166
|
const state = await page.readPositions();
|
|
148
167
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot read live Perps positions.');
|
|
149
168
|
const matching = selectedItems(input, state.positions);
|
|
150
|
-
return { action: input.action, source: state.source ?? 'perps-stream-manager-cache', count: state.positions.length, matchingCount: matching.length, positions: matching.map(redactPosition) };
|
|
169
|
+
return { action: input.action, source: state.source ?? 'perps-stream-manager-cache', network, count: state.positions.length, matchingCount: matching.length, positions: matching.map(redactPosition) };
|
|
151
170
|
});
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
export async function readOrders(input) {
|
|
155
174
|
return withExtensionPage(input, async (page) => {
|
|
175
|
+
const network = await assertNetwork(page, input.node?.network);
|
|
156
176
|
const state = await readOpenOrdersState(page);
|
|
157
177
|
const matching = selectedItems(input, state.orders);
|
|
158
|
-
return { action: input.action, source: state.source, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
178
|
+
return { action: input.action, source: state.source, network, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
159
179
|
});
|
|
160
180
|
}
|
|
161
181
|
|
|
@@ -306,6 +326,50 @@ async function closeOrderThroughBackground(input, page, timeoutMs) {
|
|
|
306
326
|
};
|
|
307
327
|
}
|
|
308
328
|
|
|
329
|
+
async function readAcceptedFilledOrder(input, page, placement, requireTerminal = false) {
|
|
330
|
+
const accepted = placement.record?.acceptance;
|
|
331
|
+
if (!accepted?.venueOrderId) return null;
|
|
332
|
+
const { provider, network } = await requirePerpsTradingContext(input, page);
|
|
333
|
+
if (provider !== 'hyperliquid') throw new Error('Accepted-order status requires the original Hyperliquid provider.\nNext: mm-harness last --json');
|
|
334
|
+
const wallet = await page.evaluate(readWalletStateExpression());
|
|
335
|
+
const user = String(wallet.selectedAccount?.address ?? '').toLowerCase();
|
|
336
|
+
if (!/^0x[a-f0-9]{40}$/u.test(user)
|
|
337
|
+
|| createHash('sha256').update(user).digest('hex') !== placement.record.identity.selectedIdentitySha256) {
|
|
338
|
+
throw new Error('Accepted-order status wallet differs from the placement receipt.\nNext: mm-harness last --json');
|
|
339
|
+
}
|
|
340
|
+
const origin = network === 'testnet' ? 'https://api.hyperliquid-testnet.xyz' : 'https://api.hyperliquid.xyz';
|
|
341
|
+
const response = await fetch(`${origin}/info`, {
|
|
342
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
343
|
+
body: JSON.stringify({ type: 'orderStatus', user, oid: Number(accepted.venueOrderId) }),
|
|
344
|
+
signal: AbortSignal.timeout(10_000),
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) throw new Error('Accepted-order status request failed.\nNext: mm-harness last --json');
|
|
347
|
+
const result = await response.json();
|
|
348
|
+
if (result.status === 'unknownOid') throw new Error('Accepted venue order status is unavailable.\nNext: mm-harness last --json');
|
|
349
|
+
const order = result.order?.order;
|
|
350
|
+
if (result.status !== 'order' || String(order?.oid) !== accepted.venueOrderId
|
|
351
|
+
|| order.coin !== placement.record.identity.market
|
|
352
|
+
|| order.side !== (placement.record.identity.side === 'long' ? 'B' : 'A')
|
|
353
|
+
|| order.orderType !== 'Limit'
|
|
354
|
+
|| parseUsdBaseUnits(order.limitPx) !== accepted.limitPriceBaseUnits
|
|
355
|
+
|| Number(order.origSz) !== Number(accepted.size)) {
|
|
356
|
+
throw new Error('Venue order status differs from the exact accepted placement identity.\nNext: mm-harness last --json');
|
|
357
|
+
}
|
|
358
|
+
if (result.order.status !== 'filled') {
|
|
359
|
+
if (requireTerminal && result.order.status !== 'canceled') {
|
|
360
|
+
throw new Error('Accepted order has not reached a verified canceled or filled outcome.\nNext: mm-harness last --json');
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (!/^0(?:\.0+)?$/u.test(String(order.sz)) || !Number.isSafeInteger(result.order.statusTimestamp)
|
|
365
|
+
|| result.order.statusTimestamp <= 0) {
|
|
366
|
+
throw new Error('Venue status does not prove a complete accepted-order fill.\nNext: mm-harness last --json');
|
|
367
|
+
}
|
|
368
|
+
return { source: 'hyperliquid-orderStatus', orderId: accepted.venueOrderId, status: 'filled',
|
|
369
|
+
market: order.coin, size: String(order.origSz), remainingSize: String(order.sz),
|
|
370
|
+
statusTimestamp: result.order.statusTimestamp };
|
|
371
|
+
}
|
|
372
|
+
|
|
309
373
|
export async function closeOrders(input) {
|
|
310
374
|
requireExplicitSelection(input);
|
|
311
375
|
return withExtensionPage(input, async (page) => {
|
|
@@ -313,8 +377,10 @@ export async function closeOrders(input) {
|
|
|
313
377
|
const hasBackgroundRequest = await page.evaluate(
|
|
314
378
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
315
379
|
);
|
|
316
|
-
if (hasBackgroundRequest
|
|
317
|
-
|
|
380
|
+
if (hasBackgroundRequest && input.node?.placement_node == null) {
|
|
381
|
+
return closeOrderThroughBackground(input, page, timeoutMs);
|
|
382
|
+
}
|
|
383
|
+
if (!hasBackgroundRequest || input.node?.placement_node != null) {
|
|
318
384
|
if (input.node?.confirm_live !== true) {
|
|
319
385
|
throw new Error('metamask.perps.close_orders requires confirm_live=true for visible live cancellation.');
|
|
320
386
|
}
|
|
@@ -329,7 +395,8 @@ export async function closeOrders(input) {
|
|
|
329
395
|
const limitPrice = String(input.node?.limit_price ?? '');
|
|
330
396
|
const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
|
|
331
397
|
if (!/^(?:long|short)$/u.test(side) || !Number.isInteger(leverage)
|
|
332
|
-
|| !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(limitPrice)
|
|
398
|
+
|| !/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(limitPrice)
|
|
399
|
+
|| !(Number(limitPrice) > 0)) {
|
|
333
400
|
throw new Error('Visible Perps cancellation requires side, amount, leverage, and explicit limit_price identity.');
|
|
334
401
|
}
|
|
335
402
|
const operation = {
|
|
@@ -352,6 +419,7 @@ export async function closeOrders(input) {
|
|
|
352
419
|
input,
|
|
353
420
|
priorLookupIdentity,
|
|
354
421
|
'metamask.perps.place_order',
|
|
422
|
+
placementReceiptSelection(input),
|
|
355
423
|
);
|
|
356
424
|
const expectedOrder = {
|
|
357
425
|
market: symbol,
|
|
@@ -376,8 +444,57 @@ export async function closeOrders(input) {
|
|
|
376
444
|
&& BigInt(cleanupCandidate.observedNotionalBaseUnits) > BigInt(expectedOrder.notionalBaseUnits)
|
|
377
445
|
&& BigInt(cleanupCandidate.overageBaseUnits)
|
|
378
446
|
=== BigInt(cleanupCandidate.observedNotionalBaseUnits) - BigInt(expectedOrder.notionalBaseUnits);
|
|
379
|
-
|
|
380
|
-
|
|
447
|
+
let priorOrder = cleanupMode ? cleanupOrder : priorPlace.record?.observation?.order;
|
|
448
|
+
const accepted = priorPlace.record?.acceptance;
|
|
449
|
+
if (accepted && (accepted.limitPriceBaseUnits !== expectedOrder.limitPriceBaseUnits
|
|
450
|
+
|| priorPlace.record.identity.amountBaseUnits !== priorLookupIdentity.amountBaseUnits
|
|
451
|
+
|| priorPlace.record.identity.leverage !== leverage)) {
|
|
452
|
+
throw new Error('Cancellation parameters differ from the exact accepted placement identity.');
|
|
453
|
+
}
|
|
454
|
+
const matchesAcceptedRemainder = (order) => accepted?.status === 'resting'
|
|
455
|
+
&& order.id === `order-card-${accepted.venueOrderId}`
|
|
456
|
+
&& order.market === symbol && order.side === side && order.orderType === 'limit'
|
|
457
|
+
&& order.limitPriceBaseUnits === accepted.limitPriceBaseUnits
|
|
458
|
+
&& Number(order.size) > 0 && Number(order.size) <= Number(accepted.size);
|
|
459
|
+
const alreadyFilledResult = (filledOrder) => ({ action: input.action, canceled: false,
|
|
460
|
+
matchingCount: 0, symbols: [symbol], reason: 'already_filled', filledOrder,
|
|
461
|
+
proofPath: 'accepted-order-status' });
|
|
462
|
+
const openCancelModal = async () => {
|
|
463
|
+
const selector = `[data-testid=${JSON.stringify(priorOrder.id)}]`;
|
|
464
|
+
try {
|
|
465
|
+
await page.click(selector);
|
|
466
|
+
await page.waitForSelector(dataTestId('perps-cancel-order-modal'), { timeoutMs });
|
|
467
|
+
} catch (error) {
|
|
468
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
469
|
+
if (message.includes(`Selector not found: ${selector}`)
|
|
470
|
+
|| message.includes(`Click target readiness timed out: ${selector}`)
|
|
471
|
+
|| message.includes('ui.wait_for timed out')) {
|
|
472
|
+
const filledOrder = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
473
|
+
if (filledOrder) return filledOrder;
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
};
|
|
479
|
+
if (accepted?.status === 'filled') {
|
|
480
|
+
const filledOrder = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
481
|
+
if (filledOrder) return alreadyFilledResult(filledOrder);
|
|
482
|
+
throw new Error(`Accepted limit order ${accepted.venueOrderId} filled; do not cancel or infer ownership of a current position.`);
|
|
483
|
+
}
|
|
484
|
+
if (accepted?.status === 'resting') {
|
|
485
|
+
await requirePerpsTradingContext(input, page);
|
|
486
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
487
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
488
|
+
const exact = (await readVisibleOrderCards(page, symbol)).filter(matchesAcceptedRemainder);
|
|
489
|
+
priorOrder = exact.length === 1 ? exact[0] : null;
|
|
490
|
+
}
|
|
491
|
+
if (accepted && priorOrder?.id !== `order-card-${accepted.venueOrderId}`) {
|
|
492
|
+
const filledOrder = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
493
|
+
if (filledOrder) return alreadyFilledResult(filledOrder);
|
|
494
|
+
throw new Error('Visible order does not match the exact venue acceptance in the placement receipt.');
|
|
495
|
+
}
|
|
496
|
+
if (!priorOrder?.id || !(accepted ? matchesAcceptedRemainder(priorOrder)
|
|
497
|
+
: cleanupMode || orderIdentityMatches(priorOrder, expectedOrder))) {
|
|
381
498
|
throw new Error('Prior place receipt has no exact visible limit-order identity to cancel.');
|
|
382
499
|
}
|
|
383
500
|
const identity = trustedExternalMutationIdentity(input, page, {
|
|
@@ -393,14 +510,19 @@ export async function closeOrders(input) {
|
|
|
393
510
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
394
511
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
395
512
|
const exact = (await readVisibleOrderCards(page, symbol))
|
|
396
|
-
.filter((order) => order.id === priorOrder.id && (cleanupMode
|
|
513
|
+
.filter((order) => order.id === priorOrder.id && (accepted ? matchesAcceptedRemainder(order) : cleanupMode
|
|
397
514
|
? cleanupCandidateIdentityMatches(order, expectedOrder)
|
|
398
515
|
&& String(order.notionalBaseUnits) === String(cleanupCandidate.observedNotionalBaseUnits)
|
|
399
516
|
: orderIdentityMatches(order, priorOrder)));
|
|
400
|
-
if (exact.length !== 1)
|
|
401
|
-
|
|
402
|
-
|
|
517
|
+
if (exact.length !== 1) {
|
|
518
|
+
const filledOrder = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
519
|
+
if (filledOrder) return alreadyFilledResult(filledOrder);
|
|
520
|
+
throw new Error('Exact authorized visible order is unavailable for cancellation.');
|
|
521
|
+
}
|
|
522
|
+
const filledOrder = await openCancelModal();
|
|
523
|
+
if (filledOrder) return alreadyFilledResult(filledOrder);
|
|
403
524
|
}
|
|
525
|
+
let filledBeforeDispatch = existingReceipt?.record?.observation?.filledOrder ?? null;
|
|
404
526
|
const mutation = await runGuardedExternalMutation({
|
|
405
527
|
input,
|
|
406
528
|
identity,
|
|
@@ -413,19 +535,31 @@ export async function closeOrders(input) {
|
|
|
413
535
|
dispatch: async () => {
|
|
414
536
|
let dispatchTarget;
|
|
415
537
|
try {
|
|
416
|
-
await requireCurrentVisibleFundingBalance(input, page, timeoutMs
|
|
417
|
-
|
|
538
|
+
const currentBalance = await requireCurrentVisibleFundingBalance(input, page, timeoutMs);
|
|
539
|
+
if (currentBalance.observedBaseUnits !== fundingBalance.observedBaseUnits) {
|
|
540
|
+
filledBeforeDispatch = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
541
|
+
if (filledBeforeDispatch) return;
|
|
542
|
+
throw new Error('Current visible Perps balance changed after receipt publication.');
|
|
543
|
+
}
|
|
418
544
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
419
545
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
420
546
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
421
547
|
const exact = (await readVisibleOrderCards(page, symbol))
|
|
422
|
-
.filter((order) => order.id === priorOrder.id && (cleanupMode
|
|
548
|
+
.filter((order) => order.id === priorOrder.id && (accepted ? matchesAcceptedRemainder(order) : cleanupMode
|
|
423
549
|
? cleanupCandidateIdentityMatches(order, expectedOrder)
|
|
424
550
|
&& String(order.notionalBaseUnits) === String(cleanupCandidate.observedNotionalBaseUnits)
|
|
425
551
|
: orderIdentityMatches(order, priorOrder)));
|
|
426
|
-
if (exact.length !== 1)
|
|
427
|
-
|
|
428
|
-
|
|
552
|
+
if (exact.length !== 1) {
|
|
553
|
+
filledBeforeDispatch = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
554
|
+
if (filledBeforeDispatch) return;
|
|
555
|
+
throw new Error('Exact authorized visible order changed before cancellation.');
|
|
556
|
+
}
|
|
557
|
+
filledBeforeDispatch = await openCancelModal();
|
|
558
|
+
if (filledBeforeDispatch) return;
|
|
559
|
+
await requirePerpsTradingContext(input, page);
|
|
560
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
561
|
+
filledBeforeDispatch = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
562
|
+
if (filledBeforeDispatch) return;
|
|
429
563
|
dispatchTarget = await page.evaluate(`(() => {
|
|
430
564
|
const element = document.querySelector('[data-testid="perps-cancel-order-button"]');
|
|
431
565
|
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
@@ -435,8 +569,12 @@ export async function closeOrders(input) {
|
|
|
435
569
|
const rect = element.getBoundingClientRect();
|
|
436
570
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
437
571
|
})()`);
|
|
438
|
-
assertTrustedExternalMutationTarget(input, page, identity);
|
|
439
572
|
} catch (error) {
|
|
573
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
574
|
+
if (message.includes('Visible Perps cancel control is unavailable.')) {
|
|
575
|
+
filledBeforeDispatch = await readAcceptedFilledOrder(input, page, priorPlace);
|
|
576
|
+
if (filledBeforeDispatch) return;
|
|
577
|
+
}
|
|
440
578
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
441
579
|
}
|
|
442
580
|
await page.session.call('Input.dispatchMouseEvent', {
|
|
@@ -449,6 +587,8 @@ export async function closeOrders(input) {
|
|
|
449
587
|
});
|
|
450
588
|
},
|
|
451
589
|
observe: async () => {
|
|
590
|
+
if (filledBeforeDispatch) return { success: true, canceled: false,
|
|
591
|
+
filledOrder: filledBeforeDispatch, reason: 'already_filled', cleanup: null };
|
|
452
592
|
const deadline = Date.now() + timeoutMs;
|
|
453
593
|
let absentSince = null;
|
|
454
594
|
while (Date.now() < deadline) {
|
|
@@ -456,12 +596,17 @@ export async function closeOrders(input) {
|
|
|
456
596
|
if (!present) {
|
|
457
597
|
absentSince ??= Date.now();
|
|
458
598
|
if (Date.now() - absentSince >= 1_000) {
|
|
599
|
+
const filledOrder = await readAcceptedFilledOrder(input, page, priorPlace, true);
|
|
600
|
+
if (filledOrder) return { success: true, canceled: false,
|
|
601
|
+
filledOrder, reason: 'already_filled', cleanup: null };
|
|
459
602
|
await page.navigateHash('#/perps-home');
|
|
460
|
-
const cleanup = await waitForStableGlobalPerpsHome(page, { timeoutMs, requireZero:
|
|
603
|
+
const cleanup = await waitForStableGlobalPerpsHome(page, { timeoutMs, requireZero: !accepted });
|
|
604
|
+
if (cleanup.orderCount !== 0) throw new Error('An order remains after exact accepted-order cancellation.');
|
|
461
605
|
return {
|
|
462
606
|
success: true,
|
|
607
|
+
canceled: true,
|
|
463
608
|
canceledOrder: priorOrder,
|
|
464
|
-
cleanup: { positionCount:
|
|
609
|
+
cleanup: { positionCount: cleanup.positionCount, orderCount: 0, balanceBaseUnits: cleanup.balanceBaseUnits },
|
|
465
610
|
};
|
|
466
611
|
}
|
|
467
612
|
} else absentSince = null;
|
|
@@ -472,14 +617,15 @@ export async function closeOrders(input) {
|
|
|
472
617
|
});
|
|
473
618
|
return {
|
|
474
619
|
action: input.action,
|
|
475
|
-
canceled:
|
|
476
|
-
matchingCount: 1,
|
|
620
|
+
canceled: mutation.observation.canceled !== false,
|
|
621
|
+
matchingCount: mutation.observation.canceled === false ? 0 : 1,
|
|
477
622
|
symbols: [symbol],
|
|
478
623
|
order: priorOrder,
|
|
479
624
|
receipt: mutation.receipt,
|
|
480
625
|
resumed: mutation.resumed,
|
|
481
626
|
cleanup: mutation.observation.cleanup,
|
|
482
|
-
|
|
627
|
+
...(mutation.observation.filledOrder ? { reason: 'already_filled', filledOrder: mutation.observation.filledOrder } : {}),
|
|
628
|
+
proofPath: mutation.observation.canceled === false ? 'accepted-order-status' : 'visible-product-controls',
|
|
483
629
|
};
|
|
484
630
|
}
|
|
485
631
|
throw new Error('Visible Perps cancellation did not return a terminal result.');
|
|
@@ -848,6 +994,8 @@ function cleanupCandidateIdentityMatches(order, expected) {
|
|
|
848
994
|
}
|
|
849
995
|
|
|
850
996
|
async function observePolicyViolationCleanupCandidate(page, expected, receipt, timeoutMs) {
|
|
997
|
+
const acceptedId = receipt?.acceptance?.venueOrderId;
|
|
998
|
+
if (!acceptedId) return null;
|
|
851
999
|
const baselineIds = Array.isArray(receipt?.facts?.baselineOrderIds) ? receipt.facts.baselineOrderIds : null;
|
|
852
1000
|
if (!baselineIds || baselineIds.length !== 0) return null;
|
|
853
1001
|
const deadline = Date.now() + Math.min(timeoutMs, 10_000);
|
|
@@ -855,7 +1003,7 @@ async function observePolicyViolationCleanupCandidate(page, expected, receipt, t
|
|
|
855
1003
|
let stableSince = 0;
|
|
856
1004
|
while (Date.now() < deadline) {
|
|
857
1005
|
const candidates = (await readVisibleOrderCards(page, expected.market))
|
|
858
|
-
.filter((order) => cleanupCandidateIdentityMatches(order, expected));
|
|
1006
|
+
.filter((order) => order.id === `order-card-${acceptedId}` && cleanupCandidateIdentityMatches(order, expected));
|
|
859
1007
|
if (candidates.length > 1) throw new Error('More than one visible over-cap order matches the cleanup identity.');
|
|
860
1008
|
if (candidates.length === 1) {
|
|
861
1009
|
const key = JSON.stringify(candidates[0]);
|
|
@@ -888,7 +1036,34 @@ async function observePolicyViolationCleanupCandidate(page, expected, receipt, t
|
|
|
888
1036
|
}
|
|
889
1037
|
|
|
890
1038
|
async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs) {
|
|
891
|
-
|
|
1039
|
+
if (expected.fromCurrentForm && !receipt?.acceptance?.venueOrderId) {
|
|
1040
|
+
throw new Error('Visible submission has no accepted venue order identity; do not infer ownership from current state.');
|
|
1041
|
+
}
|
|
1042
|
+
if (expected.fromCurrentForm && expected.orderType === 'market' && receipt.acceptance.status !== 'filled') {
|
|
1043
|
+
throw new Error('Market submission has no accepted fill; its outcome requires exact-order inspection.');
|
|
1044
|
+
}
|
|
1045
|
+
if (expected.orderType === 'limit' && receipt?.acceptance?.status === 'filled') {
|
|
1046
|
+
const deadline = Date.now() + timeoutMs;
|
|
1047
|
+
let observed = false;
|
|
1048
|
+
while (Date.now() < deadline) {
|
|
1049
|
+
const state = await page.readPositions();
|
|
1050
|
+
const matching = state.positions?.filter((position) => symbolForItem(position) === expected.market);
|
|
1051
|
+
const visible = await readVisiblePositionIdentity(page);
|
|
1052
|
+
if (state.available && matching?.length === 1
|
|
1053
|
+
&& Math.abs(Number(matching[0].size ?? matching[0].szi)) === Number(receipt.acceptance.size)
|
|
1054
|
+
&& positionIdentityMatches(visible, { ...expected, matchMargin: false })
|
|
1055
|
+
&& Number.parseFloat(visible.size) === Number(receipt.acceptance.size)) {
|
|
1056
|
+
observed = true;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
await sleep(250);
|
|
1060
|
+
}
|
|
1061
|
+
if (!observed) throw new Error(`Accepted filled order ${receipt.acceptance.venueOrderId} has not reached its position read model; cleanup remains unresolved.`);
|
|
1062
|
+
throw new Error(`Accepted limit order ${receipt.acceptance.venueOrderId} filled unexpectedly; exact fill-owned position cleanup requires separate verification.`);
|
|
1063
|
+
}
|
|
1064
|
+
if (!expected.fromCurrentForm) {
|
|
1065
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(expected.market)}`);
|
|
1066
|
+
}
|
|
892
1067
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
893
1068
|
const deadline = Date.now() + timeoutMs;
|
|
894
1069
|
let stableIdentity = null;
|
|
@@ -898,6 +1073,12 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
898
1073
|
if (expected.orderType === 'market') {
|
|
899
1074
|
const position = await readVisiblePositionIdentity(page);
|
|
900
1075
|
let observedNotionalBaseUnits = null;
|
|
1076
|
+
if (expected.fromCurrentForm) {
|
|
1077
|
+
const filledNotional = Math.ceil(Number(receipt.acceptance.filledSize) * Number(receipt.acceptance.averagePrice) * 1_000_000);
|
|
1078
|
+
if (!Number.isSafeInteger(filledNotional) || BigInt(filledNotional) > BigInt(expected.maximumNotionalBaseUnits)) {
|
|
1079
|
+
throw new Error('Accepted market fill exceeds the trusted operation cap.');
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
901
1082
|
if (position) {
|
|
902
1083
|
const size = Number(String(position.size).replace(/,/gu, '').match(/[0-9]+(?:\.[0-9]+)?/u)?.[0]);
|
|
903
1084
|
const price = await readCurrentMarketPrice(page, expected.market);
|
|
@@ -920,7 +1101,9 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
920
1101
|
observedNotionalBaseUnits,
|
|
921
1102
|
} : null,
|
|
922
1103
|
};
|
|
923
|
-
|
|
1104
|
+
const exactFilledSize = !expected.fromCurrentForm || (Number.parseFloat(String(position?.size ?? '').replace(/,/gu, '')) === Number(expected.size)
|
|
1105
|
+
&& Number(receipt.acceptance.filledSize) === Number(expected.size));
|
|
1106
|
+
if (observedNotionalBaseUnits && positionIdentityMatches(position, expected) && exactFilledSize) {
|
|
924
1107
|
const key = JSON.stringify(position);
|
|
925
1108
|
if (key !== stableIdentity) {
|
|
926
1109
|
stableIdentity = key;
|
|
@@ -932,7 +1115,8 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
932
1115
|
observedNotionalBaseUnits,
|
|
933
1116
|
maximumNotionalBaseUnits: expected.maximumNotionalBaseUnits,
|
|
934
1117
|
order: null,
|
|
935
|
-
orderId: null,
|
|
1118
|
+
orderId: receipt?.acceptance?.venueOrderId ?? null,
|
|
1119
|
+
...(expected.fromCurrentForm ? { filledSize: receipt.acceptance.filledSize, averagePrice: receipt.acceptance.averagePrice } : {}),
|
|
936
1120
|
};
|
|
937
1121
|
}
|
|
938
1122
|
} else {
|
|
@@ -956,7 +1140,10 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
956
1140
|
})),
|
|
957
1141
|
};
|
|
958
1142
|
const matching = orders
|
|
959
|
-
.filter((order) => !baselineIds.has(order.id)
|
|
1143
|
+
.filter((order) => !baselineIds.has(order.id)
|
|
1144
|
+
&& (!receipt?.acceptance || order.id === `order-card-${receipt.acceptance.venueOrderId}`)
|
|
1145
|
+
&& (expected.size == null || Number(order.size) === Number(expected.size))
|
|
1146
|
+
&& orderIdentityMatches(order, expected));
|
|
960
1147
|
if (matching.length > 1) throw new Error('More than one new visible order matches the authorized identity.');
|
|
961
1148
|
if (matching.length === 1) {
|
|
962
1149
|
const key = JSON.stringify(matching[0]);
|
|
@@ -1085,12 +1272,28 @@ export async function requirePerpsTradingContext(input, page) {
|
|
|
1085
1272
|
}
|
|
1086
1273
|
const actualProvider = String(state.activeProvider || '').toLowerCase();
|
|
1087
1274
|
const actualNetwork = state.isTestnet === true ? 'testnet' : 'mainnet';
|
|
1275
|
+
const trusted = input.context?.trustedMutation;
|
|
1276
|
+
if (trusted?.proofLane === 'source-dev') {
|
|
1277
|
+
if (state.providerKnown !== true || provider !== 'hyperliquid' || network !== 'testnet'
|
|
1278
|
+
|| actualProvider !== 'hyperliquid' || actualNetwork !== 'testnet') {
|
|
1279
|
+
throw new Error('Source-dev funded mutations require independently verified Hyperliquid testnet.');
|
|
1280
|
+
}
|
|
1281
|
+
const wallet = await page.evaluate(readWalletStateExpression());
|
|
1282
|
+
const address = String(wallet?.selectedAccount?.address ?? '').toLowerCase();
|
|
1283
|
+
if (!/^0x[0-9a-f]{40}$/u.test(address)
|
|
1284
|
+
|| createHash('sha256').update(address).digest('hex') !== trusted.selectedIdentitySha256) {
|
|
1285
|
+
throw new Error('Source-dev funded mutation selected account changed after its visible identity proof.');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1088
1288
|
if (actualProvider !== provider || actualNetwork !== network) {
|
|
1089
1289
|
throw new Error(
|
|
1090
1290
|
`Perps mutation expected ${provider}/${network}, got ${actualProvider || 'unknown'}/${actualNetwork}.`,
|
|
1091
1291
|
);
|
|
1092
1292
|
}
|
|
1093
|
-
return {
|
|
1293
|
+
return {
|
|
1294
|
+
provider, network,
|
|
1295
|
+
...(trusted?.proofLane === 'source-dev' ? { selectedIdentitySha256: trusted.selectedIdentitySha256 } : {}),
|
|
1296
|
+
};
|
|
1094
1297
|
}
|
|
1095
1298
|
|
|
1096
1299
|
export async function requireCurrentVisibleFundingBalance(input, page, timeoutMs, expectedBaseUnits) {
|
|
@@ -1137,7 +1340,7 @@ async function requireCurrentOrderEntryFundingBalance(input, page, expectedBaseU
|
|
|
1137
1340
|
if (BigInt(observedBaseUnits) < BigInt(requirement.minBaseUnits)) {
|
|
1138
1341
|
throw new Error('Current visible Perps balance is below the runner-trusted funding minimum.');
|
|
1139
1342
|
}
|
|
1140
|
-
if (observedBaseUnits !== expectedBaseUnits) {
|
|
1343
|
+
if (expectedBaseUnits != null && observedBaseUnits !== expectedBaseUnits) {
|
|
1141
1344
|
throw new Error('Current visible Perps balance changed after receipt publication.');
|
|
1142
1345
|
}
|
|
1143
1346
|
return {
|
|
@@ -1149,6 +1352,205 @@ async function requireCurrentOrderEntryFundingBalance(input, page, expectedBaseU
|
|
|
1149
1352
|
};
|
|
1150
1353
|
}
|
|
1151
1354
|
|
|
1355
|
+
async function readPreparedOrderConfiguration(page) {
|
|
1356
|
+
return page.evaluate(`(() => {
|
|
1357
|
+
const value = (testId) => document.querySelector('[data-testid="' + testId + '"] input')?.value ?? null;
|
|
1358
|
+
const submit = document.querySelector('[data-testid="submit-order-button"]');
|
|
1359
|
+
const direction = String(submit?.innerText || '').match(/\\b(long|short)\\b/iu)?.[1]?.toLowerCase() ?? null;
|
|
1360
|
+
return {
|
|
1361
|
+
route: String(location.hash),
|
|
1362
|
+
direction,
|
|
1363
|
+
autoCloseEnabled: Boolean(document.querySelector('input[data-testid="auto-close-toggle"]')?.checked),
|
|
1364
|
+
takeProfitPrice: value('tp-price-input'),
|
|
1365
|
+
stopLossPrice: value('sl-price-input'),
|
|
1366
|
+
takeProfitPercent: value('tp-percent-input'),
|
|
1367
|
+
stopLossPercent: value('sl-percent-input'),
|
|
1368
|
+
};
|
|
1369
|
+
})()`);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function readPreparedOrderSizing(page, expected, sizeDecimals) {
|
|
1373
|
+
if (!expected.reduceOnly && expected.leverage !== 1) {
|
|
1374
|
+
throw new Error('Prepared visible order sizing currently requires 1x leverage.\nNext: mm-harness actions --action metamask.perps.place_order --adapter extension --json');
|
|
1375
|
+
}
|
|
1376
|
+
const visible = await page.evaluate(`(() => {
|
|
1377
|
+
const element = document.querySelector(${JSON.stringify(expected.reduceOnly ? '[data-testid="perps-market-detail-price"]' : '[data-testid="perps-order-entry-price"]')});
|
|
1378
|
+
const rect = element?.getBoundingClientRect();
|
|
1379
|
+
const slippage = document.querySelector('[data-testid="perps-order-summary-slippage-value"]');
|
|
1380
|
+
return { priceText: rect?.width && rect?.height ? element.textContent.trim() : null,
|
|
1381
|
+
slippageText: slippage ? slippage.textContent.trim() : null };
|
|
1382
|
+
})()`);
|
|
1383
|
+
const { priceText } = visible;
|
|
1384
|
+
const match = /^\$([0-9][0-9,]*)(?:\.([0-9]+))?$/u.exec(String(priceText));
|
|
1385
|
+
if (!match || !Number.isInteger(sizeDecimals) || sizeDecimals < 0 || sizeDecimals > 8) {
|
|
1386
|
+
throw new Error('Prepared limit sizing requires the visible current price and valid venue lot precision.');
|
|
1387
|
+
}
|
|
1388
|
+
const price = Number(`${match[1].replace(/,/gu, '')}.${match[2] ?? '0'}`);
|
|
1389
|
+
const priceHalfTick = 0.5 * 10 ** -(match[2]?.length ?? 0);
|
|
1390
|
+
const multiplier = 10 ** sizeDecimals;
|
|
1391
|
+
if (!(price > priceHalfTick)) throw new Error('Visible order price must be positive at its displayed precision.');
|
|
1392
|
+
let size;
|
|
1393
|
+
if (expected.reduceOnly) {
|
|
1394
|
+
const match = /^(?:0|[1-9][0-9]{0,19})(?:\.([0-9]{1,8}))?$/u.exec(String(expected.size));
|
|
1395
|
+
if (!match || !(Number(expected.size) > 0) || (match[1] ?? '').replace(/0+$/u, '').length > sizeDecimals) {
|
|
1396
|
+
throw new Error('Full close size does not match the venue lot precision.');
|
|
1397
|
+
}
|
|
1398
|
+
size = String(expected.size);
|
|
1399
|
+
} else {
|
|
1400
|
+
const usd = Number(expected.amountBaseUnits) / 1_000_000;
|
|
1401
|
+
const lotsAt = (currentPrice) => Math.ceil(usd / currentPrice * multiplier);
|
|
1402
|
+
const lowerLots = lotsAt(price + priceHalfTick);
|
|
1403
|
+
const upperLots = lotsAt(price - priceHalfTick);
|
|
1404
|
+
if (lowerLots !== upperLots || !Number.isSafeInteger(lowerLots) || lowerLots <= 0) {
|
|
1405
|
+
throw new Error('Displayed price precision cannot determine one exact limit-order size.');
|
|
1406
|
+
}
|
|
1407
|
+
size = (lowerLots / multiplier).toFixed(sizeDecimals);
|
|
1408
|
+
}
|
|
1409
|
+
if (expected.orderType === 'market') {
|
|
1410
|
+
const maxPercent = expected.reduceOnly || visible.slippageText === null ? 3
|
|
1411
|
+
: Number(/\bMax:\s*([0-9]+(?:\.[0-9]+)?)%/u.exec(visible.slippageText)?.[1]);
|
|
1412
|
+
const slippageBps = Math.round(maxPercent * 100);
|
|
1413
|
+
if (!Number.isInteger(slippageBps) || maxPercent !== slippageBps / 100
|
|
1414
|
+
|| slippageBps < 0 || slippageBps >= 10_000 || sizeDecimals > 6) {
|
|
1415
|
+
throw new Error('Prepared market order has no valid displayed maximum slippage.');
|
|
1416
|
+
}
|
|
1417
|
+
const factor = 1 + (expected.side === 'long' ? 1 : -1) * slippageBps / 10_000;
|
|
1418
|
+
const formatPrice = (value) => {
|
|
1419
|
+
if (Number.isInteger(value)) return value;
|
|
1420
|
+
const rounded = Number(value.toFixed(6 - sizeDecimals));
|
|
1421
|
+
const decimals = Math.max(0, 5 - String(Math.trunc(rounded)).replace(/^0+/u, '').length);
|
|
1422
|
+
return Number(rounded.toFixed(decimals));
|
|
1423
|
+
};
|
|
1424
|
+
const minimumPriceBaseUnits = parseUsdBaseUnits(String(formatPrice((price - priceHalfTick) * factor)));
|
|
1425
|
+
const maximumPriceBaseUnits = parseUsdBaseUnits(String(formatPrice((price + priceHalfTick) * factor)));
|
|
1426
|
+
if (!minimumPriceBaseUnits || !maximumPriceBaseUnits || BigInt(minimumPriceBaseUnits) <= 0n) {
|
|
1427
|
+
throw new Error('Prepared market price cap could not be resolved at venue precision.');
|
|
1428
|
+
}
|
|
1429
|
+
const maximumExposure = Math.ceil(Number(size) * Math.max(price + priceHalfTick, Number(maximumPriceBaseUnits) / 1_000_000) * 1_000_000);
|
|
1430
|
+
if (!Number.isSafeInteger(maximumExposure) || BigInt(maximumExposure) > BigInt(expected.maximumNotionalBaseUnits)) {
|
|
1431
|
+
throw new Error('Prepared market size and slippage cap exceed the trusted operation cap.');
|
|
1432
|
+
}
|
|
1433
|
+
return { priceText, sizeDecimals, size, slippageBps, minimumPriceBaseUnits, maximumPriceBaseUnits };
|
|
1434
|
+
}
|
|
1435
|
+
const notionalBaseUnits = `${Math.round(Number(size) * Number(expected.limitPriceBaseUnits))}`;
|
|
1436
|
+
const maximumCurrentNotionalBaseUnits = Math.ceil(Number(size) * (price + priceHalfTick) * 1_000_000);
|
|
1437
|
+
if (!Number.isSafeInteger(maximumCurrentNotionalBaseUnits)
|
|
1438
|
+
|| BigInt(maximumCurrentNotionalBaseUnits) > BigInt(expected.maximumNotionalBaseUnits)
|
|
1439
|
+
|| BigInt(notionalBaseUnits) > BigInt(expected.maximumNotionalBaseUnits)) {
|
|
1440
|
+
throw new Error('Rounded prepared limit exposure exceeds the trusted operation cap.');
|
|
1441
|
+
}
|
|
1442
|
+
return { priceText, sizeDecimals, size, notionalBaseUnits };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
export async function observeHyperliquidSubmission(page, expected) {
|
|
1446
|
+
if (expected.provider !== 'hyperliquid' || !['market', 'limit'].includes(expected.orderType)) {
|
|
1447
|
+
throw new Error('Exact visible submission observation requires a Hyperliquid market or limit order.');
|
|
1448
|
+
}
|
|
1449
|
+
const origin = expected.network === 'testnet'
|
|
1450
|
+
? 'https://api.hyperliquid-testnet.xyz' : 'https://api.hyperliquid.xyz';
|
|
1451
|
+
const metadataResponse = await fetch(`${origin}/info`, {
|
|
1452
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1453
|
+
body: JSON.stringify({ type: 'meta' }), signal: AbortSignal.timeout(10_000),
|
|
1454
|
+
});
|
|
1455
|
+
if (!metadataResponse.ok) throw new Error('Hyperliquid market metadata is unavailable before submission.');
|
|
1456
|
+
const metadata = await metadataResponse.json();
|
|
1457
|
+
const asset = metadata.universe?.findIndex((market) => market.name === expected.market);
|
|
1458
|
+
if (!Number.isInteger(asset) || asset < 0) throw new Error('Hyperliquid market has no exact asset identity.');
|
|
1459
|
+
const sizing = await readPreparedOrderSizing(page, expected, metadata.universe[asset].szDecimals);
|
|
1460
|
+
const { session, targetId } = await page.connectServiceWorker();
|
|
1461
|
+
let armed = false;
|
|
1462
|
+
let requestId = null;
|
|
1463
|
+
let requestFacts = null;
|
|
1464
|
+
let acceptance = null;
|
|
1465
|
+
let failure = null;
|
|
1466
|
+
let pending = null;
|
|
1467
|
+
const unsubscribe = [];
|
|
1468
|
+
const close = () => {
|
|
1469
|
+
armed = false;
|
|
1470
|
+
for (const stop of unsubscribe) stop();
|
|
1471
|
+
session.close();
|
|
1472
|
+
};
|
|
1473
|
+
try {
|
|
1474
|
+
unsubscribe.push(session.on('Network.requestWillBeSent', (event) => {
|
|
1475
|
+
if (!armed || event.request?.url !== `${origin}/exchange` || event.request.method !== 'POST') return;
|
|
1476
|
+
try {
|
|
1477
|
+
const body = event.request.postData;
|
|
1478
|
+
if (typeof body !== 'string' || body.length > 131_072) throw new Error();
|
|
1479
|
+
const action = JSON.parse(body).action;
|
|
1480
|
+
if (action?.type !== 'order') return;
|
|
1481
|
+
if (requestId !== null || !Array.isArray(action.orders) || action.orders.length !== 1) throw new Error();
|
|
1482
|
+
const order = action.orders[0];
|
|
1483
|
+
const wirePrice = parseUsdBaseUnits(order.p);
|
|
1484
|
+
const priceMatches = expected.orderType === 'limit'
|
|
1485
|
+
? wirePrice === expected.limitPriceBaseUnits
|
|
1486
|
+
: wirePrice !== null && BigInt(wirePrice) >= BigInt(sizing.minimumPriceBaseUnits)
|
|
1487
|
+
&& BigInt(wirePrice) <= BigInt(sizing.maximumPriceBaseUnits);
|
|
1488
|
+
if (order.a !== asset || order.b !== (expected.side === 'long') || order.r !== Boolean(expected.reduceOnly)
|
|
1489
|
+
|| order.t?.limit?.tif !== (expected.orderType === 'limit' ? 'Gtc' : 'FrontendMarket')
|
|
1490
|
+
|| !priceMatches
|
|
1491
|
+
|| !/^(?:0|[1-9][0-9]{0,19})(?:\.[0-9]{1,8})?$/u.test(String(order.s))
|
|
1492
|
+
|| Number(order.s) !== Number(sizing.size)) throw new Error();
|
|
1493
|
+
requestId = String(event.requestId);
|
|
1494
|
+
requestFacts = { asset, size: String(order.s), limitPriceBaseUnits: wirePrice };
|
|
1495
|
+
} catch {
|
|
1496
|
+
failure = new Error('Hyperliquid submission request is missing, ambiguous, or differs from the authorized form.');
|
|
1497
|
+
}
|
|
1498
|
+
}));
|
|
1499
|
+
unsubscribe.push(session.on('Network.loadingFailed', (event) => {
|
|
1500
|
+
if (event.requestId === requestId) failure = new Error('Hyperliquid submission response was not delivered.');
|
|
1501
|
+
}));
|
|
1502
|
+
unsubscribe.push(session.on('Network.loadingFinished', (event) => {
|
|
1503
|
+
if (event.requestId !== requestId || pending) return;
|
|
1504
|
+
pending = (async () => {
|
|
1505
|
+
try {
|
|
1506
|
+
const response = await session.call('Network.getResponseBody', { requestId }, { timeoutMs: 10_000 });
|
|
1507
|
+
if (response.base64Encoded || typeof response.body !== 'string' || response.body.length > 32_768) throw new Error();
|
|
1508
|
+
const result = JSON.parse(response.body);
|
|
1509
|
+
const statuses = result.response?.data?.statuses;
|
|
1510
|
+
if (result.status !== 'ok' || result.response?.type !== 'order' || !Array.isArray(statuses) || statuses.length !== 1) throw new Error();
|
|
1511
|
+
const status = statuses[0];
|
|
1512
|
+
const kind = status.resting ? 'resting' : status.filled ? 'filled' : null;
|
|
1513
|
+
const order = kind && status[kind];
|
|
1514
|
+
if (!kind || !Number.isSafeInteger(order.oid) || order.oid <= 0) throw new Error();
|
|
1515
|
+
acceptance = {
|
|
1516
|
+
venueOrderId: String(order.oid), status: kind,
|
|
1517
|
+
requestId, serviceWorkerTargetId: targetId,
|
|
1518
|
+
...requestFacts,
|
|
1519
|
+
...(expected.orderType === 'market' && kind === 'filled'
|
|
1520
|
+
? { filledSize: String(order.totalSz), averagePrice: String(order.avgPx) } : {}),
|
|
1521
|
+
};
|
|
1522
|
+
} catch {
|
|
1523
|
+
failure = new Error('Hyperliquid response has no unambiguous accepted venue order identity.');
|
|
1524
|
+
}
|
|
1525
|
+
})();
|
|
1526
|
+
}));
|
|
1527
|
+
await session.call('Network.enable', { maxPostDataSize: 131_072 }, { timeoutMs: 5_000 });
|
|
1528
|
+
return {
|
|
1529
|
+
sizing,
|
|
1530
|
+
async checkSizing() {
|
|
1531
|
+
const current = await readPreparedOrderSizing(page, expected, sizing.sizeDecimals);
|
|
1532
|
+
if (Number(current.size) !== Number(sizing.size)) throw new Error('Prepared limit lot size changed before dispatch.');
|
|
1533
|
+
if (expected.orderType === 'market' && (current.slippageBps !== sizing.slippageBps
|
|
1534
|
+
|| current.minimumPriceBaseUnits !== sizing.minimumPriceBaseUnits || current.maximumPriceBaseUnits !== sizing.maximumPriceBaseUnits)) {
|
|
1535
|
+
throw new Error('Prepared market price or slippage changed before dispatch.');
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
arm() { armed = true; },
|
|
1539
|
+
async accepted(timeoutMs) {
|
|
1540
|
+
const deadline = Date.now() + timeoutMs;
|
|
1541
|
+
while (!failure && !acceptance && Date.now() < deadline) await sleep(50);
|
|
1542
|
+
if (failure) throw failure;
|
|
1543
|
+
if (!acceptance) throw new Error('Hyperliquid submission acceptance was not observed before timeout.');
|
|
1544
|
+
return acceptance;
|
|
1545
|
+
},
|
|
1546
|
+
close,
|
|
1547
|
+
};
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
close();
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1152
1554
|
export async function placeOrder(input) {
|
|
1153
1555
|
requirePlaceOrderInputs(input);
|
|
1154
1556
|
const symbol = marketSymbol(input);
|
|
@@ -1159,6 +1561,10 @@ export async function placeOrder(input) {
|
|
|
1159
1561
|
const orderType = resolveOrderType(input);
|
|
1160
1562
|
const positionMode = String(input.node?.position_mode ?? 'new').toLowerCase();
|
|
1161
1563
|
const submitAttempts = Number(input.node?.submit_attempts ?? 1);
|
|
1564
|
+
const fromCurrentForm = input.node?.from_current_form === true;
|
|
1565
|
+
if (fromCurrentForm && (positionMode !== 'new' || submitAttempts !== 1)) {
|
|
1566
|
+
throw new Error('Prepared-form placement requires position_mode=new and submit_attempts=1.');
|
|
1567
|
+
}
|
|
1162
1568
|
const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
|
|
1163
1569
|
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(roundingToleranceBaseUnits)
|
|
1164
1570
|
|| BigInt(roundingToleranceBaseUnits) > 1_000_000n) {
|
|
@@ -1197,6 +1603,7 @@ export async function placeOrder(input) {
|
|
|
1197
1603
|
throw new Error('Requested Perps order exposure exceeds the runner-trusted operation cap.');
|
|
1198
1604
|
}
|
|
1199
1605
|
const expectedVisibleIdentity = {
|
|
1606
|
+
fromCurrentForm,
|
|
1200
1607
|
market: symbol,
|
|
1201
1608
|
side,
|
|
1202
1609
|
leverage,
|
|
@@ -1209,11 +1616,12 @@ export async function placeOrder(input) {
|
|
|
1209
1616
|
...(orderType === 'market' ? {
|
|
1210
1617
|
maximumNotionalBaseUnits: placementBudget.maxBaseUnits,
|
|
1211
1618
|
} : {}),
|
|
1619
|
+
...(fromCurrentForm && orderType === 'limit' ? { maximumNotionalBaseUnits: placementBudget.maxBaseUnits } : {}),
|
|
1212
1620
|
};
|
|
1213
1621
|
const hasBackgroundRequest = await page.evaluate(
|
|
1214
1622
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
1215
1623
|
);
|
|
1216
|
-
if (hasBackgroundRequest) {
|
|
1624
|
+
if (hasBackgroundRequest && !fromCurrentForm) {
|
|
1217
1625
|
await requirePerpsTradingContext(input, page);
|
|
1218
1626
|
const resolvedPrice = await readCurrentMarketPrice(
|
|
1219
1627
|
page,
|
|
@@ -1242,8 +1650,15 @@ export async function placeOrder(input) {
|
|
|
1242
1650
|
}
|
|
1243
1651
|
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
1244
1652
|
if (existingReceipt) {
|
|
1653
|
+
const recordedSizing = orderType === 'market' ? existingReceipt.record?.facts?.marketSizing : existingReceipt.record?.facts?.limitSizing;
|
|
1654
|
+
if (fromCurrentForm && recordedSizing) {
|
|
1655
|
+
expectedVisibleIdentity.size = recordedSizing.size;
|
|
1656
|
+
if (orderType === 'limit') expectedVisibleIdentity.notionalBaseUnits = recordedSizing.notionalBaseUnits;
|
|
1657
|
+
}
|
|
1245
1658
|
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
1246
|
-
|
|
1659
|
+
if (!fromCurrentForm) {
|
|
1660
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
1661
|
+
}
|
|
1247
1662
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
1248
1663
|
if (positionMode !== 'new') {
|
|
1249
1664
|
throw new Error('resumed funded position modification requires manual visible inspection');
|
|
@@ -1316,25 +1731,40 @@ export async function placeOrder(input) {
|
|
|
1316
1731
|
openOrderCount: mutation.observation.order ? 1 : null,
|
|
1317
1732
|
configured: null,
|
|
1318
1733
|
outcome: mutation.observation,
|
|
1734
|
+
...(fromCurrentForm && orderType === 'market' ? { venueOrderId: mutation.observation.orderId } : {}),
|
|
1319
1735
|
proofPath: 'visible-product-controls',
|
|
1320
1736
|
};
|
|
1321
1737
|
});
|
|
1322
1738
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1739
|
+
let initialClean;
|
|
1740
|
+
let fundingBalance;
|
|
1741
|
+
if (fromCurrentForm) {
|
|
1742
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20_000 });
|
|
1743
|
+
const [positions, orders] = await Promise.all([page.readPositions(), readOpenOrders(page)]);
|
|
1744
|
+
if (!positions.available || positions.positions.length !== 0 || orders.length !== 0) {
|
|
1745
|
+
throw new Error('Prepared-form placement requires an independently verified empty position/order baseline.');
|
|
1746
|
+
}
|
|
1747
|
+
initialClean = { positionCount: 0, orderCount: 0 };
|
|
1748
|
+
fundingBalance = await requireCurrentOrderEntryFundingBalance(input, page);
|
|
1749
|
+
} else {
|
|
1750
|
+
await page.navigateHash('#/perps-home');
|
|
1751
|
+
initialClean = await waitForStableGlobalPerpsHome(page, {
|
|
1752
|
+
timeoutMs: Number(input.node?.timeout_ms ?? 45_000),
|
|
1753
|
+
requireZero: true,
|
|
1754
|
+
});
|
|
1755
|
+
fundingBalance = await requireCurrentVisibleFundingBalance(
|
|
1756
|
+
input,
|
|
1757
|
+
page,
|
|
1758
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
1759
|
+
initialClean.balanceBaseUnits,
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1334
1762
|
const tradingContext = await requirePerpsTradingContext(input, page);
|
|
1335
1763
|
const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
|
|
1336
|
-
|
|
1337
|
-
|
|
1764
|
+
if (!fromCurrentForm) {
|
|
1765
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
1766
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
1767
|
+
}
|
|
1338
1768
|
const resolvedPrice = await readCurrentMarketPrice(
|
|
1339
1769
|
page,
|
|
1340
1770
|
symbol,
|
|
@@ -1343,19 +1773,21 @@ export async function placeOrder(input) {
|
|
|
1343
1773
|
const limitPrice = orderType === 'limit'
|
|
1344
1774
|
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
1345
1775
|
: null;
|
|
1346
|
-
const baselineOrderIds = orderType === 'limit'
|
|
1776
|
+
const baselineOrderIds = orderType === 'limit' && !fromCurrentForm
|
|
1347
1777
|
? (await readVisibleOrderCards(page, symbol)).map((order) => order.id)
|
|
1348
1778
|
: [];
|
|
1349
1779
|
const initialOpenOrderCount = baselineOrderIds.length;
|
|
1350
|
-
|
|
1351
|
-
|
|
1780
|
+
if (!fromCurrentForm) {
|
|
1781
|
+
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=${positionMode}`);
|
|
1782
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20000 });
|
|
1783
|
+
}
|
|
1352
1784
|
const lateBackgroundRequest = await page.evaluate(
|
|
1353
1785
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
1354
1786
|
);
|
|
1355
|
-
if (lateBackgroundRequest) {
|
|
1787
|
+
if (lateBackgroundRequest && !fromCurrentForm) {
|
|
1356
1788
|
throw new Error('Background Perps placement appeared after visible setup; refusing an ambiguous dispatch.');
|
|
1357
1789
|
}
|
|
1358
|
-
if (!lateBackgroundRequest) {
|
|
1790
|
+
if (!lateBackgroundRequest || fromCurrentForm) {
|
|
1359
1791
|
if (input.node?.confirm_live !== true) {
|
|
1360
1792
|
throw new Error('metamask.perps.place_order requires confirm_live=true for visible live submission.');
|
|
1361
1793
|
}
|
|
@@ -1363,9 +1795,11 @@ export async function placeOrder(input) {
|
|
|
1363
1795
|
if (submitAttempts !== 1) {
|
|
1364
1796
|
throw new Error('Visible funded Perps submission requires submit_attempts=1.');
|
|
1365
1797
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1798
|
+
if (!fromCurrentForm) {
|
|
1799
|
+
await page.setInput(`${dataTestId('amount-input-field')} input`, amount);
|
|
1800
|
+
await page.setInput(`${dataTestId('leverage-input')} input`, String(leverage));
|
|
1801
|
+
}
|
|
1802
|
+
if (orderType === 'limit' && !fromCurrentForm) {
|
|
1369
1803
|
await page.click(dataTestId('order-type-limit'));
|
|
1370
1804
|
const limitInput = `${dataTestId('limit-price-input')} input`;
|
|
1371
1805
|
await page.waitForSelector(limitInput, { timeoutMs: 10000 });
|
|
@@ -1379,6 +1813,7 @@ export async function placeOrder(input) {
|
|
|
1379
1813
|
const submit = document.querySelector('[data-testid="submit-order-button"]');
|
|
1380
1814
|
return {
|
|
1381
1815
|
amount: value('amount-input-field'),
|
|
1816
|
+
denomination: document.querySelector('[data-testid="amount-input-denomination-unit"]')?.textContent?.trim() ?? null,
|
|
1382
1817
|
leverage: value('leverage-input'),
|
|
1383
1818
|
limitPrice: limit,
|
|
1384
1819
|
limitSelected: document.querySelector('[data-testid="order-type-limit"]')?.getAttribute('aria-pressed') === 'true',
|
|
@@ -1391,19 +1826,42 @@ export async function placeOrder(input) {
|
|
|
1391
1826
|
if (Number(configured.amount) !== Number(amount) || Number(configured.leverage) !== leverage) {
|
|
1392
1827
|
throw new Error('Visible Perps order inputs did not retain the requested values.');
|
|
1393
1828
|
}
|
|
1829
|
+
if (configured.denomination !== null && configured.denomination !== 'USD') {
|
|
1830
|
+
throw new Error('Visible Perps amount must be denominated in USD before funded submission.');
|
|
1831
|
+
}
|
|
1394
1832
|
if (orderType === 'limit' && (!configured.limitSelected || Number(configured.limitPrice) !== limitPrice)) {
|
|
1395
1833
|
throw new Error('Visible Perps limit inputs did not retain the requested values.');
|
|
1396
1834
|
}
|
|
1397
1835
|
if (!configured.submitEnabled) throw new Error('Visible Perps order is not submittable.');
|
|
1398
|
-
const
|
|
1836
|
+
const preparedConfiguration = fromCurrentForm ? await readPreparedOrderConfiguration(page) : null;
|
|
1837
|
+
if (preparedConfiguration && (preparedConfiguration.route.split('?')[0] !== `#/perps/trade/${encodeURIComponent(symbol)}`
|
|
1838
|
+
|| preparedConfiguration.direction !== side)) {
|
|
1839
|
+
throw new Error('Prepared Perps form does not match the requested market and direction.');
|
|
1840
|
+
}
|
|
1841
|
+
if (fromCurrentForm && orderType === 'market' && preparedConfiguration.autoCloseEnabled) {
|
|
1842
|
+
throw new Error('Receipt-bound market submission requires protection disabled before proof.');
|
|
1843
|
+
}
|
|
1844
|
+
const submissionObserver = fromCurrentForm
|
|
1845
|
+
? await observeHyperliquidSubmission(page, { ...expectedVisibleIdentity, ...tradingContext })
|
|
1846
|
+
: null;
|
|
1847
|
+
if (submissionObserver) {
|
|
1848
|
+
expectedVisibleIdentity.size = submissionObserver.sizing.size;
|
|
1849
|
+
if (orderType === 'limit') expectedVisibleIdentity.notionalBaseUnits = submissionObserver.sizing.notionalBaseUnits;
|
|
1850
|
+
}
|
|
1851
|
+
let mutation;
|
|
1852
|
+
try {
|
|
1853
|
+
mutation = await runGuardedExternalMutation({
|
|
1399
1854
|
input,
|
|
1400
1855
|
identity,
|
|
1401
|
-
publicationFacts: {
|
|
1856
|
+
publicationFacts: {
|
|
1857
|
+
balanceBeforeBaseUnits, baselineOrderIds, fundingBalance, tradingContext,
|
|
1858
|
+
...(preparedConfiguration ? { preparedConfiguration } : {}),
|
|
1859
|
+
...(submissionObserver ? { [orderType === 'market' ? 'marketSizing' : 'limitSizing']: submissionObserver.sizing } : {}),
|
|
1860
|
+
},
|
|
1402
1861
|
dispatch: async () => {
|
|
1403
1862
|
let dispatchTarget;
|
|
1404
1863
|
try {
|
|
1405
1864
|
await requireCurrentOrderEntryFundingBalance(input, page, fundingBalance.observedBaseUnits);
|
|
1406
|
-
await requirePerpsTradingContext(input, page);
|
|
1407
1865
|
const currentConfiguration = await page.evaluate(`(() => {
|
|
1408
1866
|
const value = (testId) => document.querySelector('[data-testid="' + testId + '"] input')?.value ?? null;
|
|
1409
1867
|
const limitSelected = document.querySelector('[data-testid="order-type-limit"]')?.getAttribute('aria-pressed') === 'true';
|
|
@@ -1415,24 +1873,32 @@ export async function placeOrder(input) {
|
|
|
1415
1873
|
const rect = element.getBoundingClientRect();
|
|
1416
1874
|
return {
|
|
1417
1875
|
amount: value('amount-input-field'),
|
|
1876
|
+
denomination: document.querySelector('[data-testid="amount-input-denomination-unit"]')?.textContent?.trim() ?? null,
|
|
1418
1877
|
leverage: value('leverage-input'),
|
|
1419
1878
|
limitPrice: value('limit-price-input'),
|
|
1420
1879
|
orderType: limitSelected ? 'limit' : 'market',
|
|
1421
1880
|
target: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
1422
1881
|
};
|
|
1423
1882
|
})()`);
|
|
1424
|
-
if (currentConfiguration.amount !==
|
|
1425
|
-
|| currentConfiguration.
|
|
1883
|
+
if (parseUsdBaseUnits(currentConfiguration.amount) !== identity.amountBaseUnits
|
|
1884
|
+
|| (currentConfiguration.denomination !== null && currentConfiguration.denomination !== 'USD')
|
|
1885
|
+
|| Number(currentConfiguration.leverage) !== leverage
|
|
1426
1886
|
|| currentConfiguration.orderType !== orderType
|
|
1427
|
-
|| (orderType === 'limit' && currentConfiguration.limitPrice !==
|
|
1887
|
+
|| (orderType === 'limit' && parseUsdBaseUnits(currentConfiguration.limitPrice) !== expectedVisibleIdentity.limitPriceBaseUnits)
|
|
1428
1888
|
|| (orderType === 'market' && currentConfiguration.limitPrice != null)) {
|
|
1429
1889
|
throw new Error('Visible Perps order inputs changed before dispatch.');
|
|
1430
1890
|
}
|
|
1891
|
+
if (preparedConfiguration && JSON.stringify(await readPreparedOrderConfiguration(page)) !== JSON.stringify(preparedConfiguration)) {
|
|
1892
|
+
throw new Error('Prepared Perps direction or protection changed before dispatch.');
|
|
1893
|
+
}
|
|
1431
1894
|
dispatchTarget = currentConfiguration.target;
|
|
1895
|
+
await requirePerpsTradingContext(input, page);
|
|
1432
1896
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
1897
|
+
await submissionObserver?.checkSizing();
|
|
1433
1898
|
} catch (error) {
|
|
1434
1899
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
1435
1900
|
}
|
|
1901
|
+
submissionObserver?.arm();
|
|
1436
1902
|
await page.session.call('Input.dispatchMouseEvent', {
|
|
1437
1903
|
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
1438
1904
|
button: 'left', buttons: 1, clickCount: 1,
|
|
@@ -1441,6 +1907,9 @@ export async function placeOrder(input) {
|
|
|
1441
1907
|
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
1442
1908
|
button: 'left', buttons: 0, clickCount: 1,
|
|
1443
1909
|
});
|
|
1910
|
+
return submissionObserver
|
|
1911
|
+
? submissionObserver.accepted(Number(input.node?.timeout_ms ?? 45000))
|
|
1912
|
+
: undefined;
|
|
1444
1913
|
},
|
|
1445
1914
|
observe: async ({ receipt }) => observeExactVisibleOrderResult(
|
|
1446
1915
|
page,
|
|
@@ -1448,7 +1917,10 @@ export async function placeOrder(input) {
|
|
|
1448
1917
|
receipt,
|
|
1449
1918
|
Number(input.node?.timeout_ms ?? 45000),
|
|
1450
1919
|
),
|
|
1451
|
-
|
|
1920
|
+
});
|
|
1921
|
+
} finally {
|
|
1922
|
+
submissionObserver?.close();
|
|
1923
|
+
}
|
|
1452
1924
|
let openOrderCount = null;
|
|
1453
1925
|
if (orderType === 'limit') {
|
|
1454
1926
|
openOrderCount = mutation.observation.order ? 1 : null;
|
|
@@ -1473,6 +1945,7 @@ export async function placeOrder(input) {
|
|
|
1473
1945
|
openOrderCount,
|
|
1474
1946
|
configured,
|
|
1475
1947
|
outcome: mutation.observation,
|
|
1948
|
+
...(fromCurrentForm && orderType === 'market' ? { venueOrderId: mutation.observation.orderId } : {}),
|
|
1476
1949
|
balanceBeforeBaseUnits,
|
|
1477
1950
|
proofPath: 'visible-product-controls',
|
|
1478
1951
|
};
|
|
@@ -1603,16 +2076,22 @@ async function ensureProvider(input, config) {
|
|
|
1603
2076
|
});
|
|
1604
2077
|
}
|
|
1605
2078
|
|
|
2079
|
+
async function assertNetwork(page, network) {
|
|
2080
|
+
if (!network) return { skipped: true };
|
|
2081
|
+
const before = await readPerpsRuntimeState(page);
|
|
2082
|
+
if (!before.available || !before.networkKnown) {
|
|
2083
|
+
throw new Error('Cannot verify the current Perps network.\nNext: mm-harness verify');
|
|
2084
|
+
}
|
|
2085
|
+
const actual = before.isTestnet ? 'testnet' : 'mainnet';
|
|
2086
|
+
if (actual !== network) {
|
|
2087
|
+
throw new Error(`Expected Perps network ${network}, got ${actual}.\nNext: select ${network} through the supported product UI, then rerun the recipe.`);
|
|
2088
|
+
}
|
|
2089
|
+
return { requested: network, isTestnet: before.isTestnet, changed: false };
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1606
2092
|
async function ensureNetwork(input, config) {
|
|
1607
2093
|
if (!config.network) return { skipped: true };
|
|
1608
|
-
|
|
1609
|
-
return withExtensionPage(input, async (page) => {
|
|
1610
|
-
const before = await readPerpsRuntimeState(page);
|
|
1611
|
-
if (!before.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot enforce Perps network.');
|
|
1612
|
-
if (!before.networkKnown) throw new Error('Unable to read the current Perps network from persisted controller state.');
|
|
1613
|
-
if (Boolean(before.isTestnet) === expectedTestnet) return { requested: config.network, isTestnet: before.isTestnet, changed: false };
|
|
1614
|
-
throw new Error(`Expected Perps network ${config.network}, got isTestnet=${before.isTestnet}. Automatic background network toggles are forbidden without a durable receipt; select the network through a supported visible action before running.`);
|
|
1615
|
-
});
|
|
2094
|
+
return withExtensionPage(input, (page) => assertNetwork(page, config.network));
|
|
1616
2095
|
}
|
|
1617
2096
|
|
|
1618
2097
|
async function assertReadyToTrade(input, config) {
|
|
@@ -1678,12 +2157,14 @@ export async function teardownState(input) {
|
|
|
1678
2157
|
orders: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1679
2158
|
};
|
|
1680
2159
|
const config = mergeStateConfig(defaults, params);
|
|
2160
|
+
const network = await ensureNetwork(input, config);
|
|
1681
2161
|
const orders = await applyOrdersState(input, config.orders);
|
|
1682
2162
|
const positions = await applyPositionsState(input, config.positions);
|
|
1683
2163
|
const navigation = config.page === false ? { skipped: true } : await applyStateNavigation(input, config);
|
|
1684
2164
|
return {
|
|
1685
2165
|
action: input.action,
|
|
1686
2166
|
phase: 'teardown',
|
|
2167
|
+
network,
|
|
1687
2168
|
market: config.market ?? config.symbol ?? null,
|
|
1688
2169
|
orders,
|
|
1689
2170
|
positions,
|