@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.49.0 - 2026-09-07
|
|
6
|
+
|
|
7
|
+
- Simplify Mobile setup and sizing; correct nested-route verification, lifecycle portability, wallet selection, input clearing and token-sort restoration.
|
|
8
|
+
- Keep piped help complete; add platform-scoped recipe guidance, fixture troubleshooting and action descriptions.
|
|
9
|
+
- Consolidate market/limit validation into `perps.release-live-order`; bind balances to their wallet runtime and cleanup to accepted orders, correct sizing/direction checks, and support approved source-dev testnet QA up to $20.
|
|
10
|
+
|
|
5
11
|
## 0.48.0 - 2026-09-07
|
|
6
12
|
|
|
7
13
|
- Consolidate parameterized recipes, move specialized Perps journeys into the team library, and separate short performance checks from full coverage suites.
|
package/dist/commands/help.js
CHANGED
|
@@ -3,19 +3,31 @@ import path from "node:path";
|
|
|
3
3
|
import { detectAdapter } from "../harness.js";
|
|
4
4
|
import { optionString, parseArgs, targetPath } from "./parse-args.js";
|
|
5
5
|
const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core>";
|
|
6
|
+
function validHelpLine(value) {
|
|
7
|
+
return typeof value === "string" || Boolean(value && typeof value.text === "string" && Array.isArray(value.adapters) && value.adapters.length > 0 && value.adapters.every((adapter) => ["mobile", "extension", "core"].includes(adapter)));
|
|
8
|
+
}
|
|
9
|
+
function scopedSections(topic, adapter) {
|
|
10
|
+
const select = (lines) => lines.flatMap((line) => typeof line === "string" ? [line] : line.adapters.includes(adapter) ? [line.text] : []);
|
|
11
|
+
return topic.sections.map((section) => ({
|
|
12
|
+
...section,
|
|
13
|
+
details: select(section.details),
|
|
14
|
+
commands: select(section.commands)
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
6
17
|
function loadRecipeHelp(packageRoot) {
|
|
7
18
|
const file = path.join(packageRoot, "site", "assets", "help-recipes.json");
|
|
8
19
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
9
20
|
if (value.schemaVersion !== 1 || value.topic !== "recipes" || typeof value.title !== "string" || typeof value.summary !== "string" || typeof value.capabilities !== "string" || typeof value.scopeNotice !== "string" || typeof value.recipeProtocolVersion !== "string" || typeof value.safety !== "string" || !Array.isArray(value.sections) || value.sections.length !== 8 || value.sections.some(
|
|
10
|
-
(section) => !section || typeof section.id !== "string" || typeof section.title !== "string" || typeof section.instruction !== "string" || !Array.isArray(section.details) || section.details.some((detail) =>
|
|
21
|
+
(section) => !section || typeof section.id !== "string" || typeof section.title !== "string" || typeof section.instruction !== "string" || !Array.isArray(section.details) || section.details.some((detail) => !validHelpLine(detail)) || !Array.isArray(section.commands) || section.commands.some((command) => !validHelpLine(command))
|
|
11
22
|
)) {
|
|
12
23
|
throw new Error(`Invalid recipe help data: ${file}`);
|
|
13
24
|
}
|
|
14
25
|
return value;
|
|
15
26
|
}
|
|
16
27
|
function recipeHelpPayload(topic, harnessVersion, adapter, target) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
28
|
+
const sections = scopedSections(topic, adapter);
|
|
29
|
+
const commands = Array.from(new Set(sections.flatMap((section) => section.commands)));
|
|
30
|
+
const discoveryCommands = sections.find((section) => section.id === "discover")?.commands ?? [];
|
|
19
31
|
return {
|
|
20
32
|
schemaVersion: topic.schemaVersion,
|
|
21
33
|
command: "help",
|
|
@@ -29,7 +41,7 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
|
|
|
29
41
|
summary: topic.summary,
|
|
30
42
|
capabilities: topic.capabilities,
|
|
31
43
|
scopeNotice: topic.scopeNotice,
|
|
32
|
-
sections
|
|
44
|
+
sections,
|
|
33
45
|
commands,
|
|
34
46
|
discoveryCommands,
|
|
35
47
|
safety: topic.safety
|
|
@@ -45,7 +57,7 @@ function renderRecipeHelp(topic, harnessVersion, adapter) {
|
|
|
45
57
|
"Visual tutorial: mm-harness tutorial"
|
|
46
58
|
];
|
|
47
59
|
if (adapter === "unscoped") lines.push(`No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`);
|
|
48
|
-
for (const [index, section] of topic.
|
|
60
|
+
for (const [index, section] of scopedSections(topic, adapter).entries()) {
|
|
49
61
|
lines.push("", `${index + 1}. ${section.title}`, ` ${section.instruction}`);
|
|
50
62
|
for (const detail of section.details) lines.push(` - ${detail}`);
|
|
51
63
|
for (const command of section.commands) lines.push(` $ ${command}`);
|
|
@@ -36,7 +36,8 @@ const CLAIM_DIFF_FIELDS = [
|
|
|
36
36
|
];
|
|
37
37
|
const trustedActionContexts = /* @__PURE__ */ new WeakSet();
|
|
38
38
|
const fundingSources = /* @__PURE__ */ new WeakMap();
|
|
39
|
-
const CURRENT_FUNDING_REGISTRY_SHA256 = "
|
|
39
|
+
const CURRENT_FUNDING_REGISTRY_SHA256 = "d4416e0e8a812a0e2f1798ae6f901a5820f7ee0b9783cfa5d515d0c7863ea80e";
|
|
40
|
+
const PREVIOUS_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
|
|
40
41
|
const FROZEN_G005_FUNDING_REGISTRY_SHA256 = "c2230bc02fa00d6902915d1cb81bcc08389ea7e399ee9618f26fb0d9fc00b291";
|
|
41
42
|
const LIVE_PERPS_TINY_REGISTRY = {
|
|
42
43
|
"live-perps-tiny": {
|
|
@@ -58,8 +59,15 @@ const LIVE_PERPS_TINY_REGISTRY = {
|
|
|
58
59
|
};
|
|
59
60
|
const TRUSTED_FUNDING_REGISTRIES = {
|
|
60
61
|
[CURRENT_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY,
|
|
62
|
+
[PREVIOUS_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY,
|
|
61
63
|
[FROZEN_G005_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY
|
|
62
64
|
};
|
|
65
|
+
const SOURCE_DEV_TESTNET_BUDGETS = /* @__PURE__ */ new Set([
|
|
66
|
+
"perps-market-order:usdc-base-units",
|
|
67
|
+
"perps-limit-order:usdc-base-units",
|
|
68
|
+
"perps-cancel-order:usdc-base-units",
|
|
69
|
+
"perps-close-position:usdc-base-units"
|
|
70
|
+
]);
|
|
63
71
|
const OPERATION_ACTION = /* @__PURE__ */ new Map([
|
|
64
72
|
["perps-market-order", "metamask.perps.place_order"],
|
|
65
73
|
["perps-limit-order", "metamask.perps.place_order"],
|
|
@@ -1002,7 +1010,7 @@ async function validateFundingEvidenceChain(documents, identity) {
|
|
|
1002
1010
|
}
|
|
1003
1011
|
balanceRuntime = currentRuntime;
|
|
1004
1012
|
const visibleState = requireRecord(evidenceDocument.visibleState, "funding balance evidence visibleState");
|
|
1005
|
-
if (visibleState.perpsAvailableMarginBaseUnits !== actualBaseUnits || visibleBalanceBaseUnits(trace, emittedNodes) !== actualBaseUnits) {
|
|
1013
|
+
if (visibleState.perpsAvailableMarginBaseUnits !== actualBaseUnits || visibleBalanceBaseUnits(trace, emittedNodes, currentRuntime) !== actualBaseUnits) {
|
|
1006
1014
|
throw new Error("Funding balance actual value does not match its visible trace.");
|
|
1007
1015
|
}
|
|
1008
1016
|
if (!exactTerminalPass(recipe, trace, summary, artifactManifest)) {
|
|
@@ -1086,11 +1094,17 @@ function extensionOrigin(value, label) {
|
|
|
1086
1094
|
throw new Error(`${label} Extension origin is invalid.`);
|
|
1087
1095
|
}
|
|
1088
1096
|
}
|
|
1089
|
-
function visibleBalanceBaseUnits(trace, emittedNodes) {
|
|
1097
|
+
function visibleBalanceBaseUnits(trace, emittedNodes, runtime) {
|
|
1090
1098
|
const entries = Array.isArray(trace.entries) ? trace.entries : [];
|
|
1091
1099
|
const matches = entries.filter((entry) => isRecord(entry) && entry.action === "metamask.perps.read_visible_state" && entry.ok === true && isRecord(entry.output) && entry.output.action === entry.action && entry.output.surface === "home" && entry.output.source === "visible-ui" && isRecord(entry.output.balance) && entry.output.balance.visible === true && isRecord(emittedNodes.get(String(entry.nodeId))) && emittedNodes.get(String(entry.nodeId))?.surface === "home");
|
|
1092
1100
|
const matchEntry = matches.at(-1);
|
|
1093
1101
|
if (!matchEntry) throw new Error("Funding balance evidence requires a visible Perps Home balance observation.");
|
|
1102
|
+
for (const entry of matches) {
|
|
1103
|
+
const output = requireRecord(entry.output, "funding balance observation");
|
|
1104
|
+
if (output.runtimeSessionId !== runtime.runtimeSessionId || output.cdpPort !== runtime.cdpPort || extensionOrigin(output.targetUrl, "funding balance observation") !== runtime.extensionOrigin) {
|
|
1105
|
+
throw new Error("Funding balance observation is not from its anchored wallet runtime.");
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1094
1108
|
const label = String(matchEntry.output.balance.label ?? "");
|
|
1095
1109
|
const match = /(?:^|\s)\$([0-9]+(?:\.[0-9]{1,2})?)(?:\s|$)/u.exec(label);
|
|
1096
1110
|
if (!match) throw new Error("Funding balance evidence visible amount is invalid.");
|
|
@@ -1325,7 +1339,8 @@ function validateTrustedFundingRegistry(identity, operations, budgets) {
|
|
|
1325
1339
|
for (const [key, aggregate] of requested) {
|
|
1326
1340
|
const trusted = domain.budgets.get(key);
|
|
1327
1341
|
const supplied = suppliedBudgets.get(key);
|
|
1328
|
-
|
|
1342
|
+
const maximum = BigInt(identity.registrySha256 === CURRENT_FUNDING_REGISTRY_SHA256 && identity.client === "extension" && identity.proofLane === "source-dev" && SOURCE_DEV_TESTNET_BUDGETS.has(key) ? "20000000" : trusted?.maxBaseUnits ?? "0");
|
|
1343
|
+
if (!trusted || !supplied || BigInt(supplied.maxBaseUnits) > maximum || supplied.maxCount > trusted.maxCount || aggregate.maxAmountBaseUnits > maximum || aggregate.count > trusted.maxCount || aggregate.amountBaseUnits > maximum * BigInt(trusted.maxCount)) {
|
|
1329
1344
|
throw new Error(`Funding operation ${aggregate.operation} is not bounded by the runner-trusted funding registry.`);
|
|
1330
1345
|
}
|
|
1331
1346
|
}
|
|
@@ -191,7 +191,8 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
|
191
191
|
}
|
|
192
192
|
if (action === "metamask.perps.close_orders") {
|
|
193
193
|
required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
|
|
194
|
-
|
|
194
|
+
const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
|
|
195
|
+
if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
195
196
|
findings.push({
|
|
196
197
|
severity: "error",
|
|
197
198
|
code: "recipe.invalid_param",
|
|
@@ -200,7 +201,7 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
|
200
201
|
});
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
|
-
if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? ""))) {
|
|
204
|
+
if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
204
205
|
findings.push({
|
|
205
206
|
severity: "error",
|
|
206
207
|
code: "recipe.invalid_param",
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -826,7 +826,8 @@ for (const command of REAL) {
|
|
|
826
826
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
827
827
|
}
|
|
828
828
|
if (command.name === "help") {
|
|
829
|
-
process.
|
|
829
|
+
process.exitCode = handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion });
|
|
830
|
+
return;
|
|
830
831
|
}
|
|
831
832
|
if (command.name === "tutorial") {
|
|
832
833
|
process.exit(handleTutorial(rawArgv.slice(1), { harnessVersion: pkgVersion }));
|
package/docs/RECIPES.md
CHANGED
|
@@ -115,8 +115,8 @@ mm-harness run perps.release-market-filters --describe --adapter extension --jso
|
|
|
115
115
|
| --- | --- |
|
|
116
116
|
| Activity filter routes | `perps.release-activity-filters` |
|
|
117
117
|
| Deposit and withdraw previews without submission | `perps.release-funds-flows` |
|
|
118
|
-
| Resting limit placement and exact cancellation | `perps.release-live-limit
|
|
119
|
-
| Filled market placement and exact position close | `perps.release-live-market
|
|
118
|
+
| Resting limit placement and exact cancellation | `perps.release-live-order order_type=limit limit_price='"<price>"'` |
|
|
119
|
+
| Filled market placement and exact position close | `perps.release-live-order order_type=market` |
|
|
120
120
|
| Market header and statistics | `perps.release-market-details` |
|
|
121
121
|
| Market categories | `perps.release-market-filters` |
|
|
122
122
|
| Market text search | `perps.release-market-search` |
|
|
@@ -1,26 +1,14 @@
|
|
|
1
|
+
// Load the target checkout's Perps controller and resolve fixture-backed accounts.
|
|
2
|
+
// Share headless selection, signing, formatting and adapter-output helpers.
|
|
3
|
+
|
|
1
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
5
|
import path from 'node:path';
|
|
3
6
|
import { pathToFileURL } from 'node:url';
|
|
4
7
|
|
|
5
8
|
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
|
|
6
9
|
|
|
7
|
-
// Importing a runner .ts source signals the live-adapter contract to execute
|
|
8
|
-
// this adapter under the bundled tsx (see commandFor/importsSourceTypescript in
|
|
9
|
-
// src/live-adapter-contract.ts). The core adapter dynamic-imports the perps
|
|
10
|
-
// controller TypeScript at runtime, so it MUST run under tsx, not plain node.
|
|
11
10
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
12
11
|
|
|
13
|
-
// Shared headless instantiation for the MetaMask `core` adapter.
|
|
14
|
-
//
|
|
15
|
-
// Slice 1: read-only. We instantiate @metamask/perps-controller against a
|
|
16
|
-
// resolved MetaMask/core checkout (context.projectRoot) and drive its standalone
|
|
17
|
-
// read path, which talks to HyperLiquid testnet over HTTP — no CDP, no bridge,
|
|
18
|
-
// no UI, no signer. The controller's standalone read methods
|
|
19
|
-
// (getPositions/getOpenOrders/getAccountState with { standalone: true,
|
|
20
|
-
// userAddress }) create their own InfoClient and never touch the messenger, so
|
|
21
|
-
// reads need ZERO external action handlers. Signing/account-resolution through
|
|
22
|
-
// the messenger is Slice 2.
|
|
23
|
-
|
|
24
12
|
function controllerEntry(projectRoot) {
|
|
25
13
|
return pathToFileURL(path.join(projectRoot, 'packages/perps-controller/src/index.ts')).href;
|
|
26
14
|
}
|
|
@@ -590,12 +578,7 @@ export function symbolForItem(item) {
|
|
|
590
578
|
return normalizeMarketSymbol(item?.symbol ?? item?.coin ?? '');
|
|
591
579
|
}
|
|
592
580
|
|
|
593
|
-
//
|
|
594
|
-
// Positions report side as long/short already; OPEN ORDERS report it as buy/sell
|
|
595
|
-
// (a resting BUY = long direction, a resting SELL = short). Mapping both onto
|
|
596
|
-
// long/short lets the shared `side` selector filter positions and orders alike —
|
|
597
|
-
// without this, requesting side:"long" silently drops every order (whose side is
|
|
598
|
-
// "buy"), which is the bug this normalization fixes.
|
|
581
|
+
// Orders carry buy/sell sides; Position.size is signed and authoritative.
|
|
599
582
|
function normalizeSide(rawSide) {
|
|
600
583
|
const side = String(rawSide ?? '').toLowerCase();
|
|
601
584
|
if (side === 'buy' || side === 'b') return 'long';
|
|
@@ -603,7 +586,14 @@ function normalizeSide(rawSide) {
|
|
|
603
586
|
return side;
|
|
604
587
|
}
|
|
605
588
|
|
|
606
|
-
function sideForItem(item) {
|
|
589
|
+
function sideForItem(item, itemType) {
|
|
590
|
+
if (itemType === 'position') {
|
|
591
|
+
const rawSize = item?.size ?? item?.szi;
|
|
592
|
+
if (typeof rawSize !== 'string' && typeof rawSize !== 'number') return '';
|
|
593
|
+
const size = Number(rawSize);
|
|
594
|
+
if (!Number.isFinite(size) || size === 0) return '';
|
|
595
|
+
return size > 0 ? 'long' : 'short';
|
|
596
|
+
}
|
|
607
597
|
return normalizeSide(item?.side ?? item?.direction ?? '');
|
|
608
598
|
}
|
|
609
599
|
|
|
@@ -646,7 +636,7 @@ export function requireExplicitSelection(input) {
|
|
|
646
636
|
* vocabulary as the extension perps adapter. With no selector and no
|
|
647
637
|
* mode=all, returns every item (read defaults to showing all live state).
|
|
648
638
|
*/
|
|
649
|
-
export function selectedItems(input, items) {
|
|
639
|
+
export function selectedItems(input, items, itemType = 'order') {
|
|
650
640
|
const requested = configuredSymbols(input, items);
|
|
651
641
|
const selector =
|
|
652
642
|
input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
@@ -655,7 +645,7 @@ export function selectedItems(input, items) {
|
|
|
655
645
|
const symbols = requested.length > 0 ? new Set(requested) : null;
|
|
656
646
|
return items.filter((item) => {
|
|
657
647
|
if (symbols && !symbols.has(symbolForItem(item))) return false;
|
|
658
|
-
if (side && sideForItem(item
|
|
648
|
+
if (side && sideForItem(item, itemType) !== side) return false;
|
|
659
649
|
return true;
|
|
660
650
|
});
|
|
661
651
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Assert selected standalone orders, trigger fields and parent/child protection links.
|
|
2
|
+
// Checks observed state without placing or canceling orders.
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
getCoreController,
|
|
3
6
|
isDirectRun,
|
|
@@ -8,11 +11,6 @@ import {
|
|
|
8
11
|
selectedItems,
|
|
9
12
|
} from './_controller.mjs';
|
|
10
13
|
|
|
11
|
-
// core Assert selected live Perps open orders are present or absent. Pure read
|
|
12
|
-
// over the controller's standalone getOpenOrders path (no signer / provider init
|
|
13
|
-
// needed) — throws on mismatch so the recipe fails loudly. Mirrors
|
|
14
|
-
// assert_positions.mjs.
|
|
15
|
-
//
|
|
16
14
|
// Optional expect_* fields additionally assert the TRIGGER DATA the exchange
|
|
17
15
|
// round-tripped for every matching order: expect_trigger_order_type (the
|
|
18
16
|
// normalized placement type, e.g. stop_market), expect_trigger_price,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Assert required presence, absence or count of selected standalone Perps positions.
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
getCoreController,
|
|
3
5
|
isDirectRun,
|
|
@@ -8,10 +10,6 @@ import {
|
|
|
8
10
|
selectedItems,
|
|
9
11
|
} from './_controller.mjs';
|
|
10
12
|
|
|
11
|
-
// core Assert selected live Perps positions are present or absent. Pure read
|
|
12
|
-
// over the controller's standalone path (no signer / provider init needed) —
|
|
13
|
-
// throws on mismatch so the recipe fails loudly.
|
|
14
|
-
|
|
15
13
|
export function expectedOpen(input) {
|
|
16
14
|
if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
|
|
17
15
|
const state = String(input.node.state).toLowerCase();
|
|
@@ -39,7 +37,7 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
|
39
37
|
standalone: true,
|
|
40
38
|
userAddress: accountAddress,
|
|
41
39
|
});
|
|
42
|
-
matching = selectedItems(input, positions);
|
|
40
|
+
matching = selectedItems(input, positions, 'position');
|
|
43
41
|
const reached = expectedCount === undefined
|
|
44
42
|
? matching.length > 0
|
|
45
43
|
: matching.length === Number(expectedCount);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// Cancel selected testnet orders by a fixed ID set and verify their absence.
|
|
2
|
+
// Optional expected IDs reject changed selection before signing and dispatch.
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
controllerRejection,
|
|
6
|
+
getCoreController,
|
|
3
7
|
getCoreControllerWithSigner,
|
|
4
8
|
isDirectRun,
|
|
5
9
|
redactOrder,
|
|
@@ -10,9 +14,6 @@ import {
|
|
|
10
14
|
optionalParam,
|
|
11
15
|
} from './_controller.mjs';
|
|
12
16
|
|
|
13
|
-
// Snapshot selection into exact IDs so dispatch and postcondition checks cannot
|
|
14
|
-
// expand to opposite-side or newly created orders sharing a market symbol.
|
|
15
|
-
|
|
16
17
|
function sleep(ms) {
|
|
17
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
19
|
}
|
|
@@ -46,7 +47,15 @@ export function cancelOrdersFailure(result) {
|
|
|
46
47
|
|
|
47
48
|
export async function closeOrders(input) {
|
|
48
49
|
requireExplicitSelection(input);
|
|
49
|
-
const
|
|
50
|
+
const expectedIds = input.node?.expected_order_ids;
|
|
51
|
+
if (expectedIds !== undefined && (!Array.isArray(expectedIds) ||
|
|
52
|
+
expectedIds.some((id) => typeof id !== 'string' || !id.trim()) ||
|
|
53
|
+
new Set(expectedIds).size !== expectedIds.length)) {
|
|
54
|
+
throw new Error('expected_order_ids must contain unique non-empty receipt IDs.');
|
|
55
|
+
}
|
|
56
|
+
const { controller, accountAddress, network } = await (expectedIds === undefined
|
|
57
|
+
? getCoreControllerWithSigner(input)
|
|
58
|
+
: getCoreController(input));
|
|
50
59
|
const timeoutMs = Number(
|
|
51
60
|
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
|
|
52
61
|
);
|
|
@@ -56,6 +65,14 @@ export async function closeOrders(input) {
|
|
|
56
65
|
userAddress: accountAddress,
|
|
57
66
|
});
|
|
58
67
|
const matching = selectedItems(input, orders);
|
|
68
|
+
const checkExpectedIds = (selected) => {
|
|
69
|
+
if (expectedIds === undefined) return;
|
|
70
|
+
const actual = selected.map((order) => order.orderId);
|
|
71
|
+
if (actual.length !== expectedIds.length || actual.some((id) => !expectedIds.includes(id))) {
|
|
72
|
+
throw new Error('CORE_ORDER_RECEIPT_MISMATCH: selected live orders do not match expected_order_ids; cancellation was not dispatched.');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
checkExpectedIds(matching);
|
|
59
76
|
const symbols = Array.from(new Set(matching.map(symbolForItem)));
|
|
60
77
|
|
|
61
78
|
if (symbols.length === 0) {
|
|
@@ -76,6 +93,13 @@ export async function closeOrders(input) {
|
|
|
76
93
|
if (orderIds.some((id) => typeof id !== 'string' || !id.trim()) || new Set(orderIds).size !== orderIds.length) {
|
|
77
94
|
throw new Error('Selected Core orders require unique non-empty orderId values before cancellation.\nNext: mm-harness actions metamask.perps.read_orders --adapter core --json');
|
|
78
95
|
}
|
|
96
|
+
if (expectedIds !== undefined) {
|
|
97
|
+
await getCoreControllerWithSigner(input);
|
|
98
|
+
checkExpectedIds(selectedItems(input, await controller.getOpenOrders({
|
|
99
|
+
standalone: true,
|
|
100
|
+
userAddress: accountAddress,
|
|
101
|
+
})));
|
|
102
|
+
}
|
|
79
103
|
const result = await controller.cancelOrders({ orderIds });
|
|
80
104
|
const failure = cancelOrdersFailure(result);
|
|
81
105
|
if (failure !== undefined) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
// Close selected testnet positions and verify their absence.
|
|
2
|
+
// Optional receipt guards bind the observed position, reducing side and maximum quantity.
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
controllerRejection,
|
|
3
6
|
currentMarketPrice,
|
|
7
|
+
getCoreController,
|
|
4
8
|
getCoreControllerWithSigner,
|
|
5
9
|
isDirectRun,
|
|
6
10
|
redactPosition,
|
|
@@ -11,12 +15,6 @@ import {
|
|
|
11
15
|
optionalParam,
|
|
12
16
|
} from './_controller.mjs';
|
|
13
17
|
|
|
14
|
-
// core Close selected live Perps positions on HyperLiquid testnet by driving the
|
|
15
|
-
// headless PerpsController.closePosition() per matching position (full close)
|
|
16
|
-
// through the full signing/provider path (Slice 2). Mirrors the extension
|
|
17
|
-
// adapter: read → close each matching position with a fresh price → verify the
|
|
18
|
-
// selected positions are gone.
|
|
19
|
-
|
|
20
18
|
function sleep(ms) {
|
|
21
19
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
20
|
}
|
|
@@ -45,7 +43,17 @@ export function closePositionsFailure(results) {
|
|
|
45
43
|
|
|
46
44
|
export async function closePositions(input) {
|
|
47
45
|
requireExplicitSelection(input);
|
|
48
|
-
const
|
|
46
|
+
const expectedPositions = input.node?.expected_positions;
|
|
47
|
+
if (expectedPositions !== undefined && (!Array.isArray(expectedPositions) || expectedPositions.length > 1 ||
|
|
48
|
+
expectedPositions.some((position) => !position || typeof position.coin !== 'string' ||
|
|
49
|
+
!position.coin.trim() || !Number.isFinite(Number(position.size)) || Number(position.size) === 0 ||
|
|
50
|
+
!Number.isFinite(Number(position.entryPrice)) || Number(position.entryPrice) <= 0) ||
|
|
51
|
+
new Set(expectedPositions.map((position) => position.coin)).size !== expectedPositions.length)) {
|
|
52
|
+
throw new Error('expected_positions must contain at most one market with nonzero signed size and positive entryPrice.');
|
|
53
|
+
}
|
|
54
|
+
const { controller, accountAddress, network } = await (expectedPositions === undefined
|
|
55
|
+
? getCoreControllerWithSigner(input)
|
|
56
|
+
: getCoreController(input));
|
|
49
57
|
const timeoutMs = Number(
|
|
50
58
|
optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
|
|
51
59
|
);
|
|
@@ -61,7 +69,20 @@ export async function closePositions(input) {
|
|
|
61
69
|
standalone: true,
|
|
62
70
|
userAddress: accountAddress,
|
|
63
71
|
});
|
|
64
|
-
const matching = selectedItems(input, positions);
|
|
72
|
+
const matching = selectedItems(input, positions, 'position');
|
|
73
|
+
const checkExpectedPositions = (selected) => {
|
|
74
|
+
if (expectedPositions === undefined) return;
|
|
75
|
+
if (selected.length !== expectedPositions.length || selected.some((position) => {
|
|
76
|
+
const actual = redactPosition(position);
|
|
77
|
+
const expected = expectedPositions.find((item) => item.coin === actual.coin);
|
|
78
|
+
return !expected || Number(expected.size) !== Number(actual.size) ||
|
|
79
|
+
Number(expected.entryPrice) !== Number(actual.entryPrice) ||
|
|
80
|
+
(expected.side != null && expected.side !== actual.side);
|
|
81
|
+
})) {
|
|
82
|
+
throw new Error('CORE_POSITION_RECEIPT_MISMATCH: selected live position size, side or entry price changed; close was not dispatched.');
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
checkExpectedPositions(matching);
|
|
65
86
|
const symbols = Array.from(new Set(matching.map(symbolForItem)));
|
|
66
87
|
|
|
67
88
|
if (symbols.length === 0) {
|
|
@@ -80,13 +101,29 @@ export async function closePositions(input) {
|
|
|
80
101
|
|
|
81
102
|
const results = [];
|
|
82
103
|
let successCount = 0;
|
|
104
|
+
if (expectedPositions !== undefined) await getCoreControllerWithSigner(input);
|
|
83
105
|
for (const position of matching) {
|
|
84
106
|
const symbol = symbolForItem(position);
|
|
85
107
|
const currentPrice = await currentMarketPrice(controller, symbol);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
if (expectedPositions !== undefined) {
|
|
109
|
+
checkExpectedPositions(selectedItems(input, await controller.getPositions({
|
|
110
|
+
standalone: true,
|
|
111
|
+
userAddress: accountAddress,
|
|
112
|
+
}), 'position'));
|
|
113
|
+
}
|
|
114
|
+
// Guarded closes keep receipt direction and quantity fixed. A venue position
|
|
115
|
+
// is netted, so matching fields cannot distinguish an identical replacement.
|
|
116
|
+
const expected = expectedPositions?.find((item) => item.coin === symbol);
|
|
117
|
+
const result = expected ? await controller.placeOrder({
|
|
118
|
+
symbol,
|
|
119
|
+
isBuy: Number(expected.size) < 0,
|
|
120
|
+
size: String(Math.abs(Number(expected.size))),
|
|
121
|
+
orderType: 'market',
|
|
122
|
+
reduceOnly: true,
|
|
123
|
+
currentPrice,
|
|
124
|
+
priceAtCalculation: currentPrice,
|
|
125
|
+
maxSlippageBps,
|
|
126
|
+
}) : await controller.closePosition({
|
|
90
127
|
symbol,
|
|
91
128
|
orderType: 'market',
|
|
92
129
|
currentPrice,
|
|
@@ -128,6 +165,10 @@ export async function closePositions(input) {
|
|
|
128
165
|
results,
|
|
129
166
|
positions: stillOpen.map(redactPosition),
|
|
130
167
|
proofPath: 'perps-controller-closePosition',
|
|
168
|
+
...(expectedPositions === undefined ? {} : {
|
|
169
|
+
expectedPositions,
|
|
170
|
+
proofPath: 'perps-controller-placeOrder-reduce-only-receipt-size',
|
|
171
|
+
}),
|
|
131
172
|
};
|
|
132
173
|
}
|
|
133
174
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
// Converge selected testnet orders to open or absent state, then assert the result.
|
|
2
|
+
// Reuse an existing matching order instead of placing another.
|
|
3
|
+
|
|
1
4
|
import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
2
5
|
import { assertOrders } from './assert_orders.mjs';
|
|
3
6
|
import { closeOrders } from './close_orders.mjs';
|
|
4
7
|
import { placeOrder } from './place_order.mjs';
|
|
5
8
|
|
|
6
|
-
// core Higher-level wrapper that reads selected open orders, converges them to the
|
|
7
|
-
// requested `state` (open/none) by canceling or placing, then asserts the final
|
|
8
|
-
// order state. Mirrors ensure_positions.mjs.
|
|
9
|
-
|
|
10
9
|
export async function ensureOrders(input) {
|
|
11
10
|
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
12
11
|
requireExplicitSelection(input);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
// Converge selected testnet positions to open or absent state, then assert the result.
|
|
2
|
+
// Open a position only when the selected state requires one.
|
|
3
|
+
|
|
1
4
|
import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
2
5
|
import { assertPositions } from './assert_positions.mjs';
|
|
3
6
|
import { closePositions } from './close_positions.mjs';
|
|
4
7
|
import { placeOrder } from './place_order.mjs';
|
|
5
8
|
|
|
6
|
-
// core Higher-level wrapper that reads selected positions, converges them to the
|
|
7
|
-
// requested `state` (open/none) by placing or closing, then asserts the final
|
|
8
|
-
// state. Mirrors the extension ensure_positions semantics.
|
|
9
|
-
|
|
10
9
|
export async function ensurePositions(input) {
|
|
11
10
|
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
12
11
|
requireExplicitSelection(input);
|
|
@@ -26,7 +25,7 @@ export async function ensurePositions(input) {
|
|
|
26
25
|
userAddress: accountAddress,
|
|
27
26
|
});
|
|
28
27
|
let order = null;
|
|
29
|
-
if (selectedItems(input, positions).length === 0) {
|
|
28
|
+
if (selectedItems(input, positions, 'position').length === 0) {
|
|
30
29
|
order = await placeOrder(input);
|
|
31
30
|
}
|
|
32
31
|
const assertion = await assertPositions(input, true);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Submit and verify testnet market, limit or trigger orders through the headless controller.
|
|
2
|
+
// Supports declared rejection cases and attached protection parameters.
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
configuredSymbols,
|
|
3
6
|
controllerRejection,
|
|
@@ -12,24 +15,14 @@ import {
|
|
|
12
15
|
selectedItems,
|
|
13
16
|
} from './_controller.mjs';
|
|
14
17
|
|
|
15
|
-
// core Place a real Perps order on HyperLiquid testnet by driving the headless
|
|
16
|
-
// PerpsController.placeOrder() through the full signing/provider path (Slice 2).
|
|
17
|
-
// Mirrors the extension adapter's input vocabulary and notional→size math:
|
|
18
|
-
// size = (usdAmount * leverage) / currentPrice
|
|
19
|
-
// The HyperLiquidProvider recalculates size from usdAmount internally; we still
|
|
20
|
-
// pass a derived size for the controller's own validation, plus currentPrice/
|
|
21
|
-
// priceAtCalculation so the provider's slippage guard has a fresh anchor.
|
|
22
|
-
//
|
|
23
18
|
// LIMIT orders (orderType=limit) place a RESTING order on the book instead of
|
|
24
19
|
// filling: the provider sends tif=Gtc and uses the provided `price` verbatim
|
|
25
20
|
// (calculateOrderPriceAndSize / buildOrdersArray in core's orderCalculations.ts).
|
|
26
21
|
// The limit price is derived from the live mid by `offset_pct` (e.g. -30 places a
|
|
27
|
-
// BUY 30% below mid
|
|
22
|
+
// BUY 30% below mid), or supplied absolutely via `limit_price`.
|
|
28
23
|
// For a limit order we size from the LIMIT price (not mid) and omit usdAmount so
|
|
29
|
-
// the controller
|
|
30
|
-
//
|
|
31
|
-
// though the limit sits far from mid. After placing we verify the resting open
|
|
32
|
-
// order exists (not a filled position).
|
|
24
|
+
// the controller receives an explicit size; venue quantity normalization still
|
|
25
|
+
// applies. After placing we verify the resting open order exists, not a filled position.
|
|
33
26
|
//
|
|
34
27
|
// TRIGGER placements (order_type=stop_market | stop_limit | take_profit_market |
|
|
35
28
|
// take_profit_limit) rest off-book until `trigger_price` is reached, then execute
|
|
@@ -567,7 +560,7 @@ export async function placeOrder(input) {
|
|
|
567
560
|
userAddress: accountAddress,
|
|
568
561
|
});
|
|
569
562
|
}
|
|
570
|
-
const matchingPositions = selectedItems(input, positions);
|
|
563
|
+
const matchingPositions = selectedItems(input, positions, 'position');
|
|
571
564
|
|
|
572
565
|
if (restsOnBook && matchingOrders.length === 0) {
|
|
573
566
|
throw new Error(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Read standalone Perps positions and return redacted selected data with total/matching counts.
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
getCoreController,
|
|
3
5
|
isDirectRun,
|
|
@@ -12,7 +14,7 @@ export async function readPositions(input) {
|
|
|
12
14
|
standalone: true,
|
|
13
15
|
userAddress: accountAddress,
|
|
14
16
|
});
|
|
15
|
-
const matching = selectedItems(input, positions);
|
|
17
|
+
const matching = selectedItems(input, positions, 'position');
|
|
16
18
|
return {
|
|
17
19
|
action: input.action,
|
|
18
20
|
source: 'perps-controller-standalone',
|
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
// Prepare selected testnet baselines before proof, handling positions before orders.
|
|
2
|
+
// Omitted domains are skipped; this headless action performs no UI navigation.
|
|
3
|
+
|
|
1
4
|
import { isDirectRun, resolveNetwork, runAdapter } from './_controller.mjs';
|
|
2
5
|
import { ensureOrders } from './ensure_orders.mjs';
|
|
3
6
|
import { ensurePositions } from './ensure_positions.mjs';
|
|
4
7
|
|
|
5
|
-
// core Headless composite that converges the POSITION and ORDER baselines for the
|
|
6
|
-
// selected market(s) on HyperLiquid testnet before the proof window. Mirrors the
|
|
7
|
-
// extension perps-domain-dispatcher start_state semantics but SKIPS every
|
|
8
|
-
// navigation/page/HUD step — headless core has no UI surface.
|
|
9
|
-
//
|
|
10
|
-
// Params (extension schema, testnet-default): profile, market(s)/symbol(s),
|
|
11
|
-
// positions{state,...}, orders{state,...}, network. The UI-only `page`/`hud`
|
|
12
|
-
// fields are accepted by the schema but intentionally ignored here.
|
|
13
|
-
//
|
|
14
|
-
// A baseline block converges to its `state` by composing the ensure_* helpers; a
|
|
15
|
-
// block set to `false` (or omitted) skips that domain. Position convergence runs
|
|
16
|
-
// first (closing a position also clears its TP/SL orders), then order
|
|
17
|
-
// convergence, so the final order baseline is authoritative.
|
|
18
|
-
|
|
19
8
|
const BASELINE_KEYS = [
|
|
20
9
|
'market',
|
|
21
10
|
'symbol',
|