@deeeed/metamask-harness 0.38.1 → 0.40.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 +58 -0
- package/adapters/extension/lib/playwright-cdp.cjs +17 -1
- package/adapters/extension/sidepanel-toggle.sh +135 -13
- package/dist/adapters/mobile/release-artifact.js +65 -0
- package/dist/adapters.js +27 -17
- package/dist/command-contract.js +10 -2
- package/dist/commands/call.js +7 -4
- package/dist/commands/launch/index.js +55 -3
- package/dist/commands/parse-args.js +6 -0
- package/dist/commands/run-engine.js +75 -39
- package/dist/funding-execution-context.js +1386 -0
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +170 -3
- package/dist/mm-harness-cli.js +8 -2
- package/dist/recipe-security.js +5 -12
- package/dist/runner.js +102 -6
- package/docs/RECIPES.md +74 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +6 -5
- package/library/actions/extension/assets/open_details.mjs +30 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +5 -0
- package/library/actions/extension/networks/add_chainlist.mjs +4 -0
- package/library/actions/extension/networks/add_custom.mjs +4 -0
- package/library/actions/extension/networks/custom_network.mjs +48 -1
- package/library/actions/extension/perps/assert_visible_consistency.mjs +366 -0
- package/library/actions/extension/perps/close_visible_position.mjs +299 -37
- package/library/actions/extension/perps/compare_provider_market.mjs +77 -0
- package/library/actions/extension/perps/edit_margin.mjs +285 -44
- package/library/actions/extension/perps/mutation-receipt.mjs +781 -0
- package/library/actions/extension/perps/perps.mjs +1029 -363
- package/library/actions/extension/perps/read_funds_confirmation.mjs +126 -13
- package/library/actions/extension/perps/read_snapshot.mjs +124 -3
- package/library/actions/extension/perps/read_visible_state.mjs +63 -5
- package/library/actions/extension/perps/search_markets.mjs +32 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +81 -18
- package/library/actions/extension/perps/select_market_filter.mjs +48 -4
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +251 -69
- package/library/actions/extension/perps/visible-mutation-identity.mjs +164 -0
- package/library/actions/extension/platform/cdp.mjs +88 -9
- package/library/actions/extension/settings/set_basic_functionality.mjs +35 -7
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +2 -6
- package/library/actions/extension/swap_bridge/set_amount.mjs +11 -2
- package/library/actions/extension/ui/navigate.mjs +45 -5
- package/library/actions/extension/wallet/import.mjs +70 -29
- package/library/actions/extension/wallet/lock.mjs +62 -5
- package/library/actions/extension/wallet/visible-session.mjs +218 -0
- package/library/actions/mobile/assets/import_custom_token.mjs +181 -0
- package/library/actions/mobile/assets/open_details.mjs +43 -0
- package/library/actions/mobile/assets/read_details.mjs +96 -0
- package/library/actions/mobile/assets/set_token_visibility.mjs +302 -0
- package/library/actions/mobile/assets/verify_sorting.mjs +104 -0
- package/library/actions/mobile/networks/add_custom.mjs +125 -0
- package/library/actions/mobile/networks/network-management.mjs +132 -0
- package/library/actions/mobile/networks/read_visible_state.mjs +43 -0
- package/library/actions/mobile/networks/remove_custom.mjs +100 -0
- package/library/actions/mobile/perps/search_markets.mjs +46 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +117 -0
- package/library/actions/mobile/platform/native-session-name.mjs +14 -0
- package/library/actions/mobile/platform/native-session.mjs +59 -0
- package/library/actions/mobile/platform/observe-ui.mjs +9 -1
- package/library/actions/mobile/swap_bridge/native-session.mjs +10 -45
- package/library/actions/mobile/swap_bridge/select_assets.mjs +15 -1
- package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -1
- package/library/actions/mobile/swap_bridge/set_max_amount.mjs +55 -0
- package/library/actions/mobile/swap_bridge/set_slippage.mjs +96 -0
- package/library/actions/mobile/swap_bridge/submit_transaction.mjs +141 -0
- package/library/actions/mobile/ui/native-navigation.mjs +120 -23
- package/library/actions/mobile/ui/navigate.mjs +13 -2
- package/library/actions/mobile/wallet/import.mjs +17 -10
- package/library/actions/mobile/wallet/lock.mjs +14 -0
- package/library/actions/mobile/wallet/native-ui.mjs +88 -15
- package/library/actions/shared/perps/visible-state.mjs +33 -11
- package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
- package/library/manifests/extension.action-manifest.json +1707 -1241
- package/library/manifests/mobile.action-manifest.json +308 -1
- package/library/recipes/assets/release-token-details.recipe.json +5 -4
- package/library/recipes/assets/release-token-hide-readd.recipe.json +2 -0
- package/library/recipes/{assets → extension/assets}/release-custom-network-token-persistence.recipe.json +1 -0
- package/library/recipes/{perps → extension/perps}/release-activity-filters.recipe.json +5 -5
- package/library/recipes/{perps → extension/perps}/release-funds-flows.recipe.json +3 -3
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +24 -19
- package/library/recipes/extension/perps/release-market-filters.recipe.json +97 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +4 -1
- package/library/recipes/extension/perps/release-order-validation.recipe.json +53 -14
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +47 -0
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +76 -0
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +90 -0
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +58 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +30 -13
- package/library/recipes/perps/release-market-details.recipe.json +1 -1
- package/library/recipes/perps/release-market-search.recipe.json +8 -4
- package/library/recipes/perps/release-watchlist.recipe.json +31 -19
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +5 -13
- package/library/recipes/swap-bridge/release-quote.recipe.json +1 -0
- package/library/recipes/wallet/import.recipe.json +4 -14
- package/package.json +1 -1
- package/library/actions/extension/assets/finish_send.mjs +0 -128
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +0 -363
- package/library/actions/extension/ui/activate_and_observe.mjs +0 -96
- package/library/recipes/assets/release-custom-gas-send.recipe.json +0 -88
- package/library/recipes/assets/release-native-max-cancel.recipe.json +0 -70
- package/library/recipes/assets/release-native-max-send.recipe.json +0 -78
- package/library/recipes/assets/release-own-account-send.recipe.json +0 -80
- package/library/recipes/extension/runner/action-validation.recipe.json +0 -343
|
@@ -1,34 +1,142 @@
|
|
|
1
|
-
import { dataTestId,
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
assertTrustedExternalMutationTarget,
|
|
4
|
+
externalMutationNotDispatched,
|
|
5
|
+
readExternalMutationReceipt,
|
|
6
|
+
readPriorMutationReceipt,
|
|
7
|
+
runGuardedExternalMutation,
|
|
8
|
+
trustedExternalMutationIdentity,
|
|
9
|
+
withExternalMutationRecovery,
|
|
10
|
+
} from './mutation-receipt.mjs';
|
|
11
|
+
import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
|
|
12
|
+
import {
|
|
13
|
+
absoluteDelta,
|
|
14
|
+
normalizePositionSizeIdentity,
|
|
15
|
+
normalizedNumericInputMatches,
|
|
16
|
+
parsePositionDetail,
|
|
17
|
+
parseUsdBaseUnits,
|
|
18
|
+
positionIdentityMatches,
|
|
19
|
+
waitForStableGlobalPerpsHome,
|
|
20
|
+
} from './visible-mutation-identity.mjs';
|
|
2
21
|
|
|
3
|
-
|
|
4
|
-
const market =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!market) throw new Error('metamask.perps.edit_margin requires market=<symbol>.');
|
|
8
|
-
if (mode !== 'add' && mode !== 'remove') {
|
|
9
|
-
throw new Error(`metamask.perps.edit_margin received unsupported margin_action ${JSON.stringify(mode)}.`);
|
|
10
|
-
}
|
|
11
|
-
if (!Number.isFinite(amount) || amount <= 0) {
|
|
12
|
-
throw new Error('metamask.perps.edit_margin requires a positive amount.');
|
|
22
|
+
function marketSymbol(value) {
|
|
23
|
+
const market = String(value ?? '').trim().toUpperCase();
|
|
24
|
+
if (!/^[A-Z0-9:_-]{2,20}$/u.test(market)) {
|
|
25
|
+
throw new Error('metamask.perps.edit_margin requires market=<symbol>.');
|
|
13
26
|
}
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
return market;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function decimalBaseUnits(value) {
|
|
31
|
+
const units = parseUsdBaseUnits(value);
|
|
32
|
+
if (!/^[1-9][0-9]{0,29}$/u.test(String(units ?? ''))) {
|
|
33
|
+
throw new Error('metamask.perps.edit_margin requires a positive amount with at most six decimals.');
|
|
16
34
|
}
|
|
35
|
+
return units;
|
|
36
|
+
}
|
|
17
37
|
|
|
38
|
+
async function readPosition(page, market) {
|
|
18
39
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
19
|
-
await page.waitForSelector(dataTestId('perps-position-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
41
|
+
const raw = await page.evaluate(`(() => {
|
|
42
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
43
|
+
.replace(/\\s+/gu, ' ')
|
|
44
|
+
.trim()
|
|
45
|
+
.slice(0, 500);
|
|
46
|
+
return {
|
|
47
|
+
route: String(location.hash),
|
|
48
|
+
pair: read('perps-market-detail-pair'),
|
|
49
|
+
direction: read('perps-position-leverage'),
|
|
50
|
+
size: read('perps-position-size-value'),
|
|
51
|
+
margin: read('perps-position-margin-value'),
|
|
52
|
+
liquidation: read('perps-position-liquidation-value'),
|
|
53
|
+
};
|
|
54
|
+
})()`);
|
|
55
|
+
const parsed = parsePositionDetail(raw);
|
|
56
|
+
if (!parsed || !raw.route.includes('/perps/market/') || !raw.pair.toUpperCase().startsWith(`${market}-`)) {
|
|
57
|
+
throw new Error('Visible Perps position identity is unavailable or does not match the requested market.');
|
|
58
|
+
}
|
|
59
|
+
return { ...raw, ...parsed };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactTarget(facts) {
|
|
63
|
+
const baseline = facts?.positionBefore;
|
|
64
|
+
const amountBaseUnits = String(facts?.amountBaseUnits ?? '');
|
|
65
|
+
const mode = String(facts?.marginAction ?? '');
|
|
66
|
+
if (!baseline || !/^[1-9][0-9]{0,29}$/u.test(amountBaseUnits) || !/^(?:add|remove)$/u.test(mode)) {
|
|
67
|
+
throw new Error('Margin receipt has no valid exact baseline.');
|
|
68
|
+
}
|
|
69
|
+
const target = BigInt(baseline.marginBaseUnits) + (mode === 'add' ? 1n : -1n) * BigInt(amountBaseUnits);
|
|
70
|
+
if (target <= 0n) throw new Error('Margin receipt target is invalid.');
|
|
71
|
+
return `${target}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function observeMargin(page, identity, facts, balanceTarget, balanceToleranceBaseUnits, timeoutMs) {
|
|
75
|
+
const targetMarginBaseUnits = exactTarget(facts);
|
|
76
|
+
const deadline = Date.now() + timeoutMs;
|
|
77
|
+
let stable = null;
|
|
78
|
+
let stableSince = 0;
|
|
79
|
+
let after;
|
|
80
|
+
while (Date.now() <= deadline) {
|
|
81
|
+
after = await readPosition(page, identity.market);
|
|
82
|
+
const exact = after.marginBaseUnits === targetMarginBaseUnits
|
|
83
|
+
&& after.size === facts.positionBefore.size
|
|
84
|
+
&& after.direction === facts.positionBefore.direction
|
|
85
|
+
&& after.liquidation !== facts.positionBefore.liquidation
|
|
86
|
+
&& positionIdentityMatches(after, {
|
|
87
|
+
market: identity.market,
|
|
88
|
+
side: identity.side,
|
|
89
|
+
leverage: identity.leverage,
|
|
90
|
+
matchMargin: false,
|
|
91
|
+
});
|
|
92
|
+
if (exact) {
|
|
93
|
+
const key = JSON.stringify(after);
|
|
94
|
+
if (key !== stable) {
|
|
95
|
+
stable = key;
|
|
96
|
+
stableSince = Date.now();
|
|
97
|
+
} else if (Date.now() - stableSince >= 1_000) break;
|
|
98
|
+
} else {
|
|
99
|
+
stable = null;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
102
|
+
}
|
|
103
|
+
if (!stable) throw new Error('exact margin, liquidation, size, and leverage postcondition was not visible');
|
|
104
|
+
await page.navigateHash('#/perps-home');
|
|
105
|
+
const home = await waitForStableGlobalPerpsHome(page, { timeoutMs, requireZero: false });
|
|
106
|
+
const balanceDeltaBaseUnits = balanceTarget == null
|
|
107
|
+
? null
|
|
108
|
+
: absoluteDelta(home.balanceBaseUnits, balanceTarget);
|
|
109
|
+
if (balanceDeltaBaseUnits != null && BigInt(balanceDeltaBaseUnits) > BigInt(balanceToleranceBaseUnits)) {
|
|
110
|
+
throw new Error('Margin cleanup did not reconcile the visible Perps balance.');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
positionBefore: facts.positionBefore,
|
|
115
|
+
positionAfter: after,
|
|
116
|
+
targetMarginBaseUnits,
|
|
117
|
+
balanceBeforeBaseUnits: String(facts.balanceBeforeBaseUnits),
|
|
118
|
+
balanceAfterBaseUnits: home.balanceBaseUnits,
|
|
119
|
+
balanceReconciliationTargetBaseUnits: balanceTarget,
|
|
120
|
+
balanceDeltaBaseUnits,
|
|
121
|
+
balanceToleranceBaseUnits,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function prepareMarginModal(page, mode, amount) {
|
|
24
126
|
await page.click(dataTestId('perps-margin-card'));
|
|
25
127
|
const menuItem = mode === 'add' ? 'perps-margin-menu-add' : 'perps-margin-menu-remove';
|
|
26
128
|
await page.waitForSelector(dataTestId(menuItem), { timeoutMs: 10_000 });
|
|
27
129
|
await page.click(dataTestId(menuItem));
|
|
28
130
|
const modal = mode === 'add' ? 'perps-add-margin-modal' : 'perps-decrease-margin-modal';
|
|
29
131
|
await page.waitForSelector(dataTestId(modal), { timeoutMs: 10_000 });
|
|
30
|
-
|
|
31
|
-
|
|
132
|
+
const amountSelector = `${dataTestId('perps-edit-margin-amount-input')} input`;
|
|
133
|
+
try {
|
|
134
|
+
await page.setInput(amountSelector, amount);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!/Input did not retain the supplied value length/u.test(String(error?.message ?? error))) throw error;
|
|
137
|
+
const retained = await page.evaluate(`document.querySelector(${JSON.stringify(amountSelector)})?.value ?? null`);
|
|
138
|
+
if (!normalizedNumericInputMatches(retained, amount)) throw error;
|
|
139
|
+
}
|
|
32
140
|
const saveDeadline = Date.now() + 10_000;
|
|
33
141
|
let ready;
|
|
34
142
|
while (Date.now() <= saveDeadline) {
|
|
@@ -42,41 +150,174 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
42
150
|
liquidation: String(document.querySelector('[data-testid="perps-edit-margin-liquidation-price-value"]')?.textContent || '').trim(),
|
|
43
151
|
};
|
|
44
152
|
})()`);
|
|
45
|
-
if (ready.saveEnabled) break;
|
|
153
|
+
if (Number(ready.amount) === Number(amount) && ready.saveEnabled) break;
|
|
46
154
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
155
|
}
|
|
48
|
-
if (Number(ready
|
|
156
|
+
if (!ready?.saveEnabled || Number(ready.amount) !== Number(amount)) {
|
|
49
157
|
throw new Error(`Perps margin update is not ready: ${JSON.stringify(ready)}.`);
|
|
50
158
|
}
|
|
51
|
-
|
|
159
|
+
return ready;
|
|
160
|
+
}
|
|
52
161
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
162
|
+
export async function editMargin(input) {
|
|
163
|
+
return withExtensionPage(input, async (page) => {
|
|
164
|
+
const market = marketSymbol(input.node?.market);
|
|
165
|
+
const mode = String(input.node?.margin_action ?? 'add').trim().toLowerCase();
|
|
166
|
+
const amount = String(input.node?.amount ?? '');
|
|
167
|
+
const amountBaseUnits = decimalBaseUnits(amount);
|
|
168
|
+
const fundingSide = String(input.node?.funding_side ?? '').trim().toLowerCase();
|
|
169
|
+
const fundingAmountBaseUnits = decimalBaseUnits(input.node?.funding_amount);
|
|
170
|
+
const fundingPositionSize = normalizePositionSizeIdentity(input.node?.funding_position_size);
|
|
171
|
+
const leverage = Number(input.node?.leverage);
|
|
172
|
+
const balanceToleranceBaseUnits = String(input.node?.balance_reconciliation_tolerance_base_units ?? '1000000');
|
|
173
|
+
if (!/^(?:add|remove)$/u.test(mode) || !/^(?:long|short)$/u.test(fundingSide)
|
|
174
|
+
|| fundingPositionSize == null
|
|
175
|
+
|| !Number.isInteger(leverage) || leverage < 1 || leverage > 100
|
|
176
|
+
|| !/^(?:0|[1-9][0-9]{0,6})$/u.test(balanceToleranceBaseUnits)
|
|
177
|
+
|| BigInt(balanceToleranceBaseUnits) > 1_000_000n) {
|
|
178
|
+
throw new Error('metamask.perps.edit_margin received invalid funded position inputs.');
|
|
179
|
+
}
|
|
180
|
+
if (input.node?.confirm_live !== true) {
|
|
181
|
+
throw new Error('metamask.perps.edit_margin requires confirm_live=true for live submission.');
|
|
182
|
+
}
|
|
183
|
+
const identity = trustedExternalMutationIdentity(input, page, {
|
|
184
|
+
operation: 'perps-margin-adjust',
|
|
185
|
+
authorizationCount: 2,
|
|
186
|
+
provider: input.node?.provider,
|
|
187
|
+
network: input.node?.network,
|
|
188
|
+
market,
|
|
189
|
+
side: fundingSide,
|
|
190
|
+
amount,
|
|
191
|
+
leverage,
|
|
192
|
+
orderType: 'margin',
|
|
193
|
+
positionMode: 'modify',
|
|
194
|
+
marginAction: mode,
|
|
195
|
+
positionAmountBaseUnits: fundingAmountBaseUnits,
|
|
196
|
+
positionSize: fundingPositionSize,
|
|
197
|
+
roundingToleranceBaseUnits: '0',
|
|
198
|
+
});
|
|
199
|
+
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
200
|
+
let balanceTarget = null;
|
|
201
|
+
if (mode === 'remove') {
|
|
202
|
+
const addReceipt = await readPriorMutationReceipt(
|
|
203
|
+
input,
|
|
204
|
+
identity,
|
|
205
|
+
'metamask.perps.edit_margin',
|
|
206
|
+
{ marginAction: 'add' },
|
|
207
|
+
);
|
|
208
|
+
if (addReceipt.record?.identity?.marginAction !== 'add' || addReceipt.record?.state !== 'succeeded') {
|
|
209
|
+
throw new Error('Margin removal requires one succeeded receipt-bound add operation.');
|
|
210
|
+
}
|
|
211
|
+
balanceTarget = String(addReceipt.record?.facts?.balanceBeforeBaseUnits ?? '');
|
|
212
|
+
if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceTarget)) {
|
|
213
|
+
throw new Error('Prior margin receipt has no valid balance baseline.');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (existingReceipt) {
|
|
217
|
+
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
218
|
+
const mutation = await runGuardedExternalMutation({
|
|
219
|
+
input,
|
|
220
|
+
identity,
|
|
221
|
+
dispatch: async () => { throw new Error('resumed margin mutation must not dispatch'); },
|
|
222
|
+
observe: async ({ receipt }) => observeMargin(
|
|
223
|
+
page,
|
|
224
|
+
identity,
|
|
225
|
+
receipt.facts,
|
|
226
|
+
balanceTarget,
|
|
227
|
+
balanceToleranceBaseUnits,
|
|
228
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
action: input.action,
|
|
233
|
+
market,
|
|
234
|
+
marginAction: mode,
|
|
235
|
+
amount,
|
|
236
|
+
receipt: mutation.receipt,
|
|
237
|
+
resumed: true,
|
|
238
|
+
outcome: mutation.observation,
|
|
239
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
240
|
+
};
|
|
241
|
+
});
|
|
68
242
|
}
|
|
69
|
-
|
|
70
|
-
|
|
243
|
+
|
|
244
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
|
|
245
|
+
const fundingBalance = await requireCurrentVisibleFundingBalance(input, page, timeoutMs);
|
|
246
|
+
const tradingContext = await requirePerpsTradingContext(input, page);
|
|
247
|
+
const before = await readPosition(page, market);
|
|
248
|
+
if (!positionIdentityMatches(before, { market, side: fundingSide, leverage, matchMargin: false })
|
|
249
|
+
|| normalizePositionSizeIdentity(before.size) !== fundingPositionSize) {
|
|
250
|
+
throw new Error('Visible Perps position does not match the funded market/side/leverage/size identity.');
|
|
71
251
|
}
|
|
252
|
+
const ready = await prepareMarginModal(page, mode, amount);
|
|
253
|
+
const facts = {
|
|
254
|
+
marginAction: mode,
|
|
255
|
+
amountBaseUnits,
|
|
256
|
+
balanceBeforeBaseUnits: fundingBalance.observedBaseUnits,
|
|
257
|
+
positionBefore: before,
|
|
258
|
+
fundingBalance,
|
|
259
|
+
tradingContext,
|
|
260
|
+
};
|
|
261
|
+
const mutation = await runGuardedExternalMutation({
|
|
262
|
+
input,
|
|
263
|
+
identity,
|
|
264
|
+
publicationFacts: facts,
|
|
265
|
+
dispatch: async () => {
|
|
266
|
+
let dispatchTarget;
|
|
267
|
+
try {
|
|
268
|
+
await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
|
|
269
|
+
await requirePerpsTradingContext(input, page);
|
|
270
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
271
|
+
const current = await readPosition(page, market);
|
|
272
|
+
if (current.marginBaseUnits !== before.marginBaseUnits || current.size !== before.size
|
|
273
|
+
|| current.direction !== before.direction
|
|
274
|
+
|| normalizePositionSizeIdentity(current.size) !== fundingPositionSize
|
|
275
|
+
|| !positionIdentityMatches(current, { market, side: fundingSide, leverage, matchMargin: false })) {
|
|
276
|
+
throw new Error('Visible Perps position changed before margin dispatch.');
|
|
277
|
+
}
|
|
278
|
+
await prepareMarginModal(page, mode, amount);
|
|
279
|
+
dispatchTarget = await page.evaluate(`(() => {
|
|
280
|
+
const element = document.querySelector('[data-testid="perps-edit-margin-modal-save"]');
|
|
281
|
+
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
282
|
+
throw new Error('Visible Perps margin submit control is unavailable.');
|
|
283
|
+
}
|
|
284
|
+
const rect = element.getBoundingClientRect();
|
|
285
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
286
|
+
})()`);
|
|
287
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
290
|
+
}
|
|
291
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
292
|
+
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
293
|
+
button: 'left', buttons: 1, clickCount: 1,
|
|
294
|
+
});
|
|
295
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
296
|
+
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
297
|
+
button: 'left', buttons: 0, clickCount: 1,
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
observe: async ({ receipt }) => observeMargin(
|
|
301
|
+
page,
|
|
302
|
+
identity,
|
|
303
|
+
receipt.facts,
|
|
304
|
+
balanceTarget,
|
|
305
|
+
balanceToleranceBaseUnits,
|
|
306
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
307
|
+
),
|
|
308
|
+
});
|
|
72
309
|
return {
|
|
73
310
|
action: input.action,
|
|
74
311
|
market,
|
|
75
312
|
marginAction: mode,
|
|
76
313
|
amount,
|
|
77
|
-
before,
|
|
78
314
|
ready,
|
|
79
|
-
|
|
80
|
-
|
|
315
|
+
receipt: mutation.receipt,
|
|
316
|
+
resumed: mutation.resumed,
|
|
317
|
+
outcome: mutation.observation,
|
|
318
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
81
319
|
};
|
|
82
|
-
})
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
runAdapter(editMargin);
|