@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/commands/help.js +17 -5
  3. package/dist/funding-execution-context.js +19 -4
  4. package/dist/metamask-action-validation.js +3 -2
  5. package/dist/mm-harness-cli.js +2 -1
  6. package/docs/RECIPES.md +2 -2
  7. package/library/actions/core/perps/_controller.mjs +14 -24
  8. package/library/actions/core/perps/assert_orders.mjs +3 -5
  9. package/library/actions/core/perps/assert_positions.mjs +3 -5
  10. package/library/actions/core/perps/close_orders.mjs +28 -4
  11. package/library/actions/core/perps/close_positions.mjs +53 -12
  12. package/library/actions/core/perps/edit_order.mjs +2 -0
  13. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  14. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  15. package/library/actions/core/perps/place_order.mjs +7 -14
  16. package/library/actions/core/perps/read_account.mjs +2 -0
  17. package/library/actions/core/perps/read_orders.mjs +2 -0
  18. package/library/actions/core/perps/read_positions.mjs +3 -1
  19. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  20. package/library/actions/core/perps/start_state.mjs +3 -14
  21. package/library/actions/core/perps/teardown_state.mjs +3 -14
  22. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  23. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  24. package/library/actions/extension/analytics/consent.mjs +2 -0
  25. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  26. package/library/actions/extension/assets/home-token-identity.mjs +1 -0
  27. package/library/actions/extension/assets/import_custom_token.mjs +2 -0
  28. package/library/actions/extension/assets/open_details.mjs +1 -0
  29. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  30. package/library/actions/extension/assets/read_details.mjs +1 -0
  31. package/library/actions/extension/assets/read_visible_state.mjs +1 -0
  32. package/library/actions/extension/assets/review_send.mjs +1 -0
  33. package/library/actions/extension/assets/select_network_scope.mjs +1 -0
  34. package/library/actions/extension/assets/send.mjs +2 -0
  35. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  36. package/library/actions/extension/assets/set_token_visibility.mjs +2 -0
  37. package/library/actions/extension/assets/verify_sorting.mjs +1 -0
  38. package/library/actions/extension/deeplink/open.mjs +1 -0
  39. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  40. package/library/actions/extension/networks/add_custom.mjs +2 -0
  41. package/library/actions/extension/networks/custom_network.mjs +2 -0
  42. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  43. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  44. package/library/actions/extension/performance/_navigation-memory.mjs +2 -0
  45. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +2 -0
  46. package/library/actions/extension/performance/measure_detached_dom.mjs +2 -0
  47. package/library/actions/extension/performance/measure_navigation_memory.mjs +2 -0
  48. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  49. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  50. package/library/actions/extension/perps/assert_visible_consistency.mjs +2 -0
  51. package/library/actions/extension/perps/close_orders.mjs +4 -1
  52. package/library/actions/extension/perps/close_positions.mjs +1 -0
  53. package/library/actions/extension/perps/close_visible_position.mjs +159 -10
  54. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  55. package/library/actions/extension/perps/edit_margin.mjs +2 -0
  56. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  57. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  58. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  59. package/library/actions/extension/perps/open_balance_action.mjs +1 -0
  60. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  61. package/library/actions/extension/perps/perps.mjs +401 -25
  62. package/library/actions/extension/perps/place_order.mjs +1 -0
  63. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  64. package/library/actions/extension/perps/read_funds_confirmation.mjs +2 -0
  65. package/library/actions/extension/perps/read_orders.mjs +1 -0
  66. package/library/actions/extension/perps/read_positions.mjs +1 -0
  67. package/library/actions/extension/perps/read_snapshot.mjs +2 -0
  68. package/library/actions/extension/perps/read_visible_state.mjs +2 -0
  69. package/library/actions/extension/perps/search_markets.mjs +1 -0
  70. package/library/actions/extension/perps/select_activity_filter.mjs +1 -0
  71. package/library/actions/extension/perps/select_market_filter.mjs +1 -0
  72. package/library/actions/extension/perps/set_market_favorite.mjs +1 -0
  73. package/library/actions/extension/perps/state.mjs +1 -0
  74. package/library/actions/extension/perps/update_position_tpsl.mjs +2 -0
  75. package/library/actions/extension/perps/visible-mutation-identity.mjs +2 -0
  76. package/library/actions/extension/platform/cdp.mjs +29 -2
  77. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  78. package/library/actions/extension/swap_bridge/quote-state.mjs +2 -0
  79. package/library/actions/extension/swap_bridge/read_visible_state.mjs +1 -0
  80. package/library/actions/extension/swap_bridge/select_assets.mjs +1 -0
  81. package/library/actions/extension/swap_bridge/set_amount.mjs +1 -0
  82. package/library/actions/extension/swap_bridge/set_max_amount.mjs +1 -0
  83. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -0
  84. package/library/actions/extension/ui/locators.mjs +1 -0
  85. package/library/actions/extension/ui/navigate.mjs +2 -0
  86. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  87. package/library/actions/extension/wallet/import.mjs +2 -0
  88. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  89. package/library/actions/extension/wallet/lock.mjs +2 -0
  90. package/library/actions/extension/wallet/read_state.mjs +1 -0
  91. package/library/actions/extension/wallet/reset.mjs +2 -0
  92. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  93. package/library/actions/extension/wallet/select_account.mjs +2 -0
  94. package/library/actions/extension/wallet/setup.mjs +1 -0
  95. package/library/actions/extension/wallet/state.mjs +2 -0
  96. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  97. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  98. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  99. package/library/actions/mobile/app/network-control.mjs +2 -0
  100. package/library/actions/mobile/app/network.mjs +1 -0
  101. package/library/actions/mobile/app/network_assert.mjs +1 -0
  102. package/library/actions/mobile/app/network_capture.mjs +1 -0
  103. package/library/actions/mobile/assets/import_custom_token.mjs +2 -0
  104. package/library/actions/mobile/assets/open_details.mjs +1 -0
  105. package/library/actions/mobile/assets/read_details.mjs +1 -0
  106. package/library/actions/mobile/assets/read_visible_state.mjs +1 -0
  107. package/library/actions/mobile/assets/set_token_visibility.mjs +2 -0
  108. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  109. package/library/actions/mobile/deeplink/open.mjs +1 -0
  110. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  111. package/library/actions/mobile/networks/network-management.mjs +1 -0
  112. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  113. package/library/actions/mobile/networks/remove_custom.mjs +1 -0
  114. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  115. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  116. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  117. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  118. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  119. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  120. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  121. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  122. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  123. package/library/actions/mobile/perps/perps.mjs +6 -2
  124. package/library/actions/mobile/perps/place_order.mjs +1 -0
  125. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  126. package/library/actions/mobile/perps/read-visible-state-loop.mjs +2 -0
  127. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  128. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  129. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  130. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  131. package/library/actions/mobile/perps/set_market_favorite.mjs +2 -0
  132. package/library/actions/mobile/platform/bridge.mjs +16 -4
  133. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  134. package/library/actions/mobile/platform/native-session.mjs +2 -0
  135. package/library/actions/mobile/platform/observe-ui.mjs +2 -0
  136. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  137. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  138. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  139. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  140. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  141. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  142. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  143. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  144. package/library/actions/mobile/ui/locators.mjs +1 -0
  145. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  146. package/library/actions/mobile/ui/navigate.mjs +2 -0
  147. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  148. package/library/actions/mobile/wallet/home.mjs +1 -0
  149. package/library/actions/mobile/wallet/import.mjs +2 -0
  150. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  151. package/library/actions/mobile/wallet/lock.mjs +1 -0
  152. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  153. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  154. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  155. package/library/actions/mobile/wallet/reset.mjs +1 -0
  156. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  157. package/library/actions/mobile/wallet/setup.mjs +2 -0
  158. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  159. package/library/actions/shared/analytics/consent.mjs +3 -0
  160. package/library/actions/shared/analytics/read_events.mjs +2 -0
  161. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  162. package/library/actions/shared/app/network-artifact.mjs +2 -0
  163. package/library/actions/shared/app/network-assert.mjs +2 -0
  164. package/library/actions/shared/assets/visible-state.mjs +2 -0
  165. package/library/actions/shared/deeplink/url.mjs +3 -0
  166. package/library/actions/shared/networks/visible-state.mjs +2 -0
  167. package/library/actions/shared/perps/visible-state.mjs +2 -0
  168. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  169. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  170. package/library/actions/shared/ui/locators.mjs +2 -0
  171. package/library/actions/shared/wallet/import-source.mjs +3 -0
  172. package/library/manifests/core.action-manifest.json +20 -0
  173. package/library/manifests/extension.action-manifest.json +2 -2
  174. package/library/manifests/mobile.action-manifest.json +3 -3
  175. package/library/recipes/mobile/app/{lifecycle.android-smoke.recipe.json → lifecycle-smoke.recipe.json} +12 -11
  176. package/package.json +1 -1
  177. 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
