@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,3 +1,6 @@
|
|
|
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';
|
|
2
5
|
import { createHash } from 'node:crypto';
|
|
3
6
|
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
@@ -323,6 +326,50 @@ async function closeOrderThroughBackground(input, page, timeoutMs) {
|
|
|
323
326
|
};
|
|
324
327
|
}
|
|
325
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
|
+
|
|
326
373
|
export async function closeOrders(input) {
|
|
327
374
|
requireExplicitSelection(input);
|
|
328
375
|
return withExtensionPage(input, async (page) => {
|
|
@@ -348,7 +395,8 @@ export async function closeOrders(input) {
|
|
|
348
395
|
const limitPrice = String(input.node?.limit_price ?? '');
|
|
349
396
|
const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
|
|
350
397
|
if (!/^(?:long|short)$/u.test(side) || !Number.isInteger(leverage)
|
|
351
|
-
|| !/^[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)) {
|
|
352
400
|
throw new Error('Visible Perps cancellation requires side, amount, leverage, and explicit limit_price identity.');
|
|
353
401
|
}
|
|
354
402
|
const operation = {
|
|
@@ -396,8 +444,57 @@ export async function closeOrders(input) {
|
|
|
396
444
|
&& BigInt(cleanupCandidate.observedNotionalBaseUnits) > BigInt(expectedOrder.notionalBaseUnits)
|
|
397
445
|
&& BigInt(cleanupCandidate.overageBaseUnits)
|
|
398
446
|
=== BigInt(cleanupCandidate.observedNotionalBaseUnits) - BigInt(expectedOrder.notionalBaseUnits);
|
|
399
|
-
|
|
400
|
-
|
|
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))) {
|
|
401
498
|
throw new Error('Prior place receipt has no exact visible limit-order identity to cancel.');
|
|
402
499
|
}
|
|
403
500
|
const identity = trustedExternalMutationIdentity(input, page, {
|
|
@@ -413,14 +510,19 @@ export async function closeOrders(input) {
|
|
|
413
510
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
414
511
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
415
512
|
const exact = (await readVisibleOrderCards(page, symbol))
|
|
416
|
-
.filter((order) => order.id === priorOrder.id && (cleanupMode
|
|
513
|
+
.filter((order) => order.id === priorOrder.id && (accepted ? matchesAcceptedRemainder(order) : cleanupMode
|
|
417
514
|
? cleanupCandidateIdentityMatches(order, expectedOrder)
|
|
418
515
|
&& String(order.notionalBaseUnits) === String(cleanupCandidate.observedNotionalBaseUnits)
|
|
419
516
|
: orderIdentityMatches(order, priorOrder)));
|
|
420
|
-
if (exact.length !== 1)
|
|
421
|
-
|
|
422
|
-
|
|
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);
|
|
423
524
|
}
|
|
525
|
+
let filledBeforeDispatch = existingReceipt?.record?.observation?.filledOrder ?? null;
|
|
424
526
|
const mutation = await runGuardedExternalMutation({
|
|
425
527
|
input,
|
|
426
528
|
identity,
|
|
@@ -433,18 +535,31 @@ export async function closeOrders(input) {
|
|
|
433
535
|
dispatch: async () => {
|
|
434
536
|
let dispatchTarget;
|
|
435
537
|
try {
|
|
436
|
-
await requireCurrentVisibleFundingBalance(input, page, timeoutMs
|
|
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
|
+
}
|
|
437
544
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
438
545
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
439
546
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
440
547
|
const exact = (await readVisibleOrderCards(page, symbol))
|
|
441
|
-
.filter((order) => order.id === priorOrder.id && (cleanupMode
|
|
548
|
+
.filter((order) => order.id === priorOrder.id && (accepted ? matchesAcceptedRemainder(order) : cleanupMode
|
|
442
549
|
? cleanupCandidateIdentityMatches(order, expectedOrder)
|
|
443
550
|
&& String(order.notionalBaseUnits) === String(cleanupCandidate.observedNotionalBaseUnits)
|
|
444
551
|
: orderIdentityMatches(order, priorOrder)));
|
|
445
|
-
if (exact.length !== 1)
|
|
446
|
-
|
|
447
|
-
|
|
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;
|
|
448
563
|
dispatchTarget = await page.evaluate(`(() => {
|
|
449
564
|
const element = document.querySelector('[data-testid="perps-cancel-order-button"]');
|
|
450
565
|
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
@@ -454,9 +569,12 @@ export async function closeOrders(input) {
|
|
|
454
569
|
const rect = element.getBoundingClientRect();
|
|
455
570
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
456
571
|
})()`);
|
|
457
|
-
await requirePerpsTradingContext(input, page);
|
|
458
|
-
assertTrustedExternalMutationTarget(input, page, identity);
|
|
459
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
|
+
}
|
|
460
578
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
461
579
|
}
|
|
462
580
|
await page.session.call('Input.dispatchMouseEvent', {
|
|
@@ -469,6 +587,8 @@ export async function closeOrders(input) {
|
|
|
469
587
|
});
|
|
470
588
|
},
|
|
471
589
|
observe: async () => {
|
|
590
|
+
if (filledBeforeDispatch) return { success: true, canceled: false,
|
|
591
|
+
filledOrder: filledBeforeDispatch, reason: 'already_filled', cleanup: null };
|
|
472
592
|
const deadline = Date.now() + timeoutMs;
|
|
473
593
|
let absentSince = null;
|
|
474
594
|
while (Date.now() < deadline) {
|
|
@@ -476,12 +596,17 @@ export async function closeOrders(input) {
|
|
|
476
596
|
if (!present) {
|
|
477
597
|
absentSince ??= Date.now();
|
|
478
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 };
|
|
479
602
|
await page.navigateHash('#/perps-home');
|
|
480
|
-
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.');
|
|
481
605
|
return {
|
|
482
606
|
success: true,
|
|
607
|
+
canceled: true,
|
|
483
608
|
canceledOrder: priorOrder,
|
|
484
|
-
cleanup: { positionCount:
|
|
609
|
+
cleanup: { positionCount: cleanup.positionCount, orderCount: 0, balanceBaseUnits: cleanup.balanceBaseUnits },
|
|
485
610
|
};
|
|
486
611
|
}
|
|
487
612
|
} else absentSince = null;
|
|
@@ -492,14 +617,15 @@ export async function closeOrders(input) {
|
|
|
492
617
|
});
|
|
493
618
|
return {
|
|
494
619
|
action: input.action,
|
|
495
|
-
canceled:
|
|
496
|
-
matchingCount: 1,
|
|
620
|
+
canceled: mutation.observation.canceled !== false,
|
|
621
|
+
matchingCount: mutation.observation.canceled === false ? 0 : 1,
|
|
497
622
|
symbols: [symbol],
|
|
498
623
|
order: priorOrder,
|
|
499
624
|
receipt: mutation.receipt,
|
|
500
625
|
resumed: mutation.resumed,
|
|
501
626
|
cleanup: mutation.observation.cleanup,
|
|
502
|
-
|
|
627
|
+
...(mutation.observation.filledOrder ? { reason: 'already_filled', filledOrder: mutation.observation.filledOrder } : {}),
|
|
628
|
+
proofPath: mutation.observation.canceled === false ? 'accepted-order-status' : 'visible-product-controls',
|
|
503
629
|
};
|
|
504
630
|
}
|
|
505
631
|
throw new Error('Visible Perps cancellation did not return a terminal result.');
|
|
@@ -868,6 +994,8 @@ function cleanupCandidateIdentityMatches(order, expected) {
|
|
|
868
994
|
}
|
|
869
995
|
|
|
870
996
|
async function observePolicyViolationCleanupCandidate(page, expected, receipt, timeoutMs) {
|
|
997
|
+
const acceptedId = receipt?.acceptance?.venueOrderId;
|
|
998
|
+
if (!acceptedId) return null;
|
|
871
999
|
const baselineIds = Array.isArray(receipt?.facts?.baselineOrderIds) ? receipt.facts.baselineOrderIds : null;
|
|
872
1000
|
if (!baselineIds || baselineIds.length !== 0) return null;
|
|
873
1001
|
const deadline = Date.now() + Math.min(timeoutMs, 10_000);
|
|
@@ -875,7 +1003,7 @@ async function observePolicyViolationCleanupCandidate(page, expected, receipt, t
|
|
|
875
1003
|
let stableSince = 0;
|
|
876
1004
|
while (Date.now() < deadline) {
|
|
877
1005
|
const candidates = (await readVisibleOrderCards(page, expected.market))
|
|
878
|
-
.filter((order) => cleanupCandidateIdentityMatches(order, expected));
|
|
1006
|
+
.filter((order) => order.id === `order-card-${acceptedId}` && cleanupCandidateIdentityMatches(order, expected));
|
|
879
1007
|
if (candidates.length > 1) throw new Error('More than one visible over-cap order matches the cleanup identity.');
|
|
880
1008
|
if (candidates.length === 1) {
|
|
881
1009
|
const key = JSON.stringify(candidates[0]);
|
|
@@ -908,6 +1036,31 @@ async function observePolicyViolationCleanupCandidate(page, expected, receipt, t
|
|
|
908
1036
|
}
|
|
909
1037
|
|
|
910
1038
|
async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs) {
|
|
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
|
+
}
|
|
911
1064
|
if (!expected.fromCurrentForm) {
|
|
912
1065
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(expected.market)}`);
|
|
913
1066
|
}
|
|
@@ -920,6 +1073,12 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
920
1073
|
if (expected.orderType === 'market') {
|
|
921
1074
|
const position = await readVisiblePositionIdentity(page);
|
|
922
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
|
+
}
|
|
923
1082
|
if (position) {
|
|
924
1083
|
const size = Number(String(position.size).replace(/,/gu, '').match(/[0-9]+(?:\.[0-9]+)?/u)?.[0]);
|
|
925
1084
|
const price = await readCurrentMarketPrice(page, expected.market);
|
|
@@ -942,7 +1101,9 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
942
1101
|
observedNotionalBaseUnits,
|
|
943
1102
|
} : null,
|
|
944
1103
|
};
|
|
945
|
-
|
|
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) {
|
|
946
1107
|
const key = JSON.stringify(position);
|
|
947
1108
|
if (key !== stableIdentity) {
|
|
948
1109
|
stableIdentity = key;
|
|
@@ -954,7 +1115,8 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
954
1115
|
observedNotionalBaseUnits,
|
|
955
1116
|
maximumNotionalBaseUnits: expected.maximumNotionalBaseUnits,
|
|
956
1117
|
order: null,
|
|
957
|
-
orderId: null,
|
|
1118
|
+
orderId: receipt?.acceptance?.venueOrderId ?? null,
|
|
1119
|
+
...(expected.fromCurrentForm ? { filledSize: receipt.acceptance.filledSize, averagePrice: receipt.acceptance.averagePrice } : {}),
|
|
958
1120
|
};
|
|
959
1121
|
}
|
|
960
1122
|
} else {
|
|
@@ -978,7 +1140,10 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
|
|
|
978
1140
|
})),
|
|
979
1141
|
};
|
|
980
1142
|
const matching = orders
|
|
981
|
-
.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));
|
|
982
1147
|
if (matching.length > 1) throw new Error('More than one new visible order matches the authorized identity.');
|
|
983
1148
|
if (matching.length === 1) {
|
|
984
1149
|
const key = JSON.stringify(matching[0]);
|
|
@@ -1204,6 +1369,188 @@ async function readPreparedOrderConfiguration(page) {
|
|
|
1204
1369
|
})()`);
|
|
1205
1370
|
}
|
|
1206
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
|
+
|
|
1207
1554
|
export async function placeOrder(input) {
|
|
1208
1555
|
requirePlaceOrderInputs(input);
|
|
1209
1556
|
const symbol = marketSymbol(input);
|
|
@@ -1269,6 +1616,7 @@ export async function placeOrder(input) {
|
|
|
1269
1616
|
...(orderType === 'market' ? {
|
|
1270
1617
|
maximumNotionalBaseUnits: placementBudget.maxBaseUnits,
|
|
1271
1618
|
} : {}),
|
|
1619
|
+
...(fromCurrentForm && orderType === 'limit' ? { maximumNotionalBaseUnits: placementBudget.maxBaseUnits } : {}),
|
|
1272
1620
|
};
|
|
1273
1621
|
const hasBackgroundRequest = await page.evaluate(
|
|
1274
1622
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
@@ -1302,6 +1650,11 @@ export async function placeOrder(input) {
|
|
|
1302
1650
|
}
|
|
1303
1651
|
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
1304
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
|
+
}
|
|
1305
1658
|
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
1306
1659
|
if (!fromCurrentForm) {
|
|
1307
1660
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
@@ -1378,6 +1731,7 @@ export async function placeOrder(input) {
|
|
|
1378
1731
|
openOrderCount: mutation.observation.order ? 1 : null,
|
|
1379
1732
|
configured: null,
|
|
1380
1733
|
outcome: mutation.observation,
|
|
1734
|
+
...(fromCurrentForm && orderType === 'market' ? { venueOrderId: mutation.observation.orderId } : {}),
|
|
1381
1735
|
proofPath: 'visible-product-controls',
|
|
1382
1736
|
};
|
|
1383
1737
|
});
|
|
@@ -1484,12 +1838,25 @@ export async function placeOrder(input) {
|
|
|
1484
1838
|
|| preparedConfiguration.direction !== side)) {
|
|
1485
1839
|
throw new Error('Prepared Perps form does not match the requested market and direction.');
|
|
1486
1840
|
}
|
|
1487
|
-
|
|
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({
|
|
1488
1854
|
input,
|
|
1489
1855
|
identity,
|
|
1490
1856
|
publicationFacts: {
|
|
1491
1857
|
balanceBeforeBaseUnits, baselineOrderIds, fundingBalance, tradingContext,
|
|
1492
1858
|
...(preparedConfiguration ? { preparedConfiguration } : {}),
|
|
1859
|
+
...(submissionObserver ? { [orderType === 'market' ? 'marketSizing' : 'limitSizing']: submissionObserver.sizing } : {}),
|
|
1493
1860
|
},
|
|
1494
1861
|
dispatch: async () => {
|
|
1495
1862
|
let dispatchTarget;
|
|
@@ -1527,9 +1894,11 @@ export async function placeOrder(input) {
|
|
|
1527
1894
|
dispatchTarget = currentConfiguration.target;
|
|
1528
1895
|
await requirePerpsTradingContext(input, page);
|
|
1529
1896
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
1897
|
+
await submissionObserver?.checkSizing();
|
|
1530
1898
|
} catch (error) {
|
|
1531
1899
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
1532
1900
|
}
|
|
1901
|
+
submissionObserver?.arm();
|
|
1533
1902
|
await page.session.call('Input.dispatchMouseEvent', {
|
|
1534
1903
|
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
1535
1904
|
button: 'left', buttons: 1, clickCount: 1,
|
|
@@ -1538,6 +1907,9 @@ export async function placeOrder(input) {
|
|
|
1538
1907
|
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
1539
1908
|
button: 'left', buttons: 0, clickCount: 1,
|
|
1540
1909
|
});
|
|
1910
|
+
return submissionObserver
|
|
1911
|
+
? submissionObserver.accepted(Number(input.node?.timeout_ms ?? 45000))
|
|
1912
|
+
: undefined;
|
|
1541
1913
|
},
|
|
1542
1914
|
observe: async ({ receipt }) => observeExactVisibleOrderResult(
|
|
1543
1915
|
page,
|
|
@@ -1545,7 +1917,10 @@ export async function placeOrder(input) {
|
|
|
1545
1917
|
receipt,
|
|
1546
1918
|
Number(input.node?.timeout_ms ?? 45000),
|
|
1547
1919
|
),
|
|
1548
|
-
|
|
1920
|
+
});
|
|
1921
|
+
} finally {
|
|
1922
|
+
submissionObserver?.close();
|
|
1923
|
+
}
|
|
1549
1924
|
let openOrderCount = null;
|
|
1550
1925
|
if (orderType === 'limit') {
|
|
1551
1926
|
openOrderCount = mutation.observation.order ? 1 : null;
|
|
@@ -1570,6 +1945,7 @@ export async function placeOrder(input) {
|
|
|
1570
1945
|
openOrderCount,
|
|
1571
1946
|
configured,
|
|
1572
1947
|
outcome: mutation.observation,
|
|
1948
|
+
...(fromCurrentForm && orderType === 'market' ? { venueOrderId: mutation.observation.orderId } : {}),
|
|
1573
1949
|
balanceBeforeBaseUnits,
|
|
1574
1950
|
proofPath: 'visible-product-controls',
|
|
1575
1951
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Read the displayed Perps deposit or withdrawal confirmation and check requested UI expectations.
|
|
2
|
+
// Report preview fields, submit availability, and layout without submitting funds.
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
|
|
3
5
|
function expectedList(value, name) {
|