@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,4 +1,23 @@
|
|
|
1
1
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
assertTrustedExternalMutationTarget,
|
|
4
|
+
externalMutationNotDispatched,
|
|
5
|
+
readExternalMutationReceipt,
|
|
6
|
+
readPriorMutationReceipt,
|
|
7
|
+
runGuardedExternalMutation,
|
|
8
|
+
trustedExternalMutationBudget,
|
|
9
|
+
trustedExternalMutationIdentity,
|
|
10
|
+
trustedExternalMutationPlanIsExact,
|
|
11
|
+
withExternalMutationRecovery,
|
|
12
|
+
} from './mutation-receipt.mjs';
|
|
13
|
+
import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
|
|
14
|
+
import {
|
|
15
|
+
absoluteDelta,
|
|
16
|
+
parsePositionDetail,
|
|
17
|
+
parseUsdBaseUnits,
|
|
18
|
+
positionIdentityMatches,
|
|
19
|
+
waitForStableGlobalPerpsHome,
|
|
20
|
+
} from './visible-mutation-identity.mjs';
|
|
2
21
|
|
|
3
22
|
function marketSymbol(value) {
|
|
4
23
|
const result = String(value ?? '').trim().toUpperCase();
|
|
@@ -8,18 +27,10 @@ function marketSymbol(value) {
|
|
|
8
27
|
return result;
|
|
9
28
|
}
|
|
10
29
|
|
|
11
|
-
|
|
12
|
-
const market = marketSymbol(input.node?.market);
|
|
13
|
-
const partialAmount = input.node?.amount == null ? null : Number(input.node.amount);
|
|
14
|
-
if (partialAmount != null && (!Number.isFinite(partialAmount) || partialAmount <= 0)) {
|
|
15
|
-
throw new Error('metamask.perps.close_visible_position amount must be positive.');
|
|
16
|
-
}
|
|
17
|
-
if (input.node?.confirm_live !== true) {
|
|
18
|
-
throw new Error('metamask.perps.close_visible_position requires confirm_live=true.');
|
|
19
|
-
}
|
|
30
|
+
async function readVisibleClosePosition(page, market) {
|
|
20
31
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
21
32
|
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
22
|
-
|
|
33
|
+
return page.evaluate(`(() => {
|
|
23
34
|
const market = ${JSON.stringify(market)};
|
|
24
35
|
const route = String(location.hash);
|
|
25
36
|
const symbol = String(document.querySelector('[data-testid="perps-market-detail-pair"]')?.innerText || '')
|
|
@@ -42,8 +53,12 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
42
53
|
margin: read('perps-position-margin-value'),
|
|
43
54
|
entryPrice: read('perps-position-entry-value'),
|
|
44
55
|
liquidationPrice: read('perps-position-liquidation-value'),
|
|
56
|
+
currentPrice: read('perps-market-detail-price'),
|
|
45
57
|
};
|
|
46
58
|
})()`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function prepareCloseModal(page, partialAmount) {
|
|
47
62
|
let confirmation;
|
|
48
63
|
for (let attempt = 0; attempt < 3 && !confirmation; attempt += 1) {
|
|
49
64
|
await page.click(dataTestId('perps-close-cta-button'));
|
|
@@ -94,40 +109,287 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
if (!confirmation.submitEnabled) throw new Error('Perps close confirmation is not submit-ready.');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
error: String(document.querySelector('[data-testid="perps-order-submit-error"]')?.innerText || '').trim(),
|
|
107
|
-
activity: [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
|
|
108
|
-
.slice(0, 5)
|
|
109
|
-
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500)),
|
|
110
|
-
}))()`);
|
|
111
|
-
if (finalState.error) throw new Error(`Perps close failed: ${finalState.error}`);
|
|
112
|
-
if (partialAmount != null) {
|
|
113
|
-
if (!finalState.modalVisible && finalState.positionVisible && finalState.positionSize !== before.size) break;
|
|
114
|
-
} else if (!finalState.positionVisible && !finalState.modalVisible) break;
|
|
115
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
116
|
-
}
|
|
117
|
-
const incomplete = partialAmount != null
|
|
118
|
-
? finalState?.modalVisible || !finalState?.positionVisible || finalState?.positionSize === before.size
|
|
119
|
-
: finalState?.positionVisible || finalState?.modalVisible;
|
|
120
|
-
if (incomplete) {
|
|
121
|
-
throw new Error('Perps position did not reach the requested close state after the timeout.');
|
|
112
|
+
return confirmation;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function proveGlobalCleanup(page, balanceBeforeBaseUnits, toleranceBaseUnits, timeoutMs = 20_000) {
|
|
116
|
+
await page.navigateHash('#/perps-home');
|
|
117
|
+
const state = await waitForStableGlobalPerpsHome(page, { timeoutMs, requireZero: true });
|
|
118
|
+
const balanceDeltaBaseUnits = absoluteDelta(state.balanceBaseUnits, balanceBeforeBaseUnits);
|
|
119
|
+
if (BigInt(balanceDeltaBaseUnits) > BigInt(toleranceBaseUnits)) {
|
|
120
|
+
throw new Error('Final visible Perps balance exceeds the authorized reconciliation tolerance.');
|
|
122
121
|
}
|
|
122
|
+
return {
|
|
123
|
+
positionCount: 0,
|
|
124
|
+
orderCount: 0,
|
|
125
|
+
balanceBeforeBaseUnits,
|
|
126
|
+
balanceAfterBaseUnits: state.balanceBaseUnits,
|
|
127
|
+
balanceDeltaBaseUnits,
|
|
128
|
+
toleranceBaseUnits,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function closeVisiblePosition(input) {
|
|
133
|
+
return withExtensionPage(input, async (page) => {
|
|
134
|
+
const market = marketSymbol(input.node?.market);
|
|
135
|
+
const partialAmount = input.node?.amount == null ? null : Number(input.node.amount);
|
|
136
|
+
if (partialAmount != null && (!Number.isFinite(partialAmount) || partialAmount <= 0)) {
|
|
137
|
+
throw new Error('metamask.perps.close_visible_position amount must be positive.');
|
|
138
|
+
}
|
|
139
|
+
if (input.node?.confirm_live !== true) {
|
|
140
|
+
throw new Error('metamask.perps.close_visible_position requires confirm_live=true.');
|
|
141
|
+
}
|
|
142
|
+
const fundingSide = String(input.node?.funding_side ?? '').toLowerCase();
|
|
143
|
+
const fundingAmount = String(input.node?.funding_amount ?? '');
|
|
144
|
+
const fundingPositionSize = String(input.node?.funding_position_size ?? '');
|
|
145
|
+
const leverage = Number(input.node?.leverage);
|
|
146
|
+
const cleanupOnly = input.node?.cleanup_only === true;
|
|
147
|
+
if (cleanupOnly && !trustedExternalMutationPlanIsExact(input, 'perps-close-position', 1)) {
|
|
148
|
+
throw new Error('Cleanup-only close requires an exact runner-verified close-only request and plan.');
|
|
149
|
+
}
|
|
150
|
+
if (cleanupOnly && (!/^[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u.test(fundingPositionSize)
|
|
151
|
+
|| Number.parseFloat(fundingPositionSize) <= 0 || partialAmount != null)) {
|
|
152
|
+
throw new Error('Cleanup-only close requires one exact positive full-position size identity.');
|
|
153
|
+
}
|
|
154
|
+
const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
|
|
155
|
+
const balanceToleranceBaseUnits = String(input.node?.balance_reconciliation_tolerance_base_units ?? '1000000');
|
|
156
|
+
if (!Number.isInteger(leverage) || leverage < 1 || leverage > 100
|
|
157
|
+
|| !/^(?:0|[1-9][0-9]{0,6})$/u.test(roundingToleranceBaseUnits)
|
|
158
|
+
|| !/^(?:0|[1-9][0-9]{0,6})$/u.test(balanceToleranceBaseUnits)
|
|
159
|
+
|| BigInt(roundingToleranceBaseUnits) > 1_000_000n
|
|
160
|
+
|| BigInt(balanceToleranceBaseUnits) > 1_000_000n) {
|
|
161
|
+
throw new Error('metamask.perps.close_visible_position leverage/tolerance inputs are invalid.');
|
|
162
|
+
}
|
|
163
|
+
const identity = trustedExternalMutationIdentity(input, page, {
|
|
164
|
+
operation: 'perps-close-position',
|
|
165
|
+
provider: input.node?.provider,
|
|
166
|
+
network: input.node?.network,
|
|
167
|
+
market,
|
|
168
|
+
side: fundingSide,
|
|
169
|
+
amount: fundingAmount,
|
|
170
|
+
leverage,
|
|
171
|
+
orderType: 'close',
|
|
172
|
+
positionMode: 'close',
|
|
173
|
+
limitPrice: null,
|
|
174
|
+
offsetPct: null,
|
|
175
|
+
partialCloseAmount: partialAmount == null ? null : String(input.node.amount),
|
|
176
|
+
roundingToleranceBaseUnits,
|
|
177
|
+
...(cleanupOnly ? { positionSize: fundingPositionSize, cleanupOnly: true } : {}),
|
|
178
|
+
});
|
|
179
|
+
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
180
|
+
if (existingReceipt) {
|
|
181
|
+
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
182
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
183
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
184
|
+
const mutation = await runGuardedExternalMutation({
|
|
185
|
+
input,
|
|
186
|
+
identity,
|
|
187
|
+
dispatch: async () => {
|
|
188
|
+
throw new Error('resumed close must not dispatch');
|
|
189
|
+
},
|
|
190
|
+
observe: async () => {
|
|
191
|
+
const deadline = Date.now() + 45_000;
|
|
192
|
+
let finalState;
|
|
193
|
+
let absentSince = null;
|
|
194
|
+
while (Date.now() <= deadline) {
|
|
195
|
+
finalState = await page.evaluate(`(() => ({
|
|
196
|
+
route: String(location.hash),
|
|
197
|
+
positionVisible: Boolean(document.querySelector('[data-testid="perps-position-size-value"]')),
|
|
198
|
+
positionSize: String(document.querySelector('[data-testid="perps-position-size-value"]')?.innerText || '').trim(),
|
|
199
|
+
modalVisible: Boolean(document.querySelector('[data-testid="perps-close-position-modal"]')),
|
|
200
|
+
loading: Boolean(document.querySelector('[data-testid="perps-detail-page-skeleton"], [data-testid="perps-section-skeleton"]')),
|
|
201
|
+
}))()`);
|
|
202
|
+
if (partialAmount != null && finalState.positionVisible && !finalState.modalVisible
|
|
203
|
+
&& finalState.positionSize !== existingReceipt.record?.facts?.positionBeforeSize) {
|
|
204
|
+
absentSince ??= Date.now();
|
|
205
|
+
if (Date.now() - absentSince >= 1_000) return { success: true, finalState, cleanup: null };
|
|
206
|
+
} else if (!finalState.positionVisible && !finalState.modalVisible && !finalState.loading) {
|
|
207
|
+
absentSince ??= Date.now();
|
|
208
|
+
if (Date.now() - absentSince >= 1_000) {
|
|
209
|
+
if (partialAmount != null) return { success: true, finalState, cleanup: null };
|
|
210
|
+
const balanceBeforeBaseUnits = String(existingReceipt.record?.facts?.balanceBeforeBaseUnits ?? '');
|
|
211
|
+
if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceBeforeBaseUnits)) {
|
|
212
|
+
throw new Error('Resumed close receipt has no valid pre-order balance.');
|
|
213
|
+
}
|
|
214
|
+
const cleanup = await proveGlobalCleanup(
|
|
215
|
+
page,
|
|
216
|
+
balanceBeforeBaseUnits,
|
|
217
|
+
balanceToleranceBaseUnits,
|
|
218
|
+
);
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
finalState,
|
|
222
|
+
cleanup,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
absentSince = null;
|
|
227
|
+
}
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
229
|
+
}
|
|
230
|
+
throw new Error('matching closed-position postcondition was not visible before timeout');
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
action: input.action,
|
|
235
|
+
market,
|
|
236
|
+
closed: true,
|
|
237
|
+
partial: false,
|
|
238
|
+
receipt: mutation.receipt,
|
|
239
|
+
resumed: true,
|
|
240
|
+
recovery: mutation.recovery,
|
|
241
|
+
finalState: mutation.observation.finalState,
|
|
242
|
+
cleanup: mutation.observation.cleanup ?? null,
|
|
243
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
let cleanupHomeBefore = null;
|
|
248
|
+
if (cleanupOnly) {
|
|
249
|
+
await page.navigateHash('#/perps-home');
|
|
250
|
+
cleanupHomeBefore = await waitForStableGlobalPerpsHome(page, { timeoutMs: 20_000, requireZero: false });
|
|
251
|
+
if (cleanupHomeBefore.positionCount !== 1 || cleanupHomeBefore.orderCount !== 0) {
|
|
252
|
+
throw new Error('Cleanup-only close requires exactly one primary position and globally zero orders.');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
|
|
256
|
+
const fundingBalance = await requireCurrentVisibleFundingBalance(input, page, timeoutMs);
|
|
257
|
+
const tradingContext = await requirePerpsTradingContext(input, page);
|
|
258
|
+
const before = await readVisibleClosePosition(page, market);
|
|
259
|
+
const exactBefore = parsePositionDetail({ pair: `${market}-USD`, ...before });
|
|
260
|
+
if (!positionIdentityMatches(exactBefore, {
|
|
261
|
+
market,
|
|
262
|
+
side: fundingSide,
|
|
263
|
+
leverage,
|
|
264
|
+
amountBaseUnits: identity.amountBaseUnits,
|
|
265
|
+
roundingToleranceBaseUnits,
|
|
266
|
+
matchMargin: false,
|
|
267
|
+
})) {
|
|
268
|
+
throw new Error('Visible Perps position does not match the funded market/side/leverage identity.');
|
|
269
|
+
}
|
|
270
|
+
if (cleanupOnly && exactBefore.size !== fundingPositionSize) {
|
|
271
|
+
throw new Error('Cleanup-only close visible position size does not match the authorized exact identity.');
|
|
272
|
+
}
|
|
273
|
+
const sizeMatch = /^([0-9]+(?:\.[0-9]+)?) ([A-Z0-9:_-]{2,20})$/u.exec(String(exactBefore.size ?? ''));
|
|
274
|
+
const priceBaseUnits = parseUsdBaseUnits(before.currentPrice);
|
|
275
|
+
const observedNotionalBaseUnits = sizeMatch && sizeMatch[2] === market && priceBaseUnits != null
|
|
276
|
+
? `${Math.round(Number(sizeMatch[1]) * Number(priceBaseUnits))}`
|
|
277
|
+
: null;
|
|
278
|
+
const closeBudget = trustedExternalMutationBudget(input, 'perps-close-position');
|
|
279
|
+
if (!/^[1-9][0-9]{0,29}$/u.test(String(observedNotionalBaseUnits ?? ''))
|
|
280
|
+
|| BigInt(observedNotionalBaseUnits) > BigInt(closeBudget.maxBaseUnits)) {
|
|
281
|
+
throw new Error('Visible Perps position notional is unavailable or exceeds the authorized close cap.');
|
|
282
|
+
}
|
|
283
|
+
if (!cleanupOnly) {
|
|
284
|
+
await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order');
|
|
285
|
+
}
|
|
286
|
+
const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
|
|
287
|
+
if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceBeforeBaseUnits)) {
|
|
288
|
+
throw new Error('Prior order receipt has no valid visible pre-order balance.');
|
|
289
|
+
}
|
|
290
|
+
const confirmation = await prepareCloseModal(page, partialAmount);
|
|
291
|
+
const mutation = await runGuardedExternalMutation({
|
|
292
|
+
input,
|
|
293
|
+
identity,
|
|
294
|
+
publicationFacts: {
|
|
295
|
+
balanceBeforeBaseUnits,
|
|
296
|
+
positionBeforeSize: before.size,
|
|
297
|
+
observedNotionalBaseUnits,
|
|
298
|
+
cleanupOnly,
|
|
299
|
+
fundingBalance,
|
|
300
|
+
tradingContext,
|
|
301
|
+
},
|
|
302
|
+
dispatch: async () => {
|
|
303
|
+
let dispatchTarget;
|
|
304
|
+
try {
|
|
305
|
+
await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
|
|
306
|
+
await requirePerpsTradingContext(input, page);
|
|
307
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
308
|
+
const current = await readVisibleClosePosition(page, market);
|
|
309
|
+
const currentPosition = parsePositionDetail({ pair: `${market}-USD`, ...current });
|
|
310
|
+
if (!positionIdentityMatches(currentPosition, {
|
|
311
|
+
market, side: fundingSide, leverage, amountBaseUnits: identity.amountBaseUnits,
|
|
312
|
+
roundingToleranceBaseUnits, matchMargin: false,
|
|
313
|
+
}) || currentPosition.size !== exactBefore.size) {
|
|
314
|
+
throw new Error('Visible Perps position changed before close dispatch.');
|
|
315
|
+
}
|
|
316
|
+
await prepareCloseModal(page, partialAmount);
|
|
317
|
+
dispatchTarget = await page.evaluate(`(() => {
|
|
318
|
+
const element = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
319
|
+
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
320
|
+
throw new Error('Visible Perps close submit control is unavailable.');
|
|
321
|
+
}
|
|
322
|
+
const rect = element.getBoundingClientRect();
|
|
323
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
324
|
+
})()`);
|
|
325
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
328
|
+
}
|
|
329
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
330
|
+
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
331
|
+
button: 'left', buttons: 1, clickCount: 1,
|
|
332
|
+
});
|
|
333
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
334
|
+
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
335
|
+
button: 'left', buttons: 0, clickCount: 1,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
observe: async () => {
|
|
339
|
+
const closeDeadline = Date.now() + 45_000;
|
|
340
|
+
let finalState;
|
|
341
|
+
while (Date.now() <= closeDeadline) {
|
|
342
|
+
finalState = await page.evaluate(`(() => ({
|
|
343
|
+
route: String(location.hash),
|
|
344
|
+
positionVisible: Boolean(document.querySelector('[data-testid="perps-position-size-value"]')),
|
|
345
|
+
positionSize: String(document.querySelector('[data-testid="perps-position-size-value"]')?.innerText || '').trim(),
|
|
346
|
+
modalVisible: Boolean(document.querySelector('[data-testid="perps-close-position-modal"]')),
|
|
347
|
+
error: String(document.querySelector('[data-testid="perps-order-submit-error"]')?.innerText || '').trim(),
|
|
348
|
+
}))()`);
|
|
349
|
+
if (finalState.error) throw new Error('visible Perps close error');
|
|
350
|
+
if (partialAmount != null) {
|
|
351
|
+
if (!finalState.modalVisible && finalState.positionVisible && finalState.positionSize !== before.size) {
|
|
352
|
+
return { success: true, finalState };
|
|
353
|
+
}
|
|
354
|
+
} else if (!finalState.positionVisible && !finalState.modalVisible) {
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
356
|
+
const exactAbsent = !await page.evaluate(
|
|
357
|
+
`Boolean(document.querySelector('[data-testid="perps-position-size-value"]'))`,
|
|
358
|
+
);
|
|
359
|
+
if (exactAbsent) {
|
|
360
|
+
const cleanup = await proveGlobalCleanup(
|
|
361
|
+
page,
|
|
362
|
+
balanceBeforeBaseUnits,
|
|
363
|
+
balanceToleranceBaseUnits,
|
|
364
|
+
);
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
finalState,
|
|
368
|
+
cleanup,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
373
|
+
}
|
|
374
|
+
throw new Error('matching closed-position postcondition was not visible before timeout');
|
|
375
|
+
},
|
|
376
|
+
});
|
|
123
377
|
return {
|
|
124
378
|
action: input.action,
|
|
125
379
|
market,
|
|
126
380
|
closed: partialAmount == null,
|
|
127
381
|
partial: partialAmount != null,
|
|
128
382
|
positionBefore: before,
|
|
383
|
+
observedNotionalBaseUnits,
|
|
129
384
|
confirmation,
|
|
130
|
-
finalState,
|
|
385
|
+
finalState: mutation.observation.finalState,
|
|
386
|
+
cleanup: mutation.observation.cleanup ?? null,
|
|
387
|
+
receipt: mutation.receipt,
|
|
388
|
+
resumed: mutation.resumed,
|
|
389
|
+
recovery: mutation.recovery,
|
|
131
390
|
proofPath: 'trusted-pointer-visible-ui',
|
|
132
391
|
};
|
|
133
|
-
})
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
runAdapter(closeVisiblePosition);
|
|
@@ -18,6 +18,42 @@ function relativeDifference(actual, expected) {
|
|
|
18
18
|
return Math.abs(actual - expected) / Math.max(Math.abs(expected), 1e-12);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function countdownSeconds(value) {
|
|
22
|
+
const match = String(value ?? '').match(/^(\d{2}):(\d{2}):(\d{2})$/u);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readChartSample(page) {
|
|
28
|
+
return page.evaluate(`(() => {
|
|
29
|
+
const root = document.querySelector('[data-testid="perps-market-detail-chart"]');
|
|
30
|
+
const chart = root?.querySelector('[data-testid="perps-candlestick-chart"]');
|
|
31
|
+
const text = String(root?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
32
|
+
const canvases = chart ? [...chart.querySelectorAll('canvas')] : [];
|
|
33
|
+
const samples = canvases.map((canvas) => {
|
|
34
|
+
const context = canvas.getContext('2d');
|
|
35
|
+
if (!context || canvas.width <= 0 || canvas.height <= 0) return { width: canvas.width, height: canvas.height, colors: 0 };
|
|
36
|
+
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
37
|
+
const colors = new Set();
|
|
38
|
+
const stride = Math.max(4, Math.floor(pixels.length / 4000 / 4) * 4);
|
|
39
|
+
for (let offset = 0; offset < pixels.length; offset += stride) {
|
|
40
|
+
colors.add(pixels[offset] + ',' + pixels[offset + 1] + ',' + pixels[offset + 2] + ',' + pixels[offset + 3]);
|
|
41
|
+
if (colors.size >= 16) break;
|
|
42
|
+
}
|
|
43
|
+
return { width: canvas.width, height: canvas.height, colors: colors.size };
|
|
44
|
+
});
|
|
45
|
+
const rect = chart?.getBoundingClientRect();
|
|
46
|
+
return {
|
|
47
|
+
loading: Boolean(root && !chart && !/failed to load chart data/iu.test(text)),
|
|
48
|
+
error: /failed to load chart data/iu.test(text),
|
|
49
|
+
visible: Boolean(rect && rect.width > 0 && rect.height > 0),
|
|
50
|
+
canvasCount: canvases.length,
|
|
51
|
+
nonblankCanvasCount: samples.filter((sample) => sample.colors >= 4).length,
|
|
52
|
+
samples,
|
|
53
|
+
};
|
|
54
|
+
})()`);
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
runAdapter(async (input) => {
|
|
22
58
|
const market = marketSymbol(input);
|
|
23
59
|
if (!market) throw new Error('metamask.perps.compare_provider_market requires market=<symbol>.');
|
|
@@ -45,6 +81,23 @@ runAdapter(async (input) => {
|
|
|
45
81
|
return withExtensionPage(input, async (page) => {
|
|
46
82
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
47
83
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
84
|
+
const chartSamples = [];
|
|
85
|
+
const chartDeadline = Date.now() + 15_000;
|
|
86
|
+
let loadingObserved = false;
|
|
87
|
+
while (true) {
|
|
88
|
+
const sample = await readChartSample(page);
|
|
89
|
+
loadingObserved ||= sample.loading;
|
|
90
|
+
if (sample.visible && sample.canvasCount > 0 && sample.nonblankCanvasCount > 0) {
|
|
91
|
+
chartSamples.push(sample);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (sample.error || Date.now() >= chartDeadline) {
|
|
95
|
+
throw new Error('Visible market chart did not resolve to a nonblank canvas.');
|
|
96
|
+
}
|
|
97
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 200))');
|
|
98
|
+
}
|
|
99
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 1100))');
|
|
100
|
+
chartSamples.push(await readChartSample(page));
|
|
48
101
|
const visible = await page.evaluate(`(() => {
|
|
49
102
|
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
50
103
|
const text = read('perps-market-detail-page');
|
|
@@ -55,17 +108,26 @@ runAdapter(async (input) => {
|
|
|
55
108
|
pair: read('perps-market-detail-pair'),
|
|
56
109
|
maxLeverage: read('perps-market-max-leverage'),
|
|
57
110
|
price: read('perps-market-detail-price'),
|
|
111
|
+
change24h: read('perps-market-detail-change'),
|
|
58
112
|
volume: after('24h Volume'),
|
|
59
113
|
openInterest: after('Open interest'),
|
|
60
114
|
fundingRate: after('Funding rate'),
|
|
115
|
+
fundingCountdown: text.match(/Funding rate\\s+[^\\s]+\\s+\\((\\d{2}:\\d{2}:\\d{2})\\)/u)?.[1] ?? null,
|
|
61
116
|
oraclePrice: read('perps-market-detail-oracle-price'),
|
|
62
117
|
favoriteLabel: String(document.querySelector('[data-testid="perps-market-detail-favorite-button"]')?.getAttribute('aria-label') || '').trim(),
|
|
63
118
|
};
|
|
64
119
|
})()`);
|
|
120
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 1100))');
|
|
121
|
+
const nextFundingCountdown = await page.evaluate(`(() => {
|
|
122
|
+
const text = String(document.querySelector('[data-testid="perps-market-detail-page"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
123
|
+
return text.match(/Funding rate\\s+[^\\s]+\\s+\\((\\d{2}:\\d{2}:\\d{2})\\)/u)?.[1] ?? null;
|
|
124
|
+
})()`);
|
|
65
125
|
const provider = {
|
|
66
126
|
symbol: metadata.name,
|
|
67
127
|
maxLeverage: Number(metadata.maxLeverage),
|
|
68
128
|
markPrice: Number(context.markPx),
|
|
129
|
+
change24hPercent:
|
|
130
|
+
((Number(context.markPx) - Number(context.prevDayPx)) / Number(context.prevDayPx)) * 100,
|
|
69
131
|
oraclePrice: Number(context.oraclePx),
|
|
70
132
|
volume24hUsd: Number(context.dayNtlVlm),
|
|
71
133
|
openInterestUsd: Number(context.openInterest) * Number(context.markPx),
|
|
@@ -73,6 +135,7 @@ runAdapter(async (input) => {
|
|
|
73
135
|
};
|
|
74
136
|
const observed = {
|
|
75
137
|
price: numeric(visible.price),
|
|
138
|
+
change24hPercent: numeric(visible.change24h),
|
|
76
139
|
oraclePrice: numeric(visible.oraclePrice),
|
|
77
140
|
volume24hUsd: compactUsd(visible.volume),
|
|
78
141
|
openInterestUsd: compactUsd(visible.openInterest),
|
|
@@ -85,11 +148,23 @@ runAdapter(async (input) => {
|
|
|
85
148
|
name: visible.name.length > 0,
|
|
86
149
|
maxLeverage: observed.maxLeverage === provider.maxLeverage,
|
|
87
150
|
price: observed.price != null && relativeDifference(observed.price, provider.markPrice) <= 0.02,
|
|
151
|
+
change24h:
|
|
152
|
+
observed.change24hPercent != null &&
|
|
153
|
+
Number.isFinite(provider.change24hPercent) &&
|
|
154
|
+
Math.abs(observed.change24hPercent - provider.change24hPercent) <= 0.15,
|
|
88
155
|
oraclePrice: observed.oraclePrice != null && relativeDifference(observed.oraclePrice, provider.oraclePrice) <= 0.02,
|
|
89
156
|
volume24hUsd: observed.volume24hUsd != null && relativeDifference(observed.volume24hUsd, provider.volume24hUsd) <= 0.05,
|
|
90
157
|
openInterestUsd: observed.openInterestUsd != null && relativeDifference(observed.openInterestUsd, provider.openInterestUsd) <= 0.05,
|
|
91
158
|
fundingRate: observed.fundingRate != null && Math.abs(observed.fundingRate - provider.fundingRate) <= 0.00001,
|
|
159
|
+
fundingCountdown:
|
|
160
|
+
countdownSeconds(visible.fundingCountdown) != null &&
|
|
161
|
+
countdownSeconds(nextFundingCountdown) != null &&
|
|
162
|
+
((countdownSeconds(visible.fundingCountdown) - countdownSeconds(nextFundingCountdown) + 3600) % 3600) >= 1 &&
|
|
163
|
+
((countdownSeconds(visible.fundingCountdown) - countdownSeconds(nextFundingCountdown) + 3600) % 3600) <= 3,
|
|
92
164
|
favoriteState: /favorites?/iu.test(visible.favoriteLabel),
|
|
165
|
+
chartNonblank:
|
|
166
|
+
chartSamples.length === 2 &&
|
|
167
|
+
chartSamples.every((sample) => sample.error === false && sample.visible && sample.nonblankCanvasCount > 0),
|
|
93
168
|
};
|
|
94
169
|
const failed = Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name);
|
|
95
170
|
if (failed.length > 0) {
|
|
@@ -102,6 +177,8 @@ runAdapter(async (input) => {
|
|
|
102
177
|
visible,
|
|
103
178
|
observed,
|
|
104
179
|
provider,
|
|
180
|
+
chart: { loadingObserved, samples: chartSamples },
|
|
181
|
+
nextFundingCountdown,
|
|
105
182
|
checks,
|
|
106
183
|
proofPath: 'visible-ui-plus-live-provider',
|
|
107
184
|
};
|