@deeeed/metamask-harness 0.39.0 → 0.40.1
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 +52 -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 +8 -7
- 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 +1393 -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/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 +785 -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/select_activity_filter.mjs +81 -18
- package/library/actions/extension/perps/select_market_filter.mjs +48 -4
- 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/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/ui/native-navigation.mjs +45 -0
- package/library/actions/mobile/wallet/import.mjs +17 -10
- package/library/actions/mobile/wallet/native-ui.mjs +21 -1
- package/library/actions/shared/perps/visible-state.mjs +25 -5
- package/library/manifests/extension.action-manifest.json +1696 -1271
- package/library/recipes/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/perps/release-live-market-order.recipe.json +30 -13
- package/library/recipes/perps/release-market-details.recipe.json +1 -1
- 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,49 +1,131 @@
|
|
|
1
|
-
import { dataTestId,
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
assertTrustedExternalMutationTarget,
|
|
4
|
+
externalMutationNotDispatched,
|
|
5
|
+
readExternalMutationReceipt,
|
|
6
|
+
runGuardedExternalMutation,
|
|
7
|
+
trustedExternalMutationIdentity,
|
|
8
|
+
withExternalMutationRecovery,
|
|
9
|
+
} from './mutation-receipt.mjs';
|
|
10
|
+
import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
|
|
11
|
+
import {
|
|
12
|
+
normalizePositionSizeIdentity,
|
|
13
|
+
parsePositionDetail,
|
|
14
|
+
parseUsdBaseUnits,
|
|
15
|
+
positionIdentityMatches,
|
|
16
|
+
} from './visible-mutation-identity.mjs';
|
|
2
17
|
|
|
3
|
-
function
|
|
4
|
-
const
|
|
5
|
-
|
|
18
|
+
function marketSymbol(value) {
|
|
19
|
+
const market = String(value ?? '').trim().toUpperCase();
|
|
20
|
+
if (!/^[A-Z0-9:_-]{2,20}$/u.test(market)) {
|
|
21
|
+
throw new Error('metamask.perps.update_position_tpsl requires market=<symbol>.');
|
|
22
|
+
}
|
|
23
|
+
return market;
|
|
6
24
|
}
|
|
7
25
|
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
function price(value, label) {
|
|
27
|
+
const text = String(value ?? '');
|
|
28
|
+
if (!/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(text)) {
|
|
29
|
+
throw new Error(`metamask.perps.update_position_tpsl requires a positive ${label}.`);
|
|
30
|
+
}
|
|
31
|
+
return text;
|
|
13
32
|
}
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
async function readPosition(page, market) {
|
|
35
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
36
|
+
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
37
|
+
const raw = await page.evaluate(`(() => {
|
|
38
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
39
|
+
.replace(/\\s+/gu, ' ')
|
|
40
|
+
.trim()
|
|
41
|
+
.slice(0, 500);
|
|
42
|
+
return {
|
|
43
|
+
route: String(location.hash),
|
|
44
|
+
pair: read('perps-market-detail-pair'),
|
|
45
|
+
direction: read('perps-position-leverage'),
|
|
46
|
+
size: read('perps-position-size-value'),
|
|
47
|
+
margin: read('perps-position-margin-value'),
|
|
48
|
+
liquidation: read('perps-position-liquidation-value'),
|
|
49
|
+
takeProfit: read('perps-auto-close-tp-value'),
|
|
50
|
+
stopLoss: read('perps-auto-close-sl-value'),
|
|
51
|
+
};
|
|
52
|
+
})()`);
|
|
53
|
+
const parsed = parsePositionDetail(raw);
|
|
54
|
+
if (!parsed || !raw.route.includes('/perps/market/') || !raw.pair.toUpperCase().startsWith(`${market}-`)) {
|
|
55
|
+
throw new Error('Visible Perps position identity is unavailable or does not match the requested market.');
|
|
22
56
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
57
|
+
return { ...raw, ...parsed };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exactPrices(state, takeProfit, stopLoss) {
|
|
61
|
+
return parseUsdBaseUnits(state.takeProfit) === parseUsdBaseUnits(takeProfit)
|
|
62
|
+
&& parseUsdBaseUnits(state.stopLoss) === parseUsdBaseUnits(stopLoss);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function waitForExactPrices(page, identity, facts, timeoutMs) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
let stable = null;
|
|
68
|
+
let stableSince = 0;
|
|
69
|
+
let after;
|
|
70
|
+
while (Date.now() <= deadline) {
|
|
71
|
+
after = await readPosition(page, identity.market);
|
|
72
|
+
const exact = exactPrices(after, facts.takeProfitPrice, facts.stopLossPrice)
|
|
73
|
+
&& after.size === facts.positionBefore.size
|
|
74
|
+
&& after.direction === facts.positionBefore.direction
|
|
75
|
+
&& positionIdentityMatches(after, {
|
|
76
|
+
market: identity.market,
|
|
77
|
+
side: identity.side,
|
|
78
|
+
leverage: identity.leverage,
|
|
79
|
+
matchMargin: false,
|
|
80
|
+
});
|
|
81
|
+
if (exact) {
|
|
82
|
+
const key = JSON.stringify(after);
|
|
83
|
+
if (key !== stable) {
|
|
84
|
+
stable = key;
|
|
85
|
+
stableSince = Date.now();
|
|
86
|
+
} else if (Date.now() - stableSince >= 1_000) return after;
|
|
87
|
+
} else {
|
|
88
|
+
stable = null;
|
|
26
89
|
}
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
27
91
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
92
|
+
throw new Error(`exact TP/SL postcondition was not visible: ${JSON.stringify(after)}.`);
|
|
93
|
+
}
|
|
31
94
|
|
|
32
|
-
|
|
33
|
-
await page
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
95
|
+
async function observePersistedPrices(page, identity, facts, timeoutMs) {
|
|
96
|
+
const immediate = await waitForExactPrices(page, identity, facts, timeoutMs);
|
|
97
|
+
await page.session.call('Page.reload', { ignoreCache: true });
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
99
|
+
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
100
|
+
const persisted = await waitForExactPrices(page, identity, facts, timeoutMs);
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
positionBefore: facts.positionBefore,
|
|
104
|
+
prefill: facts.prefill,
|
|
105
|
+
request: {
|
|
106
|
+
takeProfitPrice: facts.takeProfitPrice,
|
|
107
|
+
stopLossPrice: facts.stopLossPrice,
|
|
108
|
+
},
|
|
109
|
+
immediate,
|
|
110
|
+
persistedAfterReload: persisted,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function prepareTpslModal(page, before, takeProfit, stopLoss) {
|
|
38
115
|
await page.click(dataTestId('perps-auto-close-row'));
|
|
39
116
|
await page.waitForSelector(dataTestId('perps-update-tpsl-modal'), { timeoutMs: 10_000 });
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
117
|
+
const prefill = await page.evaluate(`(() => ({
|
|
118
|
+
takeProfit: document.querySelector('input[data-testid="perps-update-tpsl-tp-price-input"], [data-testid="perps-update-tpsl-tp-price-input"] input')?.value ?? null,
|
|
119
|
+
stopLoss: document.querySelector('input[data-testid="perps-update-tpsl-sl-price-input"], [data-testid="perps-update-tpsl-sl-price-input"] input')?.value ?? null,
|
|
120
|
+
}))()`);
|
|
121
|
+
const beforeTakeProfit = parseUsdBaseUnits(before.takeProfit);
|
|
122
|
+
const beforeStopLoss = parseUsdBaseUnits(before.stopLoss);
|
|
123
|
+
if ((beforeTakeProfit != null && parseUsdBaseUnits(prefill.takeProfit) !== beforeTakeProfit)
|
|
124
|
+
|| (beforeStopLoss != null && parseUsdBaseUnits(prefill.stopLoss) !== beforeStopLoss)) {
|
|
125
|
+
throw new Error('TP/SL modal did not prefill the existing visible values.');
|
|
45
126
|
}
|
|
46
|
-
|
|
127
|
+
await page.setInput('input[data-testid="perps-update-tpsl-tp-price-input"], [data-testid="perps-update-tpsl-tp-price-input"] input', takeProfit);
|
|
128
|
+
await page.setInput('input[data-testid="perps-update-tpsl-sl-price-input"], [data-testid="perps-update-tpsl-sl-price-input"] input', stopLoss);
|
|
47
129
|
const readyDeadline = Date.now() + 10_000;
|
|
48
130
|
let ready;
|
|
49
131
|
while (Date.now() <= readyDeadline) {
|
|
@@ -54,53 +136,153 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
54
136
|
takeProfit: value('perps-update-tpsl-tp-price-input'),
|
|
55
137
|
stopLoss: value('perps-update-tpsl-sl-price-input'),
|
|
56
138
|
submitEnabled: Boolean(submit && !submit.disabled && submit.getAttribute('aria-disabled') !== 'true'),
|
|
57
|
-
entry: String(document.querySelector('[data-testid="perps-update-tpsl-entry-price-value"]')?.textContent || '').trim(),
|
|
58
|
-
current: String(document.querySelector('[data-testid="perps-update-tpsl-current-price-value"]')?.textContent || '').trim(),
|
|
59
|
-
liquidation: String(document.querySelector('[data-testid="perps-update-tpsl-liquidation-price-value"]')?.textContent || '').trim(),
|
|
60
|
-
errors: [...document.querySelectorAll('[role="alert"]')]
|
|
61
|
-
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
.slice(0, 20),
|
|
64
139
|
};
|
|
65
140
|
})()`);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (retained && ready.submitEnabled) break;
|
|
141
|
+
if (Number(ready.takeProfit) === Number(takeProfit)
|
|
142
|
+
&& Number(ready.stopLoss) === Number(stopLoss) && ready.submitEnabled) break;
|
|
69
143
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
70
144
|
}
|
|
71
|
-
if (!ready?.submitEnabled || ready.
|
|
145
|
+
if (!ready?.submitEnabled || Number(ready.takeProfit) !== Number(takeProfit)
|
|
146
|
+
|| Number(ready.stopLoss) !== Number(stopLoss)) {
|
|
72
147
|
throw new Error(`Perps TP/SL update is not ready: ${JSON.stringify(ready)}.`);
|
|
73
148
|
}
|
|
74
|
-
|
|
149
|
+
return { prefill, ready };
|
|
150
|
+
}
|
|
75
151
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!after.modalVisible && reconciled(after)) break;
|
|
92
|
-
if (after.errors.length > 0) throw new Error(`Perps TP/SL update failed: ${after.errors.join(' | ')}`);
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
152
|
+
export async function updatePositionTpsl(input) {
|
|
153
|
+
return withExtensionPage(input, async (page) => {
|
|
154
|
+
const market = marketSymbol(input.node?.market);
|
|
155
|
+
const takeProfit = price(input.node?.take_profit_price, 'take_profit_price');
|
|
156
|
+
const stopLoss = price(input.node?.stop_loss_price, 'stop_loss_price');
|
|
157
|
+
const fundingSide = String(input.node?.funding_side ?? '').trim().toLowerCase();
|
|
158
|
+
const fundingAmount = String(input.node?.funding_amount ?? '');
|
|
159
|
+
const fundingAmountBaseUnits = parseUsdBaseUnits(fundingAmount);
|
|
160
|
+
const fundingPositionSize = normalizePositionSizeIdentity(input.node?.funding_position_size);
|
|
161
|
+
const leverage = Number(input.node?.leverage);
|
|
162
|
+
if (!/^(?:long|short)$/u.test(fundingSide)
|
|
163
|
+
|| !/^[1-9][0-9]{0,29}$/u.test(String(fundingAmountBaseUnits ?? ''))
|
|
164
|
+
|| fundingPositionSize == null
|
|
165
|
+
|| !Number.isInteger(leverage) || leverage < 1 || leverage > 100) {
|
|
166
|
+
throw new Error('metamask.perps.update_position_tpsl received invalid funded position inputs.');
|
|
94
167
|
}
|
|
95
|
-
if (
|
|
96
|
-
throw new Error(
|
|
168
|
+
if (input.node?.confirm_live !== true) {
|
|
169
|
+
throw new Error('metamask.perps.update_position_tpsl requires confirm_live=true for live submission.');
|
|
97
170
|
}
|
|
171
|
+
const identity = trustedExternalMutationIdentity(input, page, {
|
|
172
|
+
operation: 'perps-tpsl-update',
|
|
173
|
+
provider: input.node?.provider,
|
|
174
|
+
network: input.node?.network,
|
|
175
|
+
market,
|
|
176
|
+
side: fundingSide,
|
|
177
|
+
amount: fundingAmount,
|
|
178
|
+
leverage,
|
|
179
|
+
orderType: 'tpsl',
|
|
180
|
+
positionMode: 'modify',
|
|
181
|
+
takeProfitPrice: takeProfit,
|
|
182
|
+
stopLossPrice: stopLoss,
|
|
183
|
+
positionAmountBaseUnits: fundingAmountBaseUnits,
|
|
184
|
+
positionSize: fundingPositionSize,
|
|
185
|
+
roundingToleranceBaseUnits: '0',
|
|
186
|
+
});
|
|
187
|
+
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
188
|
+
if (existingReceipt) {
|
|
189
|
+
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
190
|
+
const mutation = await runGuardedExternalMutation({
|
|
191
|
+
input,
|
|
192
|
+
identity,
|
|
193
|
+
dispatch: async () => { throw new Error('resumed TP/SL mutation must not dispatch'); },
|
|
194
|
+
observe: async ({ receipt }) => observePersistedPrices(
|
|
195
|
+
page,
|
|
196
|
+
identity,
|
|
197
|
+
receipt.facts,
|
|
198
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
199
|
+
),
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
action: input.action,
|
|
203
|
+
market,
|
|
204
|
+
receipt: mutation.receipt,
|
|
205
|
+
resumed: true,
|
|
206
|
+
outcome: mutation.observation,
|
|
207
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
|
|
213
|
+
const fundingBalance = await requireCurrentVisibleFundingBalance(input, page, timeoutMs);
|
|
214
|
+
const tradingContext = await requirePerpsTradingContext(input, page);
|
|
215
|
+
const before = await readPosition(page, market);
|
|
216
|
+
if (!positionIdentityMatches(before, { market, side: fundingSide, leverage, matchMargin: false })
|
|
217
|
+
|| normalizePositionSizeIdentity(before.size) !== fundingPositionSize) {
|
|
218
|
+
throw new Error('Visible Perps position does not match the funded market/side/leverage/size identity.');
|
|
219
|
+
}
|
|
220
|
+
const { prefill, ready } = await prepareTpslModal(page, before, takeProfit, stopLoss);
|
|
221
|
+
const facts = {
|
|
222
|
+
positionBefore: before,
|
|
223
|
+
prefill,
|
|
224
|
+
takeProfitPrice: takeProfit,
|
|
225
|
+
stopLossPrice: stopLoss,
|
|
226
|
+
fundingBalance,
|
|
227
|
+
tradingContext,
|
|
228
|
+
};
|
|
229
|
+
const mutation = await runGuardedExternalMutation({
|
|
230
|
+
input,
|
|
231
|
+
identity,
|
|
232
|
+
publicationFacts: facts,
|
|
233
|
+
dispatch: async () => {
|
|
234
|
+
let dispatchTarget;
|
|
235
|
+
try {
|
|
236
|
+
await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
|
|
237
|
+
await requirePerpsTradingContext(input, page);
|
|
238
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
239
|
+
const current = await readPosition(page, market);
|
|
240
|
+
if (current.size !== before.size || current.direction !== before.direction
|
|
241
|
+
|| normalizePositionSizeIdentity(current.size) !== fundingPositionSize
|
|
242
|
+
|| !positionIdentityMatches(current, { market, side: fundingSide, leverage, matchMargin: false })) {
|
|
243
|
+
throw new Error('Visible Perps position changed before TP/SL dispatch.');
|
|
244
|
+
}
|
|
245
|
+
await prepareTpslModal(page, current, takeProfit, stopLoss);
|
|
246
|
+
dispatchTarget = await page.evaluate(`(() => {
|
|
247
|
+
const element = document.querySelector('[data-testid="perps-update-tpsl-modal-submit"]');
|
|
248
|
+
if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
|
|
249
|
+
throw new Error('Visible Perps TP/SL submit control is unavailable.');
|
|
250
|
+
}
|
|
251
|
+
const rect = element.getBoundingClientRect();
|
|
252
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
253
|
+
})()`);
|
|
254
|
+
assertTrustedExternalMutationTarget(input, page, identity);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
257
|
+
}
|
|
258
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
259
|
+
type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
260
|
+
button: 'left', buttons: 1, clickCount: 1,
|
|
261
|
+
});
|
|
262
|
+
await page.session.call('Input.dispatchMouseEvent', {
|
|
263
|
+
type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
|
|
264
|
+
button: 'left', buttons: 0, clickCount: 1,
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
observe: async ({ receipt }) => observePersistedPrices(
|
|
268
|
+
page,
|
|
269
|
+
identity,
|
|
270
|
+
receipt.facts,
|
|
271
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
272
|
+
),
|
|
273
|
+
});
|
|
98
274
|
return {
|
|
99
275
|
action: input.action,
|
|
100
276
|
market,
|
|
101
277
|
before,
|
|
278
|
+
prefill,
|
|
102
279
|
ready,
|
|
103
|
-
|
|
104
|
-
|
|
280
|
+
receipt: mutation.receipt,
|
|
281
|
+
resumed: mutation.resumed,
|
|
282
|
+
outcome: mutation.observation,
|
|
283
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
105
284
|
};
|
|
106
|
-
})
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
runAdapter(updatePositionTpsl);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const INTEGER = /^(0|[1-9][0-9]{0,29})$/u;
|
|
2
|
+
|
|
3
|
+
export function parseUsdBaseUnits(value) {
|
|
4
|
+
const normalized = String(value ?? '').replace(/[,$\s]/gu, '');
|
|
5
|
+
const match = /^([+-]?)(\d+)(?:\.(\d{1,6}))?$/u.exec(normalized);
|
|
6
|
+
if (!match) return null;
|
|
7
|
+
const magnitude = BigInt(`${match[2]}${String(match[3] ?? '').padEnd(6, '0')}`);
|
|
8
|
+
return `${match[1] === '-' ? -magnitude : magnitude}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parsePositiveBaseUnits(value, label = 'base units') {
|
|
12
|
+
const text = String(value ?? '');
|
|
13
|
+
if (!INTEGER.test(text) || text === '0') throw new Error(`${label} must be a positive integer string.`);
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function absoluteDelta(left, right) {
|
|
18
|
+
const delta = BigInt(left) - BigInt(right);
|
|
19
|
+
return `${delta < 0n ? -delta : delta}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function withinBaseUnitTolerance(actual, expected, tolerance) {
|
|
23
|
+
if (![actual, expected, tolerance].every((value) => /^-?\d+$/u.test(String(value)))) return false;
|
|
24
|
+
return BigInt(absoluteDelta(actual, expected)) <= BigInt(tolerance);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizedNumericInputMatches(observed, requested) {
|
|
28
|
+
const value = String(observed ?? '');
|
|
29
|
+
const expected = String(requested ?? '');
|
|
30
|
+
return /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/u.test(value)
|
|
31
|
+
&& /^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(expected)
|
|
32
|
+
&& Number(value) === Number(expected);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizePositionSizeIdentity(value) {
|
|
36
|
+
const match = /^([0-9][0-9,]*)(?:\.([0-9]+))?\s+([A-Z0-9:_-]{2,20})$/u.exec(
|
|
37
|
+
String(value ?? '').trim().toUpperCase(),
|
|
38
|
+
);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
const whole = match[1].replace(/,/gu, '').replace(/^0+(?=\d)/u, '');
|
|
41
|
+
const fraction = String(match[2] ?? '').replace(/0+$/u, '');
|
|
42
|
+
if (!/[1-9]/u.test(`${whole}${fraction}`)) return null;
|
|
43
|
+
return `${whole}${fraction ? `.${fraction}` : ''} ${match[3]}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function positionIdentityMatches(observed, expected) {
|
|
47
|
+
const identityMatches = Boolean(observed)
|
|
48
|
+
&& String(observed.market).toUpperCase() === String(expected.market).toUpperCase()
|
|
49
|
+
&& String(observed.side).toLowerCase() === String(expected.side).toLowerCase()
|
|
50
|
+
&& Number(observed.leverage) === Number(expected.leverage);
|
|
51
|
+
if (!identityMatches) return false;
|
|
52
|
+
if (expected.matchMargin === false) {
|
|
53
|
+
const size = /^([0-9][0-9,]*)(?:\.([0-9]+))?(?:\s|$)/u.exec(String(observed.size ?? '').trim());
|
|
54
|
+
return /^\d+$/u.test(String(observed.marginBaseUnits))
|
|
55
|
+
&& BigInt(observed.marginBaseUnits) > 0n
|
|
56
|
+
&& Boolean(size && /[1-9]/u.test(`${size[1]}${size[2] ?? ''}`));
|
|
57
|
+
}
|
|
58
|
+
return withinBaseUnitTolerance(
|
|
59
|
+
observed.marginBaseUnits,
|
|
60
|
+
expected.amountBaseUnits,
|
|
61
|
+
expected.roundingToleranceBaseUnits,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function orderIdentityMatches(observed, expected) {
|
|
66
|
+
return Boolean(observed)
|
|
67
|
+
&& String(observed.market).toUpperCase() === String(expected.market).toUpperCase()
|
|
68
|
+
&& String(observed.side).toLowerCase() === String(expected.side).toLowerCase()
|
|
69
|
+
&& String(observed.orderType).toLowerCase() === String(expected.orderType).toLowerCase()
|
|
70
|
+
&& withinBaseUnitTolerance(
|
|
71
|
+
observed.notionalBaseUnits,
|
|
72
|
+
expected.notionalBaseUnits,
|
|
73
|
+
expected.roundingToleranceBaseUnits,
|
|
74
|
+
)
|
|
75
|
+
&& withinBaseUnitTolerance(
|
|
76
|
+
observed.limitPriceBaseUnits,
|
|
77
|
+
expected.limitPriceBaseUnits,
|
|
78
|
+
expected.roundingToleranceBaseUnits,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parsePositionDetail(raw) {
|
|
83
|
+
const pair = String(raw?.pair ?? '').trim().toUpperCase();
|
|
84
|
+
const direction = /\b(long|short)\s+(\d+(?:\.\d+)?)x\b/iu.exec(String(raw?.direction ?? ''));
|
|
85
|
+
const marginBaseUnits = parseUsdBaseUnits(raw?.margin);
|
|
86
|
+
if (!pair || !direction || marginBaseUnits == null) return null;
|
|
87
|
+
return {
|
|
88
|
+
market: pair.split(/[-/\s]/u)[0],
|
|
89
|
+
side: direction[1].toLowerCase(),
|
|
90
|
+
leverage: Number(direction[2]),
|
|
91
|
+
marginBaseUnits,
|
|
92
|
+
size: String(raw?.size ?? '').trim().slice(0, 128),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseOrderCard(raw, market) {
|
|
97
|
+
const text = String(raw?.text ?? '').replace(/\s+/gu, ' ').trim();
|
|
98
|
+
const label = /\b(market|limit)\s+(long|short)\b/iu.exec(text);
|
|
99
|
+
const price = [...text.matchAll(/\$\s*([0-9][0-9,]*(?:\.[0-9]{1,6})?)/gu)].at(-1)?.[0];
|
|
100
|
+
const size = /([0-9]+(?:\.[0-9]+)?)\s+([A-Z0-9:_-]{2,20})\b/u.exec(text);
|
|
101
|
+
const limitPriceBaseUnits = parseUsdBaseUnits(price);
|
|
102
|
+
if (!label || !size || limitPriceBaseUnits == null) return null;
|
|
103
|
+
const notional = Number(size[1]) * Number(limitPriceBaseUnits) / 1_000_000;
|
|
104
|
+
if (!Number.isFinite(notional) || notional <= 0) return null;
|
|
105
|
+
return {
|
|
106
|
+
id: String(raw?.id ?? ''),
|
|
107
|
+
market: String(market ?? size[2]).toUpperCase(),
|
|
108
|
+
side: label[2].toLowerCase(),
|
|
109
|
+
orderType: label[1].toLowerCase(),
|
|
110
|
+
size: size[1],
|
|
111
|
+
limitPriceBaseUnits,
|
|
112
|
+
notionalBaseUnits: `${Math.round(notional * 1_000_000)}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function waitForStableGlobalPerpsHome(page, options = {}) {
|
|
117
|
+
const timeoutMs = Number(options.timeoutMs ?? 20_000);
|
|
118
|
+
const stableMs = Number(options.stableMs ?? 1_000);
|
|
119
|
+
const requireZero = options.requireZero !== false;
|
|
120
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
|
+
let stableSince = null;
|
|
122
|
+
let last = null;
|
|
123
|
+
while (Date.now() <= deadline) {
|
|
124
|
+
last = await page.evaluate(`(() => {
|
|
125
|
+
const visible = (element) => {
|
|
126
|
+
if (!element) return false;
|
|
127
|
+
const rect = element.getBoundingClientRect();
|
|
128
|
+
const style = getComputedStyle(element);
|
|
129
|
+
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
|
130
|
+
};
|
|
131
|
+
const visibleCount = (selector) => [...document.querySelectorAll(selector)].filter(visible).length;
|
|
132
|
+
const balance = document.querySelector('[data-testid="perps-balance-dropdown-balance"]');
|
|
133
|
+
return {
|
|
134
|
+
route: String(location.hash),
|
|
135
|
+
viewVisible: visible(document.querySelector('[data-testid="perps-view"]')),
|
|
136
|
+
loading: visibleCount('[data-testid$="-skeleton"], [data-testid*="loading"]') > 0,
|
|
137
|
+
marketCount: visibleCount('[data-testid="perps-explore-markets-row"]'),
|
|
138
|
+
positionCount: visibleCount('[data-testid^="position-card-"]:not([data-testid^="position-card-roe-"])'),
|
|
139
|
+
orderCount: visibleCount('[data-testid^="order-card-"]'),
|
|
140
|
+
balance: visible(balance) ? String(balance.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 200) : null,
|
|
141
|
+
};
|
|
142
|
+
})()`);
|
|
143
|
+
const loaded = last.route.includes('/perps-home') && last.viewVisible && !last.loading
|
|
144
|
+
&& last.marketCount > 0 && typeof last.balance === 'string' && last.balance.length > 0;
|
|
145
|
+
const clean = last.positionCount === 0 && last.orderCount === 0;
|
|
146
|
+
if (loaded && (!requireZero || clean)) {
|
|
147
|
+
stableSince ??= Date.now();
|
|
148
|
+
if (Date.now() - stableSince >= stableMs) {
|
|
149
|
+
const balanceBaseUnits = parseUsdBaseUnits(
|
|
150
|
+
String(last.balance).match(/\$\s*[0-9][0-9,]*(?:\.[0-9]{1,6})?/u)?.[0],
|
|
151
|
+
);
|
|
152
|
+
if (balanceBaseUnits == null) throw new Error('Visible Perps balance could not be parsed.');
|
|
153
|
+
return { ...last, balanceBaseUnits };
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
stableSince = null;
|
|
157
|
+
}
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
159
|
+
}
|
|
160
|
+
if (requireZero && last && (last.positionCount !== 0 || last.orderCount !== 0)) {
|
|
161
|
+
throw new Error('Loaded Perps Home must have globally zero positions and orders before external mutation or cleanup.');
|
|
162
|
+
}
|
|
163
|
+
throw new Error('Loaded skeleton-free Perps Home did not become stable before timeout.');
|
|
164
|
+
}
|