- const priorOrder = cleanupMode ? cleanupOrder : priorPlace.record?.observation?.order;
400
- if (!priorOrder?.id || !(cleanupMode || orderIdentityMatches(priorOrder, expectedOrder))) {
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) throw new Error('Exact authorized visible order is unavailable for cancellation.');
421
- await page.click(`[data-testid=${JSON.stringify(priorOrder.id)}]`);
422
- await page.waitForSelector(dataTestId('perps-cancel-order-modal'), { timeoutMs });
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, fundingBalance.observedBaseUnits);
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) throw new Error('Exact authorized visible order changed before cancellation.');
446
- await page.click(`[data-testid=${JSON.stringify(priorOrder.id)}]`);
447
- await page.waitForSelector(dataTestId('perps-cancel-order-modal'), { timeoutMs });
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: true });
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: 0, orderCount: 0, balanceBaseUnits: cleanup.balanceBaseUnits },
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: true,
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
- proofPath: 'visible-product-controls',
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
- if (observedNotionalBaseUnits && positionIdentityMatches(position, expected)) {
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) && orderIdentityMatches(order, expected));
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
- const mutation = await runGuardedExternalMutation({
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,4 @@
1
+ // Place a Perps order and return visible-control evidence or assert the resulting order or position state.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { assertOrders, assertPositions, placeOrder } from './perps.mjs';
3
4
  runAdapter(async (input) => {
@@ -1,3 +1,4 @@
1
+ // Fetch MetaMask geolocation and report it alongside the product's current Perps eligibility state.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  runAdapter(async (input) => {
@@ -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) {
@@ -1,3 +1,4 @@
1
+ // Read live Perps orders with optional selection and network checks, returning redacted order fields.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { readOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Read runtime Perps positions with optional selection and network checks, returning redacted position fields.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { readPositions } from './perps.mjs';
3
4
  runAdapter(readPositions);