@deeeed/metamask-harness 0.79.0 → 0.81.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 +73 -0
- package/README.md +1 -1
- package/adapters/extension/ensure-browser.sh +2 -2
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/launch-browser.cjs +39 -613
- package/adapters/extension/readiness.mjs +2 -5
- package/adapters/extension/reattach.sh +2 -2
- package/adapters/extension/stamp-runtime-title.cjs +1 -1
- package/adapters/manifest.json +16 -64
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
- package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
- package/adapters/mobile/inject.sh +16 -0
- package/adapters/mobile/launch-console-forwarder.cjs +1 -2
- package/adapters/mobile/open-device.sh +2 -2
- package/adapters/mobile/reload-app.mjs +4 -4
- package/adapters/mobile/reset-app-data.sh +1 -1
- package/adapters/mobile/start-console-forwarder.sh +3 -2
- package/adapters/mobile/start-metro.sh +17 -12
- package/adapters/mobile/wait-for-bridge.cjs +2 -2
- package/adapters/shared/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- package/adapters/shared/sync-wallet-fixture.sh +8 -5
- package/adapters/terminal/cleanup.mjs +2 -1
- package/adapters/terminal/inject.mjs +2 -1
- package/adapters/terminal/launch.mjs +17 -13
- package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
- package/adapters/terminal/lib/readiness.mjs +20 -12
- package/adapters/terminal/policy.mjs +35 -0
- package/adapters/terminal/verify.mjs +10 -3
- package/adapters/terminal/wallet-host.mjs +43 -225
- package/bin/mm-harness +9 -28
- package/dist/adapters/checkout-files.js +16 -0
- package/dist/adapters/console-capture.js +14 -1
- package/dist/adapters/core/actions.js +8 -4
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +2 -2
- package/dist/adapters/core/surface.js +16 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/browser-pid.js +56 -0
- package/dist/adapters/extension/console-capture.js +1 -1
- package/dist/adapters/extension/doctor-checks.js +1 -1
- package/dist/adapters/extension/ensure-ready.js +5 -4
- package/dist/adapters/extension/extension-id.js +1 -1
- package/dist/adapters/extension/feature-flags.js +1 -1
- package/dist/adapters/extension/fixtures.js +1 -2
- package/dist/adapters/extension/launch.js +4 -5
- package/dist/adapters/extension/launched-browser.js +1 -2
- package/dist/adapters/extension/network-observer.js +14 -1
- package/dist/adapters/extension/overlay.js +1 -1
- package/dist/adapters/extension/product-config.js +1 -1
- package/dist/adapters/extension/readiness.js +1 -1
- package/dist/adapters/extension/reload.js +1 -1
- package/dist/adapters/extension/run.js +135 -14
- package/dist/adapters/extension/runtime-decision.js +2 -1
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +15 -2
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
- package/dist/adapters/mobile/app-identity.js +10 -0
- package/dist/adapters/mobile/doctor-checks.js +1 -1
- package/dist/adapters/mobile/fixtures.js +1 -1
- package/dist/adapters/mobile/launch.js +27 -28
- package/dist/adapters/mobile/lifecycle.js +1 -1
- package/dist/adapters/mobile/metro-env.js +9 -59
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +2 -4
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +3 -4
- package/dist/adapters/mobile/provision.js +1 -1
- package/dist/adapters/mobile/readiness.js +1 -1
- package/dist/adapters/mobile/recording-target.js +15 -0
- package/dist/adapters/mobile/release-artifact-state.js +1 -1
- package/dist/adapters/mobile/reload.js +1 -1
- package/dist/adapters/mobile/run.js +39 -15
- package/dist/adapters/mobile/runtime-decision.js +2 -4
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +26 -3
- package/dist/adapters/mobile/ui.js +16 -5
- package/dist/adapters/mobile/verify.js +2 -3
- package/dist/adapters/performance/cdp-trace.js +1 -1
- package/dist/adapters/performance/device-platform.js +1 -1
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +4 -1
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +33 -5
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +27 -7
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +59 -21
- package/dist/command-contract.js +1 -26
- package/dist/commands/check.js +33 -10
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +6 -14
- package/dist/commands/config.js +2 -2
- package/dist/commands/core-readiness.js +12 -7
- package/dist/commands/debug.js +3 -3
- package/dist/commands/device-target.js +5 -5
- package/dist/commands/doctor.js +2 -14
- package/dist/commands/domain.js +2 -2
- package/dist/commands/ensure-ready.js +1 -7
- package/dist/commands/fixtures.js +5 -6
- package/dist/commands/help.js +3 -4
- package/dist/commands/launch/device.js +10 -17
- package/dist/commands/launch/extension.js +2 -8
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/logs.js +3 -3
- package/dist/commands/mobile-device-view.js +4 -2
- package/dist/commands/overlay.js +110 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/pr-body.js +2 -2
- package/dist/commands/prepare.js +4 -9
- package/dist/commands/provision.js +3 -12
- package/dist/commands/recipe-advice.js +1 -2
- package/dist/commands/recipe-quality.js +2 -3
- package/dist/commands/resolve-extension.js +1 -7
- package/dist/commands/review.js +4 -4
- package/dist/commands/runtime-decision.js +1 -6
- package/dist/commands/runtime-health.js +1 -7
- package/dist/commands/runtime-launch.js +1 -13
- package/dist/commands/self-test.js +5 -8
- package/dist/commands/status-watch.js +1 -2
- package/dist/commands/status.js +2 -5
- package/dist/commands/task-init.js +2 -3
- package/dist/commands/tutorial.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/completions-cache.js +1 -1
- package/dist/console-classifier.js +19 -0
- package/dist/doctor.js +2 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +96 -1
- package/dist/host.js +32 -0
- package/dist/index.js +1 -6
- package/dist/live-tsx.js +49 -0
- package/dist/mm-harness-cli.js +4 -6
- package/dist/paths.js +34 -64
- package/dist/performance-observation.js +3 -5
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +6 -9
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +2 -2
- package/dist/runner.js +7 -4
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +2 -3
- package/docs/CONTRIBUTING.md +5 -3
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +2 -2
- package/docs/feature-guard.md +90 -0
- package/library/README.md +3 -1
- package/library/actions/core/perps/_controller.mjs +153 -134
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +61 -0
- package/library/actions/core/wallet/list_accounts.mjs +14 -57
- package/library/actions/extension/feature_flags/state.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +73 -11
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +8 -8
- package/library/actions/mobile/platform/native-session-name.mjs +21 -1
- package/library/actions/mobile/platform/native-session.mjs +3 -4
- package/library/actions/mobile/platform/observe-ui.mjs +12 -6
- package/library/actions/mobile/ui/native-navigation.mjs +4 -3
- package/library/actions/mobile/wallet/native-ui.mjs +3 -2
- package/library/actions/shared/analytics/_adapter.mjs +1 -1
- package/library/actions/shared/console-findings.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
- package/library/actions/terminal/app/launch.mjs +3 -2
- package/library/actions/terminal/platform/hud.mjs +2 -1
- package/library/actions/terminal/platform/page.mjs +2 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
- package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
- package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
- package/library/manifests/core.action-manifest.json +87 -13
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +8 -9
- package/site/assets/help-recipes.json +1 -0
- package/adapters/extension/lib/slot-title.cjs +0 -292
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
- package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
- package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
- package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
- package/adapters/mobile/coalesce-metro-log.cjs +0 -24
- package/adapters/mobile/launch-metro.cjs +0 -77
- package/adapters/mobile/metro-config.cjs +0 -98
- package/adapters/mobile/metro-log-generation.cjs +0 -106
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/adapters/mobile/frame-metrics.js +0 -118
- package/dist/adapters/mobile/video-recorder.js +0 -289
- package/dist/artifact-files.js +0 -92
- package/dist/checkout-lock.js +0 -149
- package/dist/cli-color.js +0 -98
- package/dist/command-journal.js +0 -308
- package/dist/commands/call.js +0 -685
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/list-executables.js +0 -247
- package/dist/commands/manifest.js +0 -488
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-engine.js +0 -1340
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/run.js +0 -751
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/discovery.js +0 -15
- package/dist/execution-provenance.js +0 -332
- package/dist/harness.js +0 -451
- package/dist/heal-bounds.js +0 -137
- package/dist/json-stream.js +0 -60
- package/dist/leaf-invoke.js +0 -19
- package/dist/library-provenance.js +0 -33
- package/dist/live-adapter-contract.js +0 -380
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/dist/recording-target.js +0 -120
- package/dist/run-diagnostics.js +0 -364
- package/dist/run-recording.js +0 -481
- package/dist/runtime-proof.js +0 -90
- package/library/actions/mobile/platform/tool-paths.mjs +0 -123
|
@@ -5,9 +5,9 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
|
|
8
|
-
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
|
|
9
|
-
|
|
10
8
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
9
|
+
import { readFixtureAccounts } from '../../shared/wallet/fixture-accounts.mjs';
|
|
10
|
+
import { createRewardsClient, rewardsApiUrl } from './_rewards.mjs';
|
|
11
11
|
|
|
12
12
|
function controllerEntry(projectRoot) {
|
|
13
13
|
return pathToFileURL(path.join(projectRoot, 'packages/perps-controller/src/index.ts')).href;
|
|
@@ -27,7 +27,7 @@ function messengerEntry(projectRoot) {
|
|
|
27
27
|
// a persistent auto-reconnecting HyperLiquid WebSocket (HyperLiquidClientService
|
|
28
28
|
// .initialize → wsTransport.ready()); these logs proved the order returns over
|
|
29
29
|
// HTTP while that open socket kept the event loop alive — see disconnectAndExit.
|
|
30
|
-
function buildInfrastructure(stubbed, input) {
|
|
30
|
+
function buildInfrastructure(stubbed, input, rewardsSource) {
|
|
31
31
|
const noop = () => undefined;
|
|
32
32
|
const terminalApi = resolveTerminalApi(input);
|
|
33
33
|
return {
|
|
@@ -69,153 +69,159 @@ function buildInfrastructure(stubbed, input) {
|
|
|
69
69
|
removeItem: async () => undefined,
|
|
70
70
|
},
|
|
71
71
|
...(terminalApi ? { terminalApi } : {}),
|
|
72
|
-
rewards: {
|
|
72
|
+
rewards: {
|
|
73
|
+
getPerpsDiscountForAccount: (caipAccountId, baseFeeBips) =>
|
|
74
|
+
rewardsSource.messenger.call(REWARDS_DISCOUNT_ACTION, caipAccountId, baseFeeBips),
|
|
75
|
+
},
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
// ---
|
|
79
|
+
// --- Rewards ---
|
|
77
80
|
//
|
|
78
|
-
// The
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// wallet
|
|
82
|
-
//
|
|
83
|
-
//
|
|
81
|
+
// The clients answer the rewards dependency through the messenger's
|
|
82
|
+
// RewardsController:getPerpsDiscountForAccount (extension perps-controller-init,
|
|
83
|
+
// mobile mobileInfrastructure). Core has no RewardsController, so the headless
|
|
84
|
+
// wallet registers one: the rewards client (_rewards.mjs) asks the rewards API
|
|
85
|
+
// for the selected fixture account, as Mobile's RewardsController does. A node's
|
|
86
|
+
// rewards_discount_bips replaces that answer, to prove a discount the fixture
|
|
87
|
+
// accounts (non-VIP) cannot get from the backend.
|
|
88
|
+
//
|
|
89
|
+
// The controller asks for the discount with the account's CAIP id, built from
|
|
90
|
+
// the wallet's selected network (NetworkController:getState and
|
|
91
|
+
// getNetworkClientById), so the wallet also answers those with the network a
|
|
92
|
+
// fresh client wallet starts on, Ethereum mainnet.
|
|
93
|
+
|
|
94
|
+
const REWARDS_DISCOUNT_ACTION = 'RewardsController:getPerpsDiscountForAccount';
|
|
95
|
+
|
|
84
96
|
|
|
85
97
|
/**
|
|
86
|
-
*
|
|
87
|
-
* Returns null if the fixture file does not exist.
|
|
98
|
+
* The rewards discount a node supplies, in basis points (0-10000), or undefined.
|
|
88
99
|
*
|
|
89
|
-
* @param
|
|
90
|
-
* @param accountName - The `name` field to match in fixture.accounts.
|
|
100
|
+
* @param input - Adapter input (node.rewards_discount_bips).
|
|
91
101
|
*/
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (error?.code === 'ENOENT') return null;
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
const fixture = JSON.parse(raw);
|
|
102
|
-
if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
|
|
103
|
-
throw new Error(`wallet-fixture.json at ${fixturePath} has no accounts array.`);
|
|
104
|
-
}
|
|
105
|
-
const entry = fixture.accounts.find((a) => a?.name === accountName);
|
|
106
|
-
if (!entry) {
|
|
107
|
-
const names = fixture.accounts.map((a) => a?.name).filter(Boolean).join(', ');
|
|
108
|
-
throw new Error(
|
|
109
|
-
`wallet-fixture.json has no account named "${accountName}". Available: ${names}.`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
if (typeof entry.value !== 'string' || entry.value.trim().length === 0) {
|
|
113
|
-
throw new Error(`wallet-fixture.json account "${accountName}" has no value.`);
|
|
114
|
-
}
|
|
115
|
-
if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`wallet-fixture.json account "${accountName}" type must be mnemonic or privateKey, got "${entry.type}".`,
|
|
118
|
-
);
|
|
102
|
+
export function requestedRewardsDiscountBips(input) {
|
|
103
|
+
const raw = optionalParam(input.node, 'rewards_discount_bips', 'rewardsDiscountBips');
|
|
104
|
+
if (raw === undefined) return undefined;
|
|
105
|
+
const bips = Number(raw);
|
|
106
|
+
if (!Number.isInteger(bips) || bips < 0 || bips > 10000) {
|
|
107
|
+
throw new Error(`rewards_discount_bips must be an integer from 0 through 10000; got ${raw}.`);
|
|
119
108
|
}
|
|
120
|
-
return
|
|
109
|
+
return bips;
|
|
121
110
|
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw new Error(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
return privateKeyToAccount(normalized);
|
|
112
|
+
const REWARDS_NETWORK_ACTIONS = ['NetworkController:getState', 'NetworkController:getNetworkClientById'];
|
|
113
|
+
|
|
114
|
+
function registerRewardsHandler(rootMessenger, childMessenger, discountBips, rewardsClient) {
|
|
115
|
+
rootMessenger.registerActionHandler(
|
|
116
|
+
REWARDS_DISCOUNT_ACTION,
|
|
117
|
+
discountBips === undefined
|
|
118
|
+
? (caipAccountId, baseFeeBips) => rewardsClient.getPerpsDiscountForAccount(caipAccountId, baseFeeBips)
|
|
119
|
+
: async () => discountBips,
|
|
120
|
+
);
|
|
121
|
+
rootMessenger.registerActionHandler('NetworkController:getState', () => ({
|
|
122
|
+
selectedNetworkClientId: 'mainnet',
|
|
123
|
+
}));
|
|
124
|
+
rootMessenger.registerActionHandler('NetworkController:getNetworkClientById', (networkClientId) => {
|
|
125
|
+
if (networkClientId !== 'mainnet') throw new Error(`No network client with id ${networkClientId}.`);
|
|
126
|
+
return { configuration: { chainId: '0x1' } };
|
|
127
|
+
});
|
|
128
|
+
rootMessenger.delegate({ actions: REWARDS_NETWORK_ACTIONS, messenger: childMessenger });
|
|
141
129
|
}
|
|
142
130
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
!/^0x[0-9a-fA-F]{40}$/u.test(nodeAccount.trim())
|
|
158
|
-
) {
|
|
159
|
-
return nodeAccount.trim();
|
|
160
|
-
}
|
|
161
|
-
return 'dev1';
|
|
131
|
+
// --- Account selection from wallet-fixture.json ---
|
|
132
|
+
//
|
|
133
|
+
// The core adapter reads the same wallet-fixture.json the mobile/extension
|
|
134
|
+
// adapters use (recipeRuntimePath/wallet-fixture.json), expanded the way
|
|
135
|
+
// list_accounts shows it (dev1..devN from a mnemonic with count N). A node
|
|
136
|
+
// selects an account by name (`account_name`, or a non-address `account`) or by
|
|
137
|
+
// 0x address (`account`, `address`, `userAddress`); dev1 when it names none.
|
|
138
|
+
// An unknown name is refused, and an address is never swapped for another
|
|
139
|
+
// account: one outside the fixture is read as given, and cannot sign.
|
|
140
|
+
|
|
141
|
+
const ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/u;
|
|
142
|
+
|
|
143
|
+
function trimmedString(value) {
|
|
144
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
162
145
|
}
|
|
163
146
|
|
|
164
147
|
/**
|
|
165
|
-
* Resolve the
|
|
166
|
-
* The wallet fixture account selected by name is the only signing source.
|
|
148
|
+
* Resolve the account a node selects.
|
|
167
149
|
*
|
|
168
|
-
* @param input - Adapter input (context.projectRoot, node
|
|
169
|
-
* @returns {
|
|
150
|
+
* @param input - Adapter input (context.projectRoot, node account fields).
|
|
151
|
+
* @returns { name, address, signer } — name and signer are null for an address outside the fixture.
|
|
170
152
|
*/
|
|
171
|
-
async function
|
|
153
|
+
export async function selectAccount(input) {
|
|
154
|
+
const node = input.node ?? {};
|
|
172
155
|
const projectRoot = input.context?.projectRoot;
|
|
173
|
-
const
|
|
156
|
+
const accountField = trimmedString(node.account);
|
|
157
|
+
const names = [trimmedString(node.account_name), accountField && !ADDRESS_PATTERN.test(accountField) ? accountField : undefined]
|
|
158
|
+
.filter((value) => value !== undefined);
|
|
159
|
+
const addresses = [node.account, node.address, node.userAddress]
|
|
160
|
+
.map(trimmedString)
|
|
161
|
+
.filter((value) => value !== undefined && ADDRESS_PATTERN.test(value));
|
|
162
|
+
if (new Set(names).size > 1) {
|
|
163
|
+
throw new Error(`core perps account selectors disagree: ${[...new Set(names)].join(' vs ')}; select one account.`);
|
|
164
|
+
}
|
|
165
|
+
if (new Set(addresses.map((value) => value.toLowerCase())).size > 1) {
|
|
166
|
+
throw new Error(`core perps account selectors disagree: ${addresses.join(' vs ')}; select one account.`);
|
|
167
|
+
}
|
|
168
|
+
const [name] = names;
|
|
169
|
+
const [address] = addresses;
|
|
170
|
+
const loaded = projectRoot ? await readFixtureAccounts(projectRoot) : null;
|
|
174
171
|
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
if (!loaded) {
|
|
173
|
+
if (!name && address) return { name: null, address, signer: null };
|
|
174
|
+
throw new Error(
|
|
175
|
+
`core perps account "${name ?? 'dev1'}" needs a wallet fixture at ` +
|
|
176
|
+
`${projectRoot ? walletFixturePath(projectRoot) : 'the checkout'}; ` +
|
|
177
|
+
'a read-only action can name a 0x address via account= instead.\n' +
|
|
178
|
+
'Next: run mm-harness fixtures set',
|
|
179
|
+
);
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
const available = loaded.accounts.map((account) => account.name).join(', ');
|
|
183
|
+
const byName = (wanted) => loaded.accounts.find((account) => account.name === wanted);
|
|
184
|
+
if (name) {
|
|
185
|
+
const matches = loaded.accounts.filter((account) => account.name === name);
|
|
186
|
+
if (matches.length > 1) {
|
|
187
|
+
throw new Error(`wallet fixture has ${matches.length} accounts named "${name}"; rename one in ${loaded.fixturePath}.`);
|
|
188
|
+
}
|
|
189
|
+
const [match] = matches;
|
|
190
|
+
if (!match) {
|
|
191
|
+
throw new Error(`wallet fixture has no account named "${name}". Available: ${available}.`);
|
|
192
|
+
}
|
|
193
|
+
if (address && match.address.toLowerCase() !== address.toLowerCase()) {
|
|
194
|
+
throw new Error(`fixture account "${name}" is ${match.address}, not ${address}; select one of them.`);
|
|
195
|
+
}
|
|
196
|
+
return match;
|
|
197
|
+
}
|
|
198
|
+
if (address) {
|
|
199
|
+
return loaded.accounts.find((account) => account.address.toLowerCase() === address.toLowerCase()) ??
|
|
200
|
+
{ name: null, address, signer: null };
|
|
201
|
+
}
|
|
202
|
+
const fallback = byName('dev1');
|
|
203
|
+
if (!fallback) {
|
|
204
|
+
throw new Error(`wallet fixture has no account named "dev1" (the default). Available: ${available}; pass account=<name>.`);
|
|
205
|
+
}
|
|
206
|
+
return fallback;
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
/**
|
|
190
|
-
* Resolve the
|
|
191
|
-
*
|
|
210
|
+
* Resolve the viem signer and EVM address for writes: a fixture account,
|
|
211
|
+
* selected by name or by its address.
|
|
212
|
+
*
|
|
213
|
+
* @param input - Adapter input (context.projectRoot, node account fields).
|
|
214
|
+
* @returns { account: ViemAccount, address: string }
|
|
192
215
|
*/
|
|
193
|
-
async function
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (projectRoot) {
|
|
198
|
-
// No try/catch: loadFixtureAccount returns null when no fixture is present
|
|
199
|
-
// (the only recoverable case) and THROWS on a malformed / missing-named /
|
|
200
|
-
// empty / bad-type fixture. Those must fail loudly — swallowing them would
|
|
201
|
-
// let a read silently run against a different address than the writes use.
|
|
202
|
-
const entry = await loadFixtureAccount(projectRoot, accountName);
|
|
203
|
-
if (entry) {
|
|
204
|
-
const account = viemAccountFromFixtureEntry(entry);
|
|
205
|
-
return account.address;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Read-only calls may name an address directly.
|
|
210
|
-
const fromNode = input.node?.account ?? input.node?.address ?? input.node?.userAddress;
|
|
211
|
-
const address = String(fromNode ?? '').trim();
|
|
212
|
-
if (!/^0x[0-9a-fA-F]{40}$/u.test(address)) {
|
|
216
|
+
async function resolveSignerFromFixture(input) {
|
|
217
|
+
const selected = await selectAccount(input);
|
|
218
|
+
if (!selected.signer) {
|
|
213
219
|
throw new Error(
|
|
214
|
-
`core perps
|
|
215
|
-
|
|
220
|
+
`core perps writes sign with a wallet-fixture.json account; ${selected.address} is not one of them.\n` +
|
|
221
|
+
'Next: pass account=<fixture account name>, or an address listed by metamask.wallet.list_accounts',
|
|
216
222
|
);
|
|
217
223
|
}
|
|
218
|
-
return address;
|
|
224
|
+
return { account: selected.signer, address: selected.address };
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
let cached = null;
|
|
@@ -271,17 +277,20 @@ export function requireRuntimeExport(moduleNamespace, exportName, sourceLabel) {
|
|
|
271
277
|
export async function getCoreController(input) {
|
|
272
278
|
const projectRoot = input.context?.projectRoot;
|
|
273
279
|
if (!projectRoot) throw new Error('core adapter requires context.projectRoot.');
|
|
274
|
-
const
|
|
280
|
+
const selected = await selectAccount(input);
|
|
281
|
+
const accountAddress = selected.address;
|
|
275
282
|
const network = resolveNetwork(input);
|
|
276
283
|
const terminalApi = resolveTerminalApi(input);
|
|
277
284
|
const terminalApiKey = JSON.stringify(terminalApi ?? null);
|
|
285
|
+
const rewardsDiscountBips = requestedRewardsDiscountBips(input);
|
|
278
286
|
|
|
279
287
|
if (
|
|
280
288
|
cached &&
|
|
281
289
|
cached.projectRoot === projectRoot &&
|
|
282
290
|
cached.network === network &&
|
|
283
291
|
cached.accountAddress.toLowerCase() === accountAddress.toLowerCase() &&
|
|
284
|
-
cached.terminalApiKey === terminalApiKey
|
|
292
|
+
cached.terminalApiKey === terminalApiKey &&
|
|
293
|
+
cached.rewardsDiscountBips === rewardsDiscountBips
|
|
285
294
|
) {
|
|
286
295
|
return { ...cached, accountAddress };
|
|
287
296
|
}
|
|
@@ -307,7 +316,8 @@ export async function getCoreController(input) {
|
|
|
307
316
|
);
|
|
308
317
|
|
|
309
318
|
const stubbed = new Set();
|
|
310
|
-
const
|
|
319
|
+
const rewardsSource = {};
|
|
320
|
+
const infrastructure = buildInfrastructure(stubbed, input, rewardsSource);
|
|
311
321
|
|
|
312
322
|
// Root + child messenger pair, mirroring the real app wiring (and the core
|
|
313
323
|
// repo's own test harness in
|
|
@@ -323,6 +333,11 @@ export async function getCoreController(input) {
|
|
|
323
333
|
parent: rootMessenger,
|
|
324
334
|
});
|
|
325
335
|
registerSelectedAccountHandler(rootMessenger, messenger, accountAddress);
|
|
336
|
+
// An address outside the fixture has no signer, so the client answers null:
|
|
337
|
+
// no subscription, as on Mobile for an account that never signed in.
|
|
338
|
+
const rewardsClient = createRewardsClient({ signer: selected.signer, baseUrl: rewardsApiUrl() });
|
|
339
|
+
registerRewardsHandler(rootMessenger, messenger, rewardsDiscountBips, rewardsClient);
|
|
340
|
+
rewardsSource.messenger = rootMessenger;
|
|
326
341
|
|
|
327
342
|
// Default testnet. Mainnet only on explicit node.network: "mainnet" — mainnet
|
|
328
343
|
// reads are safe; mainnet mutations use real funds (gated in getCoreControllerWithSigner).
|
|
@@ -365,6 +380,8 @@ export async function getCoreController(input) {
|
|
|
365
380
|
network,
|
|
366
381
|
accountAddress,
|
|
367
382
|
terminalApiKey,
|
|
383
|
+
rewardsDiscountBips,
|
|
384
|
+
rewardsClient,
|
|
368
385
|
};
|
|
369
386
|
return { ...cached, accountAddress };
|
|
370
387
|
}
|
|
@@ -450,6 +467,9 @@ function registerSignerHandlers(rootMessenger, childMessenger, account) {
|
|
|
450
467
|
'KeyringController:signTypedMessage received no typed data to sign.',
|
|
451
468
|
);
|
|
452
469
|
}
|
|
470
|
+
if (msgParams?.from && String(msgParams.from).toLowerCase() !== account.address.toLowerCase()) {
|
|
471
|
+
throw new Error(`KeyringController:signTypedMessage asked to sign for ${msgParams.from}; the selected account is ${account.address}.`);
|
|
472
|
+
}
|
|
453
473
|
const { domain, types, primaryType, message } = typedData;
|
|
454
474
|
// Strip the EIP712Domain entry if present: viem derives the domain types
|
|
455
475
|
// from `domain` itself and rejects a duplicate EIP712Domain in `types`.
|
|
@@ -486,16 +506,16 @@ function registerSignerHandlers(rootMessenger, childMessenger, account) {
|
|
|
486
506
|
* @returns { controller, projectRoot, network, accountAddress, signerAddress }.
|
|
487
507
|
*/
|
|
488
508
|
export async function getCoreControllerWithSigner(input) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
/^0x[0-9a-fA-F]{40}$/u.test(requestedName.trim())
|
|
493
|
-
)) {
|
|
509
|
+
// A selection given only as empty values must not quietly sign as dev1.
|
|
510
|
+
const selectors = ['account_name', 'account', 'address', 'userAddress'].filter((key) => input.node?.[key] != null);
|
|
511
|
+
if (selectors.length > 0 && selectors.every((key) => trimmedString(input.node[key]) === undefined)) {
|
|
494
512
|
throw new Error(
|
|
495
|
-
'Core signing requires a non-empty fixture account name
|
|
496
|
-
'Next: pass account=<
|
|
513
|
+
'Core signing requires a non-empty fixture account name or address.\n' +
|
|
514
|
+
'Next: pass account=<fixture account name>, or omit it to sign as dev1.',
|
|
497
515
|
);
|
|
498
516
|
}
|
|
517
|
+
// Refuse an account that cannot sign before any controller I/O.
|
|
518
|
+
const { account, address: signerAddress } = await resolveSignerFromFixture(input);
|
|
499
519
|
const base = await getCoreController(input);
|
|
500
520
|
const { controller, messenger: childMessenger, rootMessenger, accountAddress } = base;
|
|
501
521
|
if (!rootMessenger || !childMessenger) {
|
|
@@ -517,7 +537,6 @@ export async function getCoreControllerWithSigner(input) {
|
|
|
517
537
|
);
|
|
518
538
|
}
|
|
519
539
|
|
|
520
|
-
const { account, address: signerAddress } = await resolveSignerFromFixture(input);
|
|
521
540
|
registerSignerHandlers(rootMessenger, childMessenger, account);
|
|
522
541
|
|
|
523
542
|
// Bring up the active provider. placeOrder/closePosition call
|
|
@@ -741,7 +760,7 @@ export function redactOrder(order) {
|
|
|
741
760
|
};
|
|
742
761
|
}
|
|
743
762
|
|
|
744
|
-
// --- Adapter IO (mirrors the live-adapter contract in
|
|
763
|
+
// --- Adapter IO (mirrors the live-adapter contract in @farmslot/recipe-cli/harness live-adapter-contract) ---
|
|
745
764
|
|
|
746
765
|
async function loadInput() {
|
|
747
766
|
const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
|
|
@@ -766,7 +785,7 @@ const fmtError = (e) => e?.stack ?? e?.message ?? String(e);
|
|
|
766
785
|
* wsTransport.ready(). Orders themselves go over the HTTP ExchangeClient and
|
|
767
786
|
* RETURN normally, but that open WebSocket keeps the Node event loop alive, so
|
|
768
787
|
* the adapter process never exits. The runner only resolves on the child's
|
|
769
|
-
* `close` event (
|
|
788
|
+
* `close` event (@farmslot/recipe-cli/harness live-adapter-contract runProcess), so it waits out
|
|
770
789
|
* live_adapter_timeout_ms, SIGTERMs the child, and reports `fail` even though
|
|
771
790
|
* the trade filled and the output was computed. READ adapters use the
|
|
772
791
|
* standalone HTTP-only InfoClient (utils/standaloneInfoClient) — no WebSocket —
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Headless rewards client for the core Perps controller's `rewards` dependency.
|
|
2
|
+
// It makes the calls Mobile's RewardsController makes to answer
|
|
3
|
+
// getPerpsDiscountForAccount: public opt-in check, mobile-login with a
|
|
4
|
+
// personal_sign over `rewards,<address>,<unix seconds>`, then GET /vip/fees.
|
|
5
|
+
// The session token stays in memory and is never printed. Mobile also gates the
|
|
6
|
+
// call on its remote VIP feature flag (off: null); core has no remote flags, so
|
|
7
|
+
// the client answers as Mobile does with VIP enabled.
|
|
8
|
+
|
|
9
|
+
// Mobile's default outside production builds (METAMASK_ENVIRONMENT=dev on farm
|
|
10
|
+
// slots), from AppConstants.REWARDS_API_URL.UAT.
|
|
11
|
+
export const DEFAULT_REWARDS_API_URL = 'https://rewards.uat-api.cx.metamask.io';
|
|
12
|
+
|
|
13
|
+
const CLIENT_ID = 'mm-harness-core';
|
|
14
|
+
|
|
15
|
+
// Mobile's rewards-data-service DEFAULT_REQUEST_TIMEOUT_MS. A stalled backend
|
|
16
|
+
// must not hold a fee calculation (and the order behind it) open.
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
19
|
+
export function rewardsApiUrl(env = process.env) {
|
|
20
|
+
return (env.CORE_PERPS_REWARDS_API_URL?.trim() || DEFAULT_REWARDS_API_URL).replace(/\/+$/u, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mobile's VIP math (RewardsController #getVipPerpsDiscountBips): the backend
|
|
25
|
+
* returns an absolute builder fee, converted to a discount off `baseFeeBips`.
|
|
26
|
+
*
|
|
27
|
+
* @param vipFees - The /vip/fees body.
|
|
28
|
+
* @param baseFeeBips - The perps MetaMask builder base fee in bips.
|
|
29
|
+
* @returns Discount bips; 0 without a VIP fee; null for an unusable fee.
|
|
30
|
+
*/
|
|
31
|
+
export function vipDiscountBips(vipFees, baseFeeBips) {
|
|
32
|
+
if (!vipFees?.fees || !(vipFees.vipTier > 0)) return 0;
|
|
33
|
+
const raw = vipFees.fees.hyperliquid?.builderFeeBips;
|
|
34
|
+
if (raw === undefined || raw === null) return 0;
|
|
35
|
+
const builderFeeBips = Number.parseFloat(raw);
|
|
36
|
+
if (!Number.isFinite(builderFeeBips) || builderFeeBips < 0) return null;
|
|
37
|
+
const discount = Math.round(10000 * (1 - builderFeeBips / baseFeeBips));
|
|
38
|
+
return discount >= 0 && discount <= 10000 ? discount : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// An error response keeps only its status and the fields the client acts on:
|
|
42
|
+
// the body itself may echo the request, credentials included, and controller
|
|
43
|
+
// logs print error messages.
|
|
44
|
+
class RewardsApiError extends Error {
|
|
45
|
+
constructor(method, route, status, body) {
|
|
46
|
+
super(`rewards API ${method} ${route} returned ${status}.`);
|
|
47
|
+
this.status = status;
|
|
48
|
+
const data = parseErrorBody(body);
|
|
49
|
+
this.invalidTimestamp = /Invalid timestamp/u.test(String(data?.message ?? ''));
|
|
50
|
+
this.serverTimestampMs = Number(data?.serverTimestamp);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseErrorBody(body) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(body);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// A non-JSON error page carries no field the client acts on; the status
|
|
59
|
+
// already says what failed.
|
|
60
|
+
if (error instanceof SyntaxError) return null;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param options.signer - viem account of the selected fixture account, or null.
|
|
67
|
+
* @param options.baseUrl - Rewards API base URL.
|
|
68
|
+
* @param options.fetchImpl - fetch, injectable for tests.
|
|
69
|
+
* @param options.now - clock in ms, injectable for tests.
|
|
70
|
+
* @param options.timeoutMs - per-request deadline, headers and body included.
|
|
71
|
+
* @returns The `rewards` platform dependency plus `calls`, one entry per
|
|
72
|
+
* answered request, for action output.
|
|
73
|
+
*/
|
|
74
|
+
export function createRewardsClient({
|
|
75
|
+
signer,
|
|
76
|
+
baseUrl,
|
|
77
|
+
fetchImpl = fetch,
|
|
78
|
+
now = Date.now,
|
|
79
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
80
|
+
}) {
|
|
81
|
+
const calls = [];
|
|
82
|
+
let feesPromise;
|
|
83
|
+
|
|
84
|
+
async function request(method, route, { body, token } = {}) {
|
|
85
|
+
const response = await fetchImpl(`${baseUrl}${route}`, {
|
|
86
|
+
method,
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'rewards-client-id': CLIENT_ID,
|
|
90
|
+
...(token ? { 'rewards-access-token': token } : {}),
|
|
91
|
+
},
|
|
92
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
93
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
94
|
+
});
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
if (!response.ok) throw new RewardsApiError(method, route, response.status, text);
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function login(address, timestamp, retried = false) {
|
|
101
|
+
const message = `rewards,${address},${timestamp}`;
|
|
102
|
+
const signature = await signer.signMessage({ message });
|
|
103
|
+
try {
|
|
104
|
+
return await request('POST', '/auth/mobile-login', { body: { account: address, timestamp, signature } });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Mobile re-signs once with the server clock when the backend rejects ours.
|
|
107
|
+
if (
|
|
108
|
+
!(error instanceof RewardsApiError) || retried || !error.invalidTimestamp ||
|
|
109
|
+
!Number.isFinite(error.serverTimestampMs)
|
|
110
|
+
) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
return login(address, Math.floor(error.serverTimestampMs / 1000), true);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Mobile's silent auth: skip login only when the public opt-in check says the
|
|
118
|
+
// account never opted in, and read a 401 from login as not opted in.
|
|
119
|
+
async function openSession(address) {
|
|
120
|
+
let optedIn;
|
|
121
|
+
try {
|
|
122
|
+
const status = await request('POST', '/public/rewards/ois', { body: { addresses: [address] } });
|
|
123
|
+
optedIn = status?.ois?.[0];
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Mobile logs in anyway when the opt-in check fails: login answers the
|
|
126
|
+
// same question. A failure to reach the backend at all still throws.
|
|
127
|
+
if (!(error instanceof RewardsApiError)) throw error;
|
|
128
|
+
}
|
|
129
|
+
if (optedIn === false) return { optedIn: false };
|
|
130
|
+
let sessionId;
|
|
131
|
+
try {
|
|
132
|
+
({ sessionId } = await login(address, Math.floor(now() / 1000)));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof RewardsApiError && error.status === 401) return { optedIn: false };
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
138
|
+
throw new Error('rewards API mobile-login returned no sessionId.');
|
|
139
|
+
}
|
|
140
|
+
return { optedIn: true, token: sessionId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// One login and one /vip/fees read per process (Mobile caches the fees for
|
|
144
|
+
// five minutes; an action process is shorter). The token is fresh, so the
|
|
145
|
+
// 403 re-login Mobile needs for a stored token does not arise here.
|
|
146
|
+
async function vipFees(address) {
|
|
147
|
+
const session = await openSession(address);
|
|
148
|
+
if (!session.optedIn) return session;
|
|
149
|
+
return { optedIn: true, fees: await request('GET', '/vip/fees', { token: session.token }) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function getPerpsDiscountForAccount(caipAccountId, baseFeeBips) {
|
|
153
|
+
// Lowercase, as Mobile's internal accounts store EVM addresses; the backend
|
|
154
|
+
// matches addresses case-insensitively.
|
|
155
|
+
const address = String(caipAccountId).split(':')[2]?.toLowerCase();
|
|
156
|
+
const record = (entry) => {
|
|
157
|
+
calls.push({ caipAccountId, baseFeeBips, ...entry });
|
|
158
|
+
return entry.discountBips;
|
|
159
|
+
};
|
|
160
|
+
if (!(Number.isFinite(baseFeeBips) && baseFeeBips > 0)) {
|
|
161
|
+
return record({ discountBips: null, reason: 'invalid-base-fee' });
|
|
162
|
+
}
|
|
163
|
+
// Only the selected fixture account can sign in; any other account has no
|
|
164
|
+
// subscription here, which Mobile also answers with null.
|
|
165
|
+
if (!signer || address !== signer.address.toLowerCase()) {
|
|
166
|
+
return record({ discountBips: null, reason: 'no-signer-for-account' });
|
|
167
|
+
}
|
|
168
|
+
feesPromise ??= vipFees(address);
|
|
169
|
+
let result;
|
|
170
|
+
try {
|
|
171
|
+
result = await feesPromise;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Not cached: the next fee calculation asks the backend again.
|
|
174
|
+
feesPromise = undefined;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (!result.optedIn) return record({ discountBips: null, reason: 'not-opted-in' });
|
|
178
|
+
return record({
|
|
179
|
+
discountBips: vipDiscountBips(result.fees, baseFeeBips),
|
|
180
|
+
vipTier: result.fees.vipTier,
|
|
181
|
+
builderFeeBips: result.fees.fees?.hyperliquid?.builderFeeBips ?? null,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { getPerpsDiscountForAccount, calls };
|
|
186
|
+
}
|
|
@@ -19,7 +19,7 @@ function sleep(ms) {
|
|
|
19
19
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function waitForPositionsAbsent(controller, accountAddress, symbols, timeoutMs) {
|
|
22
|
+
export async function waitForPositionsAbsent(controller, accountAddress, symbols, timeoutMs) {
|
|
23
23
|
const deadline = Date.now() + timeoutMs;
|
|
24
24
|
let positions = [];
|
|
25
25
|
for (;;) {
|