@metamask-previews/perps-controller 22.0.0-preview-35f016caa → 22.1.0-preview-01e391e

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/dist/PerpsController-method-action-types.d.ts +13 -1
  3. package/dist/PerpsController-method-action-types.d.ts.map +1 -1
  4. package/dist/PerpsController-method-action-types.js.map +1 -1
  5. package/dist/PerpsController.d.ts +10 -1
  6. package/dist/PerpsController.d.ts.map +1 -1
  7. package/dist/PerpsController.js +18 -0
  8. package/dist/PerpsController.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/AggregatedPerpsProvider.d.ts +9 -1
  13. package/dist/providers/AggregatedPerpsProvider.d.ts.map +1 -1
  14. package/dist/providers/AggregatedPerpsProvider.js +13 -0
  15. package/dist/providers/AggregatedPerpsProvider.js.map +1 -1
  16. package/dist/providers/HyperLiquidProvider.d.ts +7 -1
  17. package/dist/providers/HyperLiquidProvider.d.ts.map +1 -1
  18. package/dist/providers/HyperLiquidProvider.js +831 -172
  19. package/dist/providers/HyperLiquidProvider.js.map +1 -1
  20. package/dist/providers/LighterProvider.d.ts +1 -1
  21. package/dist/providers/LighterProvider.d.ts.map +1 -1
  22. package/dist/providers/LighterProvider.js +69 -9
  23. package/dist/providers/LighterProvider.js.map +1 -1
  24. package/dist/services/FeatureFlagConfigurationService.d.ts.map +1 -1
  25. package/dist/services/FeatureFlagConfigurationService.js +18 -8
  26. package/dist/services/FeatureFlagConfigurationService.js.map +1 -1
  27. package/dist/services/HyperLiquidSubscriptionService.d.ts.map +1 -1
  28. package/dist/services/HyperLiquidSubscriptionService.js +10 -3
  29. package/dist/services/HyperLiquidSubscriptionService.js.map +1 -1
  30. package/dist/types/index.d.ts +50 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/index.js.map +1 -1
  33. package/dist/utils/accountUtils.d.ts +9 -1
  34. package/dist/utils/accountUtils.d.ts.map +1 -1
  35. package/dist/utils/accountUtils.js +28 -0
  36. package/dist/utils/accountUtils.js.map +1 -1
  37. package/dist/utils/hyperLiquidAdapter.d.ts.map +1 -1
  38. package/dist/utils/hyperLiquidAdapter.js +28 -0
  39. package/dist/utils/hyperLiquidAdapter.js.map +1 -1
  40. package/dist/utils/lighterAdapter.d.ts +17 -1
  41. package/dist/utils/lighterAdapter.d.ts.map +1 -1
  42. package/dist/utils/lighterAdapter.js +67 -2
  43. package/dist/utils/lighterAdapter.js.map +1 -1
  44. package/dist/utils/stringParseUtils.d.ts +7 -0
  45. package/dist/utils/stringParseUtils.d.ts.map +1 -1
  46. package/dist/utils/stringParseUtils.js +14 -0
  47. package/dist/utils/stringParseUtils.js.map +1 -1
  48. package/package.json +4 -4
@@ -791,6 +791,9 @@ export class HyperLiquidProvider {
791
791
  // Session cache for referral state (cleared on disconnect/reconnect)
792
792
  // Key: `network:userAddress`, Value: true if referral is set
793
793
  #referralCheckCache = new Map();
794
+ // Session-scoped, account/network-keyed support probes. Provider instances
795
+ // are discarded on reconnect; rejected probes are not retained.
796
+ #accountSupportByContext = new Map();
794
797
  // Session cache for builder fee approval state (cleared on disconnect/reconnect)
795
798
  // Key: `network:userAddress`, Value: true if builder fee is approved
796
799
  #builderFeeCheckCache = new Map();
@@ -1736,19 +1739,21 @@ export class HyperLiquidProvider {
1736
1739
  * multi-sig account with `ApiRequestError: Multi-sig required`, so the
1737
1740
  * unified-account migration must not be attempted for those accounts.
1738
1741
  *
1739
- * If the probe throws (transient network), returns `false` — fail open so
1740
- * one bad probe never blocks migration for a normal single-signer account.
1742
+ * If the probe throws (transient network), returns `undefined` so callers
1743
+ * can fail open without caching the transient result.
1741
1744
  * The `isHyperLiquidMultiSigRequiredError` fallback in the write's catch
1742
1745
  * block remains the safety net.
1743
1746
  *
1744
1747
  * @param userAddress - The wallet address to check.
1745
1748
  * @param infoClient - Optional captured reader for an operation-scoped probe.
1746
- * @returns True only when Hyperliquid reports a multi-sig signer set.
1749
+ * @returns True for multi-sig, false for single-signer, or undefined when
1750
+ * the probe could not complete.
1747
1751
  * @private
1748
1752
  */
1749
- async #isHyperliquidMultiSigAccount(userAddress, infoClient = this.#clientService.getInfoClient()) {
1753
+ async #isHyperliquidMultiSigAccount(userAddress, infoClient) {
1750
1754
  try {
1751
- const signers = await infoClient.userToMultiSigSigners({
1755
+ const reader = infoClient ?? this.#clientService.getInfoClient();
1756
+ const signers = await reader.userToMultiSigSigners({
1752
1757
  user: userAddress,
1753
1758
  });
1754
1759
  return signers !== null && signers !== undefined;
@@ -1758,8 +1763,176 @@ export class HyperLiquidProvider {
1758
1763
  user: userAddress,
1759
1764
  error: ensureError(error, 'HyperLiquidProvider.isHyperliquidMultiSigAccount').message,
1760
1765
  });
1766
+ return undefined;
1767
+ }
1768
+ }
1769
+ #getAccountSupportCacheKey(network, userAddress) {
1770
+ return `${network}:${userAddress.toLowerCase()}`;
1771
+ }
1772
+ #recordAccountSupport(network, userAddress, support) {
1773
+ this.#accountSupportByContext.set(this.#getAccountSupportCacheKey(network, userAddress), Promise.resolve(support));
1774
+ }
1775
+ /**
1776
+ * Replace an optimistic support result after the venue authoritatively
1777
+ * rejects a write from the account that supplied its signature.
1778
+ *
1779
+ * The captured context, rather than the currently selected account, owns the
1780
+ * rejection because selection may change while the request is in flight.
1781
+ *
1782
+ * @param error - The failed write or mapped account-mode error.
1783
+ * @param accountContext - Account and network that authorized the write.
1784
+ * @returns Whether the rejection requires Hyperliquid multi-signature.
1785
+ */
1786
+ #recordUnsupportedAccountFromError(error, accountContext) {
1787
+ const isMultiSigRequired = isHyperLiquidMultiSigRequiredError(error) ||
1788
+ ensureError(error, 'HyperLiquidProvider.recordUnsupportedAccountFromError').message === PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED;
1789
+ if (!isMultiSigRequired) {
1761
1790
  return false;
1762
1791
  }
1792
+ this.#recordAccountSupport(accountContext.network, accountContext.userAddress, {
1793
+ isSupported: false,
1794
+ reason: 'multi_sig_account',
1795
+ });
1796
+ return true;
1797
+ }
1798
+ /**
1799
+ * Correct support from errors returned as resolved per-entry statuses.
1800
+ *
1801
+ * @param statuses - Venue status entries.
1802
+ * @param answer - Full venue answer that owns the statuses.
1803
+ * @param accountContext - Account and network that authorized the write.
1804
+ * @returns Whether any status requires Hyperliquid multi-signature.
1805
+ */
1806
+ #recordUnsupportedAccountFromStatuses(statuses, answer, accountContext) {
1807
+ return statuses.some((status) => isStatusObject(status) &&
1808
+ typeof status.error === 'string' &&
1809
+ this.#recordUnsupportedAccountFromError(createStatusError(status.error, answer), accountContext));
1810
+ }
1811
+ /**
1812
+ * Read support for an account context without initializing the provider.
1813
+ * Successful results are stable for the provider session and coalesced by
1814
+ * account and network. Failed probes fail open and remain retryable.
1815
+ *
1816
+ * @param context - An operation-scoped account context. Defaults to the
1817
+ * currently active account and network.
1818
+ * @returns Whether the account can submit Hyperliquid actions.
1819
+ */
1820
+ async #getAccountSupportForContext(context) {
1821
+ const userAddress = context?.userAddress ??
1822
+ (await this.#walletService.getUserAddressWithDefault());
1823
+ const network = context?.network ??
1824
+ (this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
1825
+ const cacheKey = this.#getAccountSupportCacheKey(network, userAddress);
1826
+ const cachedSupport = this.#accountSupportByContext.get(cacheKey);
1827
+ if (cachedSupport) {
1828
+ return cachedSupport;
1829
+ }
1830
+ const probe = this.#isHyperliquidMultiSigAccount(userAddress, context?.infoClient);
1831
+ const support = probe.then((isMultiSig) => isMultiSig
1832
+ ? { isSupported: false, reason: 'multi_sig_account' }
1833
+ : { isSupported: true }, () => ({ isSupported: true }));
1834
+ this.#accountSupportByContext.set(cacheKey, support);
1835
+ probe
1836
+ .then((isMultiSig) => {
1837
+ if (isMultiSig === undefined &&
1838
+ this.#accountSupportByContext.get(cacheKey) === support) {
1839
+ this.#accountSupportByContext.delete(cacheKey);
1840
+ }
1841
+ }, () => {
1842
+ if (this.#accountSupportByContext.get(cacheKey) === support) {
1843
+ this.#accountSupportByContext.delete(cacheKey);
1844
+ }
1845
+ })
1846
+ .catch(() => undefined);
1847
+ return support;
1848
+ }
1849
+ async #assertAccountContextCurrent(context, operation) {
1850
+ this.#assertProviderLifecycleCurrent(context.lifecycleGeneration, operation);
1851
+ await this.#assertAccountIdentityCurrent(context);
1852
+ this.#assertProviderLifecycleCurrent(context.lifecycleGeneration, operation);
1853
+ }
1854
+ /**
1855
+ * Confirm that the selected address and network still match an action,
1856
+ * without requiring the provider lifecycle that created it to remain active.
1857
+ *
1858
+ * @param context - The account and network captured by the action.
1859
+ */
1860
+ async #assertAccountIdentityCurrent(context) {
1861
+ const currentAddress = await this.#walletService
1862
+ .getUserAddressWithDefault()
1863
+ .catch(() => undefined);
1864
+ const currentNetwork = this.#clientService.isTestnetMode()
1865
+ ? 'testnet'
1866
+ : 'mainnet';
1867
+ if (currentAddress?.toLowerCase() !== context.userAddress.toLowerCase() ||
1868
+ currentNetwork !== context.network) {
1869
+ throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
1870
+ }
1871
+ }
1872
+ /**
1873
+ * Permit best-effort rollback after teardown only while it still belongs to
1874
+ * the same address and network, and the account is not known unsupported.
1875
+ *
1876
+ * @param context - The account and network captured by the parent action.
1877
+ */
1878
+ async #assertRollbackAccountWritable(context) {
1879
+ await this.#assertAccountIdentityCurrent(context);
1880
+ const cachedSupport = this.#accountSupportByContext.get(this.#getAccountSupportCacheKey(context.network, context.userAddress));
1881
+ const support = cachedSupport ? await cachedSupport : undefined;
1882
+ await this.#assertAccountIdentityCurrent(context);
1883
+ if (support && !support.isSupported) {
1884
+ throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
1885
+ }
1886
+ }
1887
+ /**
1888
+ * Block an action when another step has authoritatively corrected support
1889
+ * for its captured account. A missing cache entry remains fail-open: this
1890
+ * fence must not turn a transient probe failure into another network probe.
1891
+ *
1892
+ * @param context - The account, network, and lifecycle captured by the action.
1893
+ * @param operation - Operation name included in lifecycle diagnostics.
1894
+ */
1895
+ async #assertNoKnownUnsupportedAccount(context, operation) {
1896
+ await this.#assertAccountContextCurrent(context, operation);
1897
+ const cachedSupport = this.#accountSupportByContext.get(this.#getAccountSupportCacheKey(context.network, context.userAddress));
1898
+ const support = cachedSupport ? await cachedSupport : undefined;
1899
+ await this.#assertAccountContextCurrent(context, operation);
1900
+ if (support && !support.isSupported) {
1901
+ throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
1902
+ }
1903
+ }
1904
+ /**
1905
+ * Create an operation-local exchange client that revalidates its captured
1906
+ * account after signing and immediately before transport dispatch.
1907
+ *
1908
+ * @param context - Account, network, and lifecycle authorized for the write.
1909
+ * @param operation - Operation name included in lifecycle diagnostics.
1910
+ * @returns An exchange client fenced to the captured account context.
1911
+ */
1912
+ #getAccountGuardedExchangeClient(context, operation) {
1913
+ return this.#clientService.getExchangeClient(async () => await this.#assertNoKnownUnsupportedAccount(context, operation));
1914
+ }
1915
+ /**
1916
+ * Stop an action before signing when Hyperliquid reports a native multi-sig
1917
+ * signer set for the active account.
1918
+ *
1919
+ * @param expectedContext - Existing operation context to validate, or omit
1920
+ * to capture the currently selected account and network.
1921
+ * @returns The account context whose support was checked.
1922
+ */
1923
+ async #assertAccountSupported(expectedContext) {
1924
+ const context = expectedContext ??
1925
+ {
1926
+ lifecycleGeneration: this.#lifecycleGeneration,
1927
+ network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
1928
+ userAddress: await this.#walletService.getUserAddressWithDefault(),
1929
+ };
1930
+ const support = await this.#getAccountSupportForContext(context);
1931
+ await this.#assertAccountContextCurrent(context, 'Account support check');
1932
+ if (!support.isSupported) {
1933
+ throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
1934
+ }
1935
+ return context;
1763
1936
  }
1764
1937
  /**
1765
1938
  * Attempt to enable HyperLiquid Unified Account mode for HIP-3 orders
@@ -1772,6 +1945,8 @@ export class HyperLiquidProvider {
1772
1945
  *
1773
1946
  * @param options - Optional configuration.
1774
1947
  * @param options.allowUserSigning - When true, runs the migration for `default` / `disabled` accounts. Defaults to false so init does not surface a signing prompt; action-time entry points (trading, withdraw) pass true.
1948
+ * @param options.accountContext - Account/network/lifecycle captured by the
1949
+ * action that is allowed to authorize the migration.
1775
1950
  * @private
1776
1951
  */
1777
1952
  async #ensureUnifiedAccountEnabled(options) {
@@ -1790,8 +1965,11 @@ export class HyperLiquidProvider {
1790
1965
  if (!this.#useUnifiedAccount) {
1791
1966
  return; // Feature disabled
1792
1967
  }
1793
- const userAddress = await this.#walletService.getUserAddressWithDefault();
1794
- const network = this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet';
1968
+ const { accountContext } = options ?? {};
1969
+ const userAddress = accountContext?.userAddress ??
1970
+ (await this.#walletService.getUserAddressWithDefault());
1971
+ const network = accountContext?.network ??
1972
+ (this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
1795
1973
  // Check global cache first to avoid repeated signing requests
1796
1974
  // This is CRITICAL for hardware wallets to prevent repeated signing prompts
1797
1975
  // while browsing.
@@ -1859,6 +2037,9 @@ export class HyperLiquidProvider {
1859
2037
  currentMode = await infoClient.userAbstraction({
1860
2038
  user: userAddress,
1861
2039
  });
2040
+ if (accountContext) {
2041
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account mode lookup');
2042
+ }
1862
2043
  if (currentMode === 'unifiedAccount' ||
1863
2044
  currentMode === 'portfolioMargin') {
1864
2045
  // portfolioMargin is a superset of unifiedAccount — it already supports
@@ -1902,17 +2083,21 @@ export class HyperLiquidProvider {
1902
2083
  // surfaced on the Perps tab on every entry (TAT-3214). Probe right
1903
2084
  // before the write so accounts that never reach one (already compatible,
1904
2085
  // deferred, unknown mode) do not pay the extra round trip.
1905
- const isMultiSig = await this.#isHyperliquidMultiSigAccount(userAddress);
1906
- if (isMultiSig) {
2086
+ const accountSupport = await this.#getAccountSupportForContext({
2087
+ userAddress,
2088
+ network,
2089
+ infoClient,
2090
+ });
2091
+ if (!accountSupport.isSupported) {
1907
2092
  this.#deps.debugLogger.log('[ensureUnifiedAccountEnabled] Multi-sig account, skipping unified account migration', { user: userAddress, network, mode: currentMode });
1908
2093
  this.#deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.AccountSetup, {
1909
2094
  [PERPS_EVENT_PROPERTY.PREVIOUS_ABSTRACTION_MODE]: currentMode,
1910
2095
  [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.NOT_APPLICABLE,
1911
2096
  [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: 'multi_sig_account',
1912
2097
  });
1913
- // Final state: the write can never succeed for this account, so cache
1914
- // it as attempted with unified mode off. Perps keeps working through
1915
- // the programmatic collateral-transfer fallback.
2098
+ // Final state: the migration can never succeed for this account, so
2099
+ // cache it as attempted with unified mode off. Action readiness blocks
2100
+ // this account before any later exchange write.
1916
2101
  TradingReadinessCache.set(network, userAddress, {
1917
2102
  attempted: true,
1918
2103
  enabled: false,
@@ -1933,7 +2118,13 @@ export class HyperLiquidProvider {
1933
2118
  previousMode: currentMode,
1934
2119
  note: 'HyperLiquid will auto-manage collateral for HIP-3 orders',
1935
2120
  });
1936
- await this.#clientService.getExchangeClient().agentSetAbstraction({
2121
+ if (accountContext) {
2122
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account migration');
2123
+ }
2124
+ const exchangeClient = this.#clientService.getExchangeClient(accountContext
2125
+ ? async () => await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account migration dispatch')
2126
+ : undefined);
2127
+ await exchangeClient.agentSetAbstraction({
1937
2128
  abstraction: HL_ABSTRACTION_WIRE.unifiedAccount,
1938
2129
  });
1939
2130
  this.#deps.debugLogger.log('✅ HyperLiquidProvider: Unified Account enabled successfully');
@@ -1988,6 +2179,10 @@ export class HyperLiquidProvider {
1988
2179
  // account-shape condition, not a failure worth reporting or retrying.
1989
2180
  if (isHyperLiquidMultiSigRequiredError(error)) {
1990
2181
  this.#deps.debugLogger.log('[ensureUnifiedAccountEnabled] Multi-sig account (race/probe fallback), skipping unified account migration', { user: userAddress, network, mode: currentMode });
2182
+ this.#recordAccountSupport(network, userAddress, {
2183
+ isSupported: false,
2184
+ reason: 'multi_sig_account',
2185
+ });
1991
2186
  this.#deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.AccountSetup, {
1992
2187
  ...(currentMode && {
1993
2188
  [PERPS_EVENT_PROPERTY.PREVIOUS_ABSTRACTION_MODE]: currentMode,
@@ -2020,6 +2215,12 @@ export class HyperLiquidProvider {
2020
2215
  completeInFlight();
2021
2216
  return;
2022
2217
  }
2218
+ if (accountContext &&
2219
+ ensureError(error, 'HyperLiquidProvider.ensureUnifiedAccountEnabled')
2220
+ .message === PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
2221
+ completeInFlight();
2222
+ throw error;
2223
+ }
2023
2224
  // Agent-path failures and read-only userAbstraction lookup failures
2024
2225
  // are not final: signal #ensureReady to drop its memoized promise and
2025
2226
  // retry on the next entry instead of pinning the user in the
@@ -2088,10 +2289,26 @@ export class HyperLiquidProvider {
2088
2289
  // over. The memoized promise is dropped while the cooldown is active
2089
2290
  // (see below), so a later entry retries. Action-time callers are not
2090
2291
  // gated.
2091
- if (Date.now() >= this.#unifiedAccountTransportRetryAt) {
2092
- await this.#ensureUnifiedAccountEnabled({
2093
- allowUserSigning: !this.#walletService.requiresSignatureConfirmation(),
2094
- });
2292
+ if (!this.#isDisconnected &&
2293
+ Date.now() >= this.#unifiedAccountTransportRetryAt) {
2294
+ if (this.#walletService.requiresSignatureConfirmation()) {
2295
+ await this.#ensureUnifiedAccountEnabled({
2296
+ allowUserSigning: false,
2297
+ });
2298
+ }
2299
+ else {
2300
+ const accountContext = {
2301
+ lifecycleGeneration: this.#lifecycleGeneration,
2302
+ network: this.#clientService.isTestnetMode()
2303
+ ? 'testnet'
2304
+ : 'mainnet',
2305
+ userAddress: await this.#walletService.getUserAddressWithDefault(),
2306
+ };
2307
+ await this.#ensureUnifiedAccountEnabled({
2308
+ allowUserSigning: true,
2309
+ accountContext,
2310
+ });
2311
+ }
2095
2312
  }
2096
2313
  })();
2097
2314
  // Await initialization - keep the promise so subsequent calls resolve immediately
@@ -2169,13 +2386,17 @@ export class HyperLiquidProvider {
2169
2386
  * code (the approval is otherwise non-blocking).
2170
2387
  * @param options.reportRefusal - Throw the venue's refusal, even without an
2171
2388
  * approval failure code.
2389
+ * @param accountContext - Account/network/lifecycle captured by the action
2390
+ * that is allowed to authorize this approval.
2172
2391
  * @returns The account, network, and configured builder for the action.
2173
2392
  */
2174
- async #ensureBuilderFeeSetup(approvalFailureCode, options = {}) {
2175
- const lifecycleGeneration = this.#lifecycleGeneration;
2176
- const isTestnet = this.#clientService.isTestnetMode();
2177
- const network = isTestnet ? 'testnet' : 'mainnet';
2178
- const userAddress = await this.#walletService.getUserAddressWithDefault();
2393
+ async #ensureBuilderFeeSetup(approvalFailureCode, options = {}, accountContext) {
2394
+ const lifecycleGeneration = accountContext?.lifecycleGeneration ?? this.#lifecycleGeneration;
2395
+ const network = accountContext?.network ??
2396
+ (this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
2397
+ const isTestnet = network === 'testnet';
2398
+ const userAddress = accountContext?.userAddress ??
2399
+ (await this.#walletService.getUserAddressWithDefault());
2179
2400
  const cacheKey = this.#getCacheKey(network, userAddress);
2180
2401
  const builderAddress = this.#getBuilderAddress(isTestnet);
2181
2402
  const context = {
@@ -2203,7 +2424,7 @@ export class HyperLiquidProvider {
2203
2424
  }
2204
2425
  let pendingApproval = this.#builderFeeSetupPromises.get(setupKey);
2205
2426
  if (!pendingApproval) {
2206
- pendingApproval = this.#ensureBuilderFeeApproval(context);
2427
+ pendingApproval = this.#ensureBuilderFeeApproval(context, accountContext);
2207
2428
  this.#builderFeeSetupPromises.set(setupKey, pendingApproval);
2208
2429
  }
2209
2430
  try {
@@ -2211,6 +2432,10 @@ export class HyperLiquidProvider {
2211
2432
  }
2212
2433
  catch (error) {
2213
2434
  this.#deps.debugLogger.log('[ensureBuilderFeeSetup] Builder fee approval failed', error);
2435
+ if (accountContext &&
2436
+ this.#recordUnsupportedAccountFromError(error, accountContext)) {
2437
+ throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
2438
+ }
2214
2439
  // A signer that could not sign is retryable, not an approval failure.
2215
2440
  if (approvalFailureCode || options.reportSignerFailure) {
2216
2441
  const signerFailure = await this.#classifySignerFailure(error);
@@ -2243,10 +2468,15 @@ export class HyperLiquidProvider {
2243
2468
  async #ensureReadyForTrading(options) {
2244
2469
  // First ensure basic initialization is complete
2245
2470
  await this.#ensureReady();
2471
+ const accountContext = await this.#assertAccountSupported(options.accountContext);
2246
2472
  // The migration was deferred during init to avoid a signing prompt on
2247
2473
  // Perps section open. Drive it here, gated by its own cache so
2248
2474
  // already-migrated users are not re-prompted.
2249
- await this.#ensureUnifiedAccountEnabled({ allowUserSigning: true });
2475
+ await this.#ensureUnifiedAccountEnabled({
2476
+ allowUserSigning: true,
2477
+ accountContext,
2478
+ });
2479
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Trading setup');
2250
2480
  // Reset right before the check, with no await in between, so a failure
2251
2481
  // above does not leave the setup for an order to run.
2252
2482
  if (options.recheckPendingReferral && this.#referralAwaitsBuilderCode) {
@@ -2268,7 +2498,7 @@ export class HyperLiquidProvider {
2268
2498
  }
2269
2499
  }
2270
2500
  // Set up referral code independently from builder-fee applicability.
2271
- await this.#ensureReferralSet();
2501
+ await this.#ensureReferralSet(accountContext);
2272
2502
  this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Trading setup completion');
2273
2503
  // Only mark complete if the main-account signer is ready (signing
2274
2504
  // could actually happen) and the referral does not need another
@@ -2291,10 +2521,14 @@ export class HyperLiquidProvider {
2291
2521
  }
2292
2522
  }
2293
2523
  const builderFeeSetupContext = options.requiresBuilderFee
2294
- ? await this.#ensureBuilderFeeSetup(options.builderFeeApprovalFailureCode)
2524
+ ? await this.#ensureBuilderFeeSetup(options.builderFeeApprovalFailureCode, {}, accountContext)
2295
2525
  : undefined;
2526
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Trading setup');
2296
2527
  this.#deps.debugLogger.log('[ensureReadyForTrading] Trading setup complete');
2297
- return builderFeeSetupContext;
2528
+ return {
2529
+ accountContext,
2530
+ ...(builderFeeSetupContext && { builderFeeSetupContext }),
2531
+ };
2298
2532
  }
2299
2533
  /**
2300
2534
  * Get current price for a symbol using WebSocket cache first, REST API fallback
@@ -3228,10 +3462,14 @@ export class HyperLiquidProvider {
3228
3462
  *
3229
3463
  * @param message - The status entry's error.
3230
3464
  * @param answer - The venue answer with the entry.
3465
+ * @param accountContext - Account and network that authorized the write.
3231
3466
  * @returns The mapped error.
3232
3467
  */
3233
- async #mapStatusError(message, answer) {
3468
+ async #mapStatusError(message, answer, accountContext) {
3234
3469
  const error = createStatusError(message, answer);
3470
+ if (accountContext) {
3471
+ this.#recordUnsupportedAccountFromError(error, accountContext);
3472
+ }
3235
3473
  return (await this.#classifySignerFailure(error)) ?? this.#mapError(error);
3236
3474
  }
3237
3475
  /**
@@ -3360,10 +3598,17 @@ export class HyperLiquidProvider {
3360
3598
  * @param params.network - HyperLiquid network for the approval.
3361
3599
  * @param params.userAddress - Account that owns the approval.
3362
3600
  * @param params.builderAddress - Builder address being approved.
3601
+ * @param accountContext - Account/network/lifecycle captured by the action
3602
+ * that is allowed to authorize this approval.
3363
3603
  */
3364
- async #ensureBuilderFeeApproval(params) {
3365
- const lifecycleGeneration = this.#lifecycleGeneration;
3604
+ async #ensureBuilderFeeApproval(params, accountContext) {
3605
+ const lifecycleGeneration = accountContext?.lifecycleGeneration ?? this.#lifecycleGeneration;
3366
3606
  const { network, userAddress, builderAddress } = params;
3607
+ const supportContext = accountContext ?? {
3608
+ lifecycleGeneration,
3609
+ network,
3610
+ userAddress,
3611
+ };
3367
3612
  this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Builder fee approval');
3368
3613
  const cacheKey = this.#getCacheKey(network, userAddress);
3369
3614
  // Check GLOBAL cache first to avoid repeated signing requests across reconnections
@@ -3401,7 +3646,12 @@ export class HyperLiquidProvider {
3401
3646
  return;
3402
3647
  }
3403
3648
  const { isApproved, requiredDecimal } = await this.#checkBuilderFeeStatus(builderAddress, userAddress);
3404
- this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Builder fee approval');
3649
+ if (accountContext) {
3650
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Builder fee approval lookup');
3651
+ }
3652
+ else {
3653
+ this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Builder fee approval');
3654
+ }
3405
3655
  if (isApproved) {
3406
3656
  // User already has approval on-chain
3407
3657
  PerpsSigningCache.setBuilderFee(network, userAddress, {
@@ -3414,12 +3664,20 @@ export class HyperLiquidProvider {
3414
3664
  }
3415
3665
  else {
3416
3666
  this.#deps.debugLogger.log('[ensureBuilderFeeApproval] Approval required (will show signing request)', { builder: builderAddress, requiredDecimal });
3417
- const exchangeClient = this.#clientService.getExchangeClient();
3667
+ if (accountContext) {
3668
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Builder fee approval');
3669
+ }
3670
+ const exchangeClient = this.#getAccountGuardedExchangeClient(supportContext, 'Builder fee approval dispatch');
3418
3671
  const maxFeeRate = BUILDER_FEE_CONFIG.MaxFeeRate;
3419
- await exchangeClient.approveBuilderFee({
3672
+ const approvalResult = await exchangeClient.approveBuilderFee({
3420
3673
  builder: builderAddress,
3421
3674
  maxFeeRate,
3422
3675
  });
3676
+ if (approvalResult.status !== 'ok') {
3677
+ const approvalError = new Error(`Builder fee approval failed: ${JSON.stringify(approvalResult)}`);
3678
+ this.#recordUnsupportedAccountFromError(approvalError, supportContext);
3679
+ throw approvalError;
3680
+ }
3423
3681
  // Verify approval was successful before caching
3424
3682
  const afterApprovalDecimal = await this.#checkBuilderFeeApproval(builderAddress, userAddress);
3425
3683
  if (afterApprovalDecimal === null ||
@@ -3445,6 +3703,7 @@ export class HyperLiquidProvider {
3445
3703
  completeInFlight();
3446
3704
  }
3447
3705
  catch (error) {
3706
+ this.#recordUnsupportedAccountFromError(error, supportContext);
3448
3707
  if (ensureError(error, 'HyperLiquidProvider.ensureBuilderFeeApproval')
3449
3708
  .message === PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
3450
3709
  completeInFlight();
@@ -3641,11 +3900,13 @@ export class HyperLiquidProvider {
3641
3900
  * @param params - Transfer parameters
3642
3901
  * @param params.targetDex - HIP-3 DEX name (e.g., 'xyz')
3643
3902
  * @param params.requiredMargin - Required margin with buffer
3903
+ * @param params.accountContext - Account/network/lifecycle captured by the
3904
+ * order that may authorize a transfer.
3644
3905
  * @returns Transfer info for rollback, or null if no transfer needed
3645
3906
  * @private
3646
3907
  */
3647
3908
  async #autoTransferForHip3Order(params) {
3648
- const { targetDex, requiredMargin } = params;
3909
+ const { targetDex, requiredMargin, accountContext } = params;
3649
3910
  // Check target DEX balance
3650
3911
  const targetBalance = await this.#getBalanceForDex({ dex: targetDex });
3651
3912
  this.#deps.debugLogger.log('HyperLiquidProvider: HIP-3 balance check', {
@@ -3674,15 +3935,19 @@ export class HyperLiquidProvider {
3674
3935
  to: targetDex,
3675
3936
  amount: transferAmount,
3676
3937
  });
3677
- const result = await this.transferBetweenDexs({
3938
+ const transferParams = {
3678
3939
  sourceDex: source.sourceDex,
3679
3940
  destinationDex: targetDex,
3680
3941
  amount: transferAmount,
3681
- });
3942
+ };
3943
+ const result = accountContext
3944
+ ? await this.#transferBetweenDexsForContext(transferParams, accountContext)
3945
+ : await this.transferBetweenDexs(transferParams);
3682
3946
  if (!result.success) {
3683
- // The signer could not sign the transfer: retryable, not a defect.
3684
- if (result.error === PERPS_ERROR_CODES.KEYRING_LOCKED) {
3685
- throw new Error(PERPS_ERROR_CODES.KEYRING_LOCKED);
3947
+ // Preserve stable account/signer outcomes that callers handle directly.
3948
+ if (result.error === PERPS_ERROR_CODES.KEYRING_LOCKED ||
3949
+ result.error === PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED) {
3950
+ throw new Error(result.error);
3686
3951
  }
3687
3952
  throw new Error(`Auto-transfer failed: ${result.error ?? 'Unknown error'}`);
3688
3953
  }
@@ -3710,11 +3975,13 @@ export class HyperLiquidProvider {
3710
3975
  * @param params.freedMargin - Amount of margin released from position close
3711
3976
  * @param params.transferAll - (Future) Transfer all available balance instead
3712
3977
  * @param params.skipTransfer - (Future) Skip auto-transfer if disabled
3978
+ * @param params.accountContext - Original action context to preserve across
3979
+ * the balance read and transfer.
3713
3980
  * @returns Transfer info if successful, null if skipped/failed
3714
3981
  * @private
3715
3982
  */
3716
3983
  async #autoTransferBackAfterClose(params) {
3717
- const { sourceDex, freedMargin, transferAll = false, skipTransfer = false, } = params;
3984
+ const { sourceDex, freedMargin, transferAll = false, skipTransfer = false, accountContext, } = params;
3718
3985
  // Future: Check user preference to skip auto-transfer
3719
3986
  if (skipTransfer) {
3720
3987
  this.#deps.debugLogger.log('Auto-transfer back skipped (disabled by config)');
@@ -3750,11 +4017,14 @@ export class HyperLiquidProvider {
3750
4017
  to: 'main',
3751
4018
  });
3752
4019
  // Execute transfer back to main DEX (empty string '' represents main DEX)
3753
- const result = await this.transferBetweenDexs({
4020
+ const transferParams = {
3754
4021
  sourceDex,
3755
4022
  destinationDex: '',
3756
4023
  amount: transferAmount.toFixed(USDC_DECIMALS),
3757
- });
4024
+ };
4025
+ const result = accountContext
4026
+ ? await this.#transferBetweenDexsForContext(transferParams, accountContext)
4027
+ : await this.transferBetweenDexs(transferParams);
3758
4028
  if (!result.success) {
3759
4029
  this.#deps.debugLogger.log('❌ Auto-transfer back failed', {
3760
4030
  error: result.error,
@@ -3878,11 +4148,12 @@ export class HyperLiquidProvider {
3878
4148
  * @param params.transferInfo - The transfer information.
3879
4149
  * @param params.transferInfo.amount - The amount value.
3880
4150
  * @param params.transferInfo.sourceDex - The source DEX for the transfer.
4151
+ * @param params.accountContext - Original action context for any rebalance.
3881
4152
  * @returns Whether the balance check and any required transfer succeeded.
3882
4153
  * @private
3883
4154
  */
3884
4155
  async #handleHip3PostOrderRebalance(params) {
3885
- const { dexName, transferInfo } = params;
4156
+ const { accountContext, dexName, transferInfo } = params;
3886
4157
  try {
3887
4158
  const postOrderBalance = await this.#getBalanceForDex({ dex: dexName });
3888
4159
  const transferredAmount = transferInfo.amount;
@@ -3907,11 +4178,11 @@ export class HyperLiquidProvider {
3907
4178
  excessAmount: excessAmount.toFixed(2),
3908
4179
  destinationDex: transferInfo.sourceDex,
3909
4180
  });
3910
- const transferResult = await this.transferBetweenDexs({
4181
+ const transferResult = await this.#transferBetweenDexsForCleanup({
3911
4182
  sourceDex: dexName,
3912
4183
  destinationDex: transferInfo.sourceDex,
3913
4184
  amount: excessAmount.toFixed(USDC_DECIMALS),
3914
- });
4185
+ }, accountContext);
3915
4186
  if (!transferResult.success) {
3916
4187
  // The signer could not sign the transfer: retryable, not a defect.
3917
4188
  if (transferResult.error === PERPS_ERROR_CODES.KEYRING_LOCKED) {
@@ -3971,10 +4242,11 @@ export class HyperLiquidProvider {
3971
4242
  * @param params.transferInfo - The transfer information.
3972
4243
  * @param params.transferInfo.amount - The amount value.
3973
4244
  * @param params.transferInfo.sourceDex - The source DEX for the transfer.
4245
+ * @param params.accountContext - Original action context for the rollback.
3974
4246
  * @private
3975
4247
  */
3976
4248
  async #handleHip3OrderRollback(params) {
3977
- const { dexName, transferInfo } = params;
4249
+ const { accountContext, dexName, transferInfo } = params;
3978
4250
  try {
3979
4251
  this.#deps.debugLogger.log('HyperLiquidProvider: Rolling back failed order transfer', {
3980
4252
  from: dexName,
@@ -3982,11 +4254,11 @@ export class HyperLiquidProvider {
3982
4254
  amount: transferInfo.amount.toFixed(USDC_DECIMALS),
3983
4255
  reason: 'order_failed',
3984
4256
  });
3985
- const rollbackResult = await this.transferBetweenDexs({
4257
+ const rollbackResult = await this.#transferBetweenDexsForContext({
3986
4258
  sourceDex: dexName, // From HIP-3 DEX
3987
4259
  destinationDex: transferInfo.sourceDex, // Back to source
3988
4260
  amount: transferInfo.amount.toFixed(USDC_DECIMALS),
3989
- });
4261
+ }, accountContext);
3990
4262
  if (rollbackResult.success) {
3991
4263
  this.#deps.debugLogger.log('✅ HyperLiquidProvider: Rollback successful', {
3992
4264
  amount: transferInfo.amount.toFixed(USDC_DECIMALS),
@@ -4160,7 +4432,7 @@ export class HyperLiquidProvider {
4160
4432
  * @param params - The operation parameters.
4161
4433
  */
4162
4434
  async #prepareAssetForTrading(params) {
4163
- const { symbol, assetId, leverage } = params;
4435
+ const { symbol, assetId, leverage, accountContext } = params;
4164
4436
  if (!leverage) {
4165
4437
  return;
4166
4438
  }
@@ -4171,14 +4443,21 @@ export class HyperLiquidProvider {
4171
4443
  requestedLeverage: leverage,
4172
4444
  leverageType: marginMode,
4173
4445
  });
4174
- const exchangeClient = this.#clientService.getExchangeClient();
4175
- const leverageResult = await exchangeClient.updateLeverage({
4176
- asset: assetId,
4177
- isCross: marginMode === 'cross',
4178
- leverage,
4179
- });
4180
- if (leverageResult.status !== 'ok') {
4181
- throw new Error(`Failed to update leverage: ${JSON.stringify(leverageResult)}`);
4446
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Leverage update');
4447
+ try {
4448
+ const exchangeClient = this.#getAccountGuardedExchangeClient(accountContext, 'Leverage update dispatch');
4449
+ const leverageResult = await exchangeClient.updateLeverage({
4450
+ asset: assetId,
4451
+ isCross: marginMode === 'cross',
4452
+ leverage,
4453
+ });
4454
+ if (leverageResult.status !== 'ok') {
4455
+ throw new Error(`Failed to update leverage: ${JSON.stringify(leverageResult)}`);
4456
+ }
4457
+ }
4458
+ catch (error) {
4459
+ this.#recordUnsupportedAccountFromError(error, accountContext);
4460
+ throw error;
4182
4461
  }
4183
4462
  this.#deps.debugLogger.log('Leverage updated successfully:', {
4184
4463
  symbol,
@@ -4216,7 +4495,7 @@ export class HyperLiquidProvider {
4216
4495
  * @returns A promise that resolves to the result.
4217
4496
  */
4218
4497
  async #handleHip3PreOrder(params) {
4219
- const { dexName, symbol, orderPrice, positionSize, leverage, isBuy } = params;
4498
+ const { accountContext, dexName, symbol, orderPrice, positionSize, leverage, isBuy, } = params;
4220
4499
  // TAT-3304: Only USDC-collateral HIP-3 DEXs are supported for trading.
4221
4500
  // Following the USDH sunset, reject orders on any non-USDC-collateral
4222
4501
  // DEX here instead of attempting the (now-removed) USDH auto-swap path.
@@ -4254,6 +4533,7 @@ export class HyperLiquidProvider {
4254
4533
  const transferInfo = await this.#autoTransferForHip3Order({
4255
4534
  targetDex: dexName,
4256
4535
  requiredMargin: requiredMarginWithBuffer,
4536
+ accountContext,
4257
4537
  });
4258
4538
  return { transferInfo };
4259
4539
  }
@@ -4275,10 +4555,10 @@ export class HyperLiquidProvider {
4275
4555
  */
4276
4556
  async #submitOrderWithRollback(params) {
4277
4557
  const { orders, grouping, isHip3Order, dexName, transferInfo, symbol } = params;
4278
- const exchangeClient = this.#clientService.getExchangeClient();
4558
+ const exchangeClient = this.#getAccountGuardedExchangeClient(params.accountContext, 'Order submission dispatch');
4279
4559
  const builder = params.chargesMetamaskBuilderFee
4280
4560
  ? await this.#getBuilderOrderContext(params.builderFeeSetupContext ??
4281
- (await this.#ensureBuilderFeeSetup()))
4561
+ (await this.#ensureBuilderFeeSetup(undefined, {}, params.accountContext)))
4282
4562
  : undefined;
4283
4563
  this.#deps.debugLogger.log('Submitting order via asset ID routing', {
4284
4564
  symbol,
@@ -4289,6 +4569,7 @@ export class HyperLiquidProvider {
4289
4569
  isHip3: Boolean(dexName),
4290
4570
  });
4291
4571
  try {
4572
+ await this.#assertNoKnownUnsupportedAccount(params.accountContext, 'Order submission');
4292
4573
  const result = await exchangeClient.order({
4293
4574
  orders: this.#applySubscriptionCloid(orders),
4294
4575
  grouping,
@@ -4298,6 +4579,9 @@ export class HyperLiquidProvider {
4298
4579
  throw new Error(`Order failed: ${JSON.stringify(result)}`);
4299
4580
  }
4300
4581
  const status = result.response?.data?.statuses?.[0];
4582
+ if (hasProperty(status, 'error') && typeof status.error === 'string') {
4583
+ throw createStatusError(status.error, result);
4584
+ }
4301
4585
  // Note: `in` narrows the HyperLiquid SDK discriminated union to the
4302
4586
  // branch that has the property; `hasProperty` types the property as
4303
4587
  // `unknown`, losing downstream access to `.oid`, `.totalSz`, `.avgPx`.
@@ -4307,7 +4591,11 @@ export class HyperLiquidProvider {
4307
4591
  /* eslint-enable no-restricted-syntax */
4308
4592
  // Success - auto-rebalance excess funds
4309
4593
  if (isHip3Order && transferInfo && dexName) {
4310
- await this.#handleHip3PostOrderRebalance({ dexName, transferInfo });
4594
+ await this.#handleHip3PostOrderRebalance({
4595
+ accountContext: params.accountContext,
4596
+ dexName,
4597
+ transferInfo,
4598
+ });
4311
4599
  }
4312
4600
  return {
4313
4601
  success: true,
@@ -4323,9 +4611,14 @@ export class HyperLiquidProvider {
4323
4611
  };
4324
4612
  }
4325
4613
  catch (orderError) {
4614
+ this.#recordUnsupportedAccountFromError(orderError, params.accountContext);
4326
4615
  // Failure - rollback transfer
4327
4616
  if (transferInfo && dexName) {
4328
- await this.#handleHip3OrderRollback({ dexName, transferInfo });
4617
+ await this.#handleHip3OrderRollback({
4618
+ accountContext: params.accountContext,
4619
+ dexName,
4620
+ transferInfo,
4621
+ });
4329
4622
  }
4330
4623
  throw orderError;
4331
4624
  }
@@ -4380,6 +4673,20 @@ export class HyperLiquidProvider {
4380
4673
  * @returns A promise that resolves to the result.
4381
4674
  */
4382
4675
  async placeOrder(input, _retryCount = 0, reportFailure = true) {
4676
+ return await this.#placeOrder(input, _retryCount, reportFailure);
4677
+ }
4678
+ /**
4679
+ * Place an order while preserving one account context across parent actions
4680
+ * and minimum-value retries.
4681
+ *
4682
+ * @param input - Order parameters.
4683
+ * @param _retryCount - Unused. Preserves the public call shape internally.
4684
+ * @param reportFailure - Whether this call owns provider-level reporting.
4685
+ * @param expectedAccountContext - Context captured by a parent action or the
4686
+ * first minimum-value attempt.
4687
+ * @returns The order result.
4688
+ */
4689
+ async #placeOrder(input, _retryCount, reportFailure, expectedAccountContext) {
4383
4690
  const deferProviderErrorReport = input.deferProviderErrorReport === true;
4384
4691
  const params = { ...input };
4385
4692
  delete params.deferProviderErrorReport;
@@ -4390,6 +4697,7 @@ export class HyperLiquidProvider {
4390
4697
  error: PERPS_ERROR_CODES.ORDER_SCALE_PREVIEW_STALE,
4391
4698
  };
4392
4699
  }
4700
+ let actionAccountContext = expectedAccountContext;
4393
4701
  try {
4394
4702
  this.#deps.debugLogger.log('Placing order via HyperLiquid SDK:', params);
4395
4703
  // Basic sync validation (backward compatibility)
@@ -4417,7 +4725,7 @@ export class HyperLiquidProvider {
4417
4725
  // array, the HIP-3 transfer, and the atomic single-order submit, none of
4418
4726
  // which describe what a TWAP, a ladder, or a chase does.
4419
4727
  if (isStrategyOrderType(params.orderType)) {
4420
- return await this.#placeStrategyOrder(params, params.orderType);
4728
+ return await this.#placeStrategyOrder(params, params.orderType, expectedAccountContext);
4421
4729
  }
4422
4730
  // Extract DEX name for API calls (main DEX = null)
4423
4731
  const { dex: dexName } = parseAssetName(params.symbol);
@@ -4466,9 +4774,11 @@ export class HyperLiquidProvider {
4466
4774
  // Kept after validation so invalid orders never trigger signature prompts
4467
4775
  // (builder-fee approval, DEX abstraction enablement, etc.).
4468
4776
  const { chargesMetamaskBuilderFee } = this.#resolveOrderFeePolicy(params);
4469
- const builderFeeSetupContext = await this.#ensureReadyForTrading({
4777
+ const { accountContext, builderFeeSetupContext } = await this.#ensureReadyForTrading({
4470
4778
  requiresBuilderFee: chargesMetamaskBuilderFee,
4779
+ accountContext: actionAccountContext,
4471
4780
  });
4781
+ actionAccountContext = accountContext;
4472
4782
  // Debug: Log asset map state before order placement
4473
4783
  const allMapKeys = Array.from(this.#symbolToAssetId.keys());
4474
4784
  const hip3Keys = allMapKeys.filter((key) => key.includes(':'));
@@ -4526,6 +4836,7 @@ export class HyperLiquidProvider {
4526
4836
  assetId,
4527
4837
  leverage: params.leverage,
4528
4838
  marginMode: params.marginMode,
4839
+ accountContext,
4529
4840
  });
4530
4841
  // 6. Handle HIP-3 balance management (if applicable)
4531
4842
  const isHip3Order = dexName !== null;
@@ -4533,6 +4844,7 @@ export class HyperLiquidProvider {
4533
4844
  if (isHip3Order && dexName) {
4534
4845
  const effectiveLeverage = params.leverage ?? assetInfo.maxLeverage ?? 1;
4535
4846
  const hip3Result = await this.#handleHip3PreOrder({
4847
+ accountContext,
4536
4848
  dexName,
4537
4849
  symbol: params.symbol,
4538
4850
  orderPrice,
@@ -4575,6 +4887,7 @@ export class HyperLiquidProvider {
4575
4887
  symbol: params.symbol,
4576
4888
  assetId,
4577
4889
  chargesMetamaskBuilderFee,
4890
+ accountContext,
4578
4891
  builderFeeSetupContext,
4579
4892
  });
4580
4893
  }
@@ -4607,15 +4920,16 @@ export class HyperLiquidProvider {
4607
4920
  *
4608
4921
  * @param params - Order parameters, already validated.
4609
4922
  * @param orderType - The strategy type, narrowed by the caller.
4923
+ * @param expectedAccountContext - Context captured by a parent action.
4610
4924
  * @returns A promise that resolves to the result.
4611
4925
  */
4612
- async #placeStrategyOrder(params, orderType) {
4926
+ async #placeStrategyOrder(params, orderType, expectedAccountContext) {
4613
4927
  // Captured before the shared preamble, not after it: that preamble awaits
4614
4928
  // asset info, validation, trading readiness and a leverage update, and a
4615
4929
  // disconnect during any of them has to be seen before or after submission.
4616
4930
  const generation = this.#strategyGeneration;
4617
4931
  if (orderType !== 'chase') {
4618
- const context = await this.#prepareStrategyPlacement(params);
4932
+ const context = await this.#prepareStrategyPlacement(params, expectedAccountContext);
4619
4933
  return await this.#submitPreparedStrategy(params, context, async () => {
4620
4934
  if (generation !== this.#strategyGeneration) {
4621
4935
  throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
@@ -4648,7 +4962,7 @@ export class HyperLiquidProvider {
4648
4962
  this.#chasePlacementWaiters.add(placementWaiter);
4649
4963
  try {
4650
4964
  await this.#chaseTickQueue;
4651
- const context = await this.#prepareStrategyPlacement(params);
4965
+ const context = await this.#prepareStrategyPlacement(params, expectedAccountContext);
4652
4966
  return await this.#submitPreparedStrategy(params, context, async () => await this.#startChaseSession(params, context, generation));
4653
4967
  }
4654
4968
  finally {
@@ -4674,24 +4988,36 @@ export class HyperLiquidProvider {
4674
4988
  * @returns The strategy placement result.
4675
4989
  */
4676
4990
  async #submitPreparedStrategy(params, context, submit) {
4677
- const { dexName, network, transferInfo, userAddress } = context;
4991
+ const { accountContext, dexName, network, transferInfo, userAddress } = context;
4678
4992
  let result;
4679
4993
  try {
4680
4994
  result = await submit();
4681
4995
  }
4682
4996
  catch (error) {
4997
+ this.#recordUnsupportedAccountFromError(error, accountContext);
4683
4998
  if (dexName && transferInfo) {
4684
- await this.#handleHip3OrderRollback({ dexName, transferInfo });
4999
+ await this.#handleHip3OrderRollback({
5000
+ accountContext,
5001
+ dexName,
5002
+ transferInfo,
5003
+ });
4685
5004
  }
4686
5005
  throw error;
4687
5006
  }
5007
+ if (!result.success && result.error) {
5008
+ this.#recordUnsupportedAccountFromError(new Error(result.error), accountContext);
5009
+ }
4688
5010
  const resultFilledSize = new BigNumber(result.filledSize ?? 0);
4689
5011
  const hasVenueExposure = result.success === true ||
4690
5012
  result.orderId !== undefined ||
4691
5013
  (result.childOrderIds?.length ?? 0) > 0 ||
4692
5014
  (resultFilledSize.isFinite() && resultFilledSize.gt(0));
4693
5015
  if (dexName && transferInfo && !hasVenueExposure) {
4694
- await this.#handleHip3OrderRollback({ dexName, transferInfo });
5016
+ await this.#handleHip3OrderRollback({
5017
+ accountContext,
5018
+ dexName,
5019
+ transferInfo,
5020
+ });
4695
5021
  return result;
4696
5022
  }
4697
5023
  if (params.orderType === 'twap' && result.orderId) {
@@ -4702,7 +5028,13 @@ export class HyperLiquidProvider {
4702
5028
  }), {
4703
5029
  symbol: params.symbol,
4704
5030
  ...(dexName && transferInfo
4705
- ? { hip3Transfer: { dexName, transferInfo } }
5031
+ ? {
5032
+ hip3Transfer: {
5033
+ accountContext,
5034
+ dexName,
5035
+ transferInfo,
5036
+ },
5037
+ }
4706
5038
  : {}),
4707
5039
  });
4708
5040
  return result;
@@ -4714,7 +5046,11 @@ export class HyperLiquidProvider {
4714
5046
  transferInfo) {
4715
5047
  const session = this.#chaseSessions.get(result.orderId);
4716
5048
  if (session) {
4717
- session.hip3Transfer = { dexName, transferInfo };
5049
+ session.hip3Transfer = {
5050
+ accountContext,
5051
+ dexName,
5052
+ transferInfo,
5053
+ };
4718
5054
  return result;
4719
5055
  }
4720
5056
  }
@@ -4726,7 +5062,11 @@ export class HyperLiquidProvider {
4726
5062
  return result;
4727
5063
  }
4728
5064
  if (dexName && transferInfo) {
4729
- await this.#handleHip3PostOrderRebalance({ dexName, transferInfo });
5065
+ await this.#handleHip3PostOrderRebalance({
5066
+ accountContext,
5067
+ dexName,
5068
+ transferInfo,
5069
+ });
4730
5070
  }
4731
5071
  return result;
4732
5072
  }
@@ -4738,9 +5078,10 @@ export class HyperLiquidProvider {
4738
5078
  * so a strategy order is held to the same rules as an ordinary one.
4739
5079
  *
4740
5080
  * @param params - Order parameters.
5081
+ * @param expectedAccountContext - Context captured by a parent action.
4741
5082
  * @returns The asset and sizing context the strategy handlers submit with.
4742
5083
  */
4743
- async #prepareStrategyPlacement(params) {
5084
+ async #prepareStrategyPlacement(params, expectedAccountContext) {
4744
5085
  const { dex: dexName } = parseAssetName(params.symbol);
4745
5086
  const { assetInfo, currentPrice, meta } = await this.#getAssetInfo({
4746
5087
  symbol: params.symbol,
@@ -4803,9 +5144,16 @@ export class HyperLiquidProvider {
4803
5144
  // Kept after validation so an invalid strategy order never triggers the
4804
5145
  // signature prompts in trading setup — same ordering as `placeOrder`.
4805
5146
  const { chargesMetamaskBuilderFee } = this.#resolveOrderFeePolicy(params);
4806
- const builderFeeSetupContext = chargesMetamaskBuilderFee
4807
- ? await this.#ensureReadyForTrading({ requiresBuilderFee: true })
4808
- : await this.#ensureReadyForTrading({ requiresBuilderFee: false });
5147
+ const tradingActionContext = chargesMetamaskBuilderFee
5148
+ ? await this.#ensureReadyForTrading({
5149
+ requiresBuilderFee: true,
5150
+ accountContext: expectedAccountContext,
5151
+ })
5152
+ : await this.#ensureReadyForTrading({
5153
+ requiresBuilderFee: false,
5154
+ accountContext: expectedAccountContext,
5155
+ });
5156
+ const { accountContext, builderFeeSetupContext } = tradingActionContext;
4809
5157
  const assetId = await this.#getAssetIdWithRepair({
4810
5158
  symbol: params.symbol,
4811
5159
  dexName,
@@ -4816,19 +5164,19 @@ export class HyperLiquidProvider {
4816
5164
  assetId,
4817
5165
  leverage: params.leverage,
4818
5166
  marginMode: params.marginMode,
5167
+ accountContext,
4819
5168
  });
4820
5169
  const builder = builderFeeSetupContext
4821
5170
  ? await this.#getBuilderOrderContext(builderFeeSetupContext)
4822
5171
  : undefined;
4823
- const network = this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet';
4824
- const userAddress = builderFeeSetupContext?.userAddress ??
4825
- (await this.#walletService.getUserAddressWithDefault());
5172
+ const { network, userAddress } = accountContext;
4826
5173
  let transferInfo = null;
4827
5174
  if (dexName) {
4828
5175
  const orderPrice = ladder === undefined
4829
5176
  ? effectivePrice
4830
5177
  : Math.max(...ladder.prices.map(Number.parseFloat));
4831
5178
  const hip3Result = await this.#handleHip3PreOrder({
5179
+ accountContext,
4832
5180
  dexName,
4833
5181
  symbol: params.symbol,
4834
5182
  orderPrice,
@@ -4840,6 +5188,7 @@ export class HyperLiquidProvider {
4840
5188
  transferInfo = hip3Result.transferInfo;
4841
5189
  }
4842
5190
  return {
5191
+ accountContext,
4843
5192
  assetId,
4844
5193
  szDecimals: assetInfo.szDecimals,
4845
5194
  formattedSize,
@@ -4927,7 +5276,7 @@ export class HyperLiquidProvider {
4927
5276
  * @returns A promise that resolves to the result.
4928
5277
  */
4929
5278
  async #placeTwapOrder(params, context, generation) {
4930
- const { assetId, formattedSize } = context;
5279
+ const { accountContext, assetId, formattedSize } = context;
4931
5280
  const durationMinutes = params.twapDuration;
4932
5281
  if (durationMinutes === undefined) {
4933
5282
  throw new Error(PERPS_ERROR_CODES.ORDER_TWAP_DURATION_REQUIRED);
@@ -4945,6 +5294,7 @@ export class HyperLiquidProvider {
4945
5294
  durationMinutes,
4946
5295
  randomize: params.twapRandomize ?? false,
4947
5296
  });
5297
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'TWAP order submission');
4948
5298
  const result = await exchangeClient.twapOrder({
4949
5299
  twap: {
4950
5300
  a: assetId,
@@ -4974,6 +5324,7 @@ export class HyperLiquidProvider {
4974
5324
  if (generation !== this.#strategyGeneration) {
4975
5325
  let remainsLive = true;
4976
5326
  try {
5327
+ await this.#assertRollbackAccountWritable(accountContext);
4977
5328
  const cancelResult = await exchangeClient.twapCancel({
4978
5329
  a: assetId,
4979
5330
  t: running.twapId,
@@ -5021,7 +5372,7 @@ export class HyperLiquidProvider {
5021
5372
  * @returns A promise that resolves to the result.
5022
5373
  */
5023
5374
  async #placeScaleOrder(params, context, generation) {
5024
- const { assetId, formattedSize, ladder, builder } = context;
5375
+ const { accountContext, assetId, formattedSize, ladder, builder } = context;
5025
5376
  // Built and validated in `#prepareStrategyPlacement`, before anything was
5026
5377
  // signed, so what is submitted here is exactly what the minimums were
5027
5378
  // applied to.
@@ -5062,6 +5413,7 @@ export class HyperLiquidProvider {
5062
5413
  let result;
5063
5414
  let thrownBulkOrderError;
5064
5415
  try {
5416
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Scale order submission');
5065
5417
  result = await exchangeClient.order({
5066
5418
  orders,
5067
5419
  grouping: 'na',
@@ -5078,6 +5430,7 @@ export class HyperLiquidProvider {
5078
5430
  }
5079
5431
  const rawStatuses = result.response?.data?.statuses;
5080
5432
  const statuses = Array.isArray(rawStatuses) ? rawStatuses : [];
5433
+ const isMultiSigRequired = this.#recordUnsupportedAccountFromStatuses(statuses, result, accountContext);
5081
5434
  const outcomes = Array.from({ length: count }, (_unused, index) => parseScaleBulkOrderStatus(statuses[index]));
5082
5435
  const acceptedCount = outcomes.filter((outcome) => outcome?.kind === 'accepted').length;
5083
5436
  const everyStatusClassified = outcomes.every((outcome) => outcome !== undefined);
@@ -5130,6 +5483,19 @@ export class HyperLiquidProvider {
5130
5483
  ...acceptedResult,
5131
5484
  });
5132
5485
  const cancelNonRejectedOrders = async () => {
5486
+ try {
5487
+ await this.#assertRollbackAccountWritable(accountContext);
5488
+ }
5489
+ catch (error) {
5490
+ this.#deps.debugLogger.log('Scale placement rollback not signed after its account context changed', {
5491
+ error: ensureError(error, 'HyperLiquidProvider.placeScaleOrder.rollback').message,
5492
+ groupId,
5493
+ });
5494
+ return {
5495
+ orderIds: restingChildOrderIds,
5496
+ clientOrderIds: cleanupClientOrderIds,
5497
+ };
5498
+ }
5133
5499
  const [orderIds, pendingClientOrderIds] = await Promise.all([
5134
5500
  this.#cancelOrderRequests(exchangeClient, restingChildOrderIds.map((orderId) => ({
5135
5501
  a: assetId,
@@ -5219,7 +5585,9 @@ export class HyperLiquidProvider {
5219
5585
  childOrderIds: [],
5220
5586
  });
5221
5587
  }
5222
- throw new Error(PERPS_ERROR_CODES.ORDER_REJECTED);
5588
+ throw new Error(isMultiSigRequired
5589
+ ? PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED
5590
+ : PERPS_ERROR_CODES.ORDER_REJECTED);
5223
5591
  }
5224
5592
  this.#scaleOrderGroups.set(groupId, {
5225
5593
  symbol: params.symbol,
@@ -5240,7 +5608,7 @@ export class HyperLiquidProvider {
5240
5608
  * @returns A promise that resolves to the result.
5241
5609
  */
5242
5610
  async #startChaseSession(params, context, generation) {
5243
- const { assetId, szDecimals, formattedSize, builder } = context;
5611
+ const { accountContext, assetId, szDecimals, formattedSize, builder } = context;
5244
5612
  // Captured now, alongside the builder fee and for the same reason: the fee
5245
5613
  // resolution behind it is cleared when the caller's `placeOrder` returns,
5246
5614
  // which for a chase is before any replacement runs.
@@ -5288,6 +5656,7 @@ export class HyperLiquidProvider {
5288
5656
  reduceOnly: params.reduceOnly ?? false,
5289
5657
  builder,
5290
5658
  marksSubscriptionCloid,
5659
+ accountContext,
5291
5660
  exchangeClient: placingClient,
5292
5661
  });
5293
5662
  break;
@@ -5321,6 +5690,7 @@ export class HyperLiquidProvider {
5321
5690
  const intervalMs = params.chaseIntervalMs ?? CHASE_ORDER_CONFIG.DefaultIntervalMs;
5322
5691
  const sessionId = generatePerpsId('chase');
5323
5692
  const session = {
5693
+ accountContext,
5324
5694
  symbol: params.symbol,
5325
5695
  assetId,
5326
5696
  isBuy: params.isBuy,
@@ -5357,10 +5727,12 @@ export class HyperLiquidProvider {
5357
5727
  // the provider stays pointed at the account that placed it — a disconnect
5358
5728
  // is usually followed by an account or network switch, after which handing
5359
5729
  // the id back is no remedy at all. So it is cancelled here, through the
5360
- // client that signed it — the account switch cannot reach that instance —
5361
- // and the failure is then true of the venue as well as of this provider.
5362
- // Best-effort still: the transport underneath that client may already be
5363
- // closing, so a cancel that does not land falls back to reporting the id.
5730
+ // client that signed it. The held client remains usable after teardown,
5731
+ // but its adapter resolves the selected account again for each signature,
5732
+ // so rollback validates the captured identity before cancellation. The
5733
+ // failure is then true of the venue as well as of this provider.
5734
+ // Best-effort still: a context change or closing transport falls back to
5735
+ // reporting the live id.
5364
5736
  if (generation !== this.#strategyGeneration) {
5365
5737
  this.#deps.debugLogger.log('Chase placement outlived its provider', {
5366
5738
  orderId,
@@ -5455,9 +5827,12 @@ export class HyperLiquidProvider {
5455
5827
  * than looked up here so a first placement can keep the instance it signed
5456
5828
  * with, which is the only one that can take the order back once `disconnect`
5457
5829
  * has dropped the service's reference.
5830
+ * @param params.accountContext - Account/network/lifecycle captured by the
5831
+ * action that owns the chase.
5458
5832
  * @returns The resting order's exchange ID.
5459
5833
  */
5460
5834
  async #restChaseOrder(params) {
5835
+ await this.#assertNoKnownUnsupportedAccount(params.accountContext, 'Chase order submission');
5461
5836
  const result = await params.exchangeClient.order({
5462
5837
  orders: this.#applySubscriptionCloid([
5463
5838
  {
@@ -5498,6 +5873,15 @@ export class HyperLiquidProvider {
5498
5873
  this.#chaseTickQueue = this.#chaseTickQueue
5499
5874
  .then(() => this.#runChaseTick(sessionId))
5500
5875
  .catch(async (error) => {
5876
+ const failedSession = this.#chaseSessions.get(sessionId);
5877
+ const accountUnsupported = failedSession
5878
+ ? this.#recordUnsupportedAccountFromError(error, failedSession.accountContext)
5879
+ : false;
5880
+ if (accountUnsupported) {
5881
+ this.#stopChaseSession(sessionId);
5882
+ this.#deps.debugLogger.log('Chase stopped after account support correction', { sessionId });
5883
+ return;
5884
+ }
5501
5885
  // A rejected agent is dropped so the next tick asks for another.
5502
5886
  await this.#evictRejectedAgent(error);
5503
5887
  // Resolve the shared queue after every failure. Otherwise one
@@ -5812,8 +6196,7 @@ export class HyperLiquidProvider {
5812
6196
  reduceOnly: session.reduceOnly,
5813
6197
  builder: session.builder,
5814
6198
  marksSubscriptionCloid: session.marksSubscriptionCloid,
5815
- // A running session is on a live provider, so the current client is
5816
- // the right one; only the first placement has a teardown to survive.
6199
+ accountContext: session.accountContext,
5817
6200
  exchangeClient: this.#clientService.getExchangeClient(),
5818
6201
  });
5819
6202
  session.size = remaining;
@@ -6037,6 +6420,8 @@ export class HyperLiquidProvider {
6037
6420
  * @param placingClient - Client to cancel through. Omitted by every cancel on
6038
6421
  * a live provider, which wants the current one; an abandoned placement passes
6039
6422
  * the client it signed with, the only one still able to reach its order.
6423
+ * That path separately validates the captured rollback account because the
6424
+ * client's wallet adapter resolves the selected account for every signature.
6040
6425
  * @returns Whether the order was cancelled, was already gone, or still rests.
6041
6426
  */
6042
6427
  async #cancelChaseChild(session, placingClient) {
@@ -6045,6 +6430,12 @@ export class HyperLiquidProvider {
6045
6430
  }
6046
6431
  // Looked up only once there is something to cancel, so a session with
6047
6432
  // nothing resting still answers on a provider whose client is already gone.
6433
+ if (placingClient) {
6434
+ await this.#assertRollbackAccountWritable(session.accountContext);
6435
+ }
6436
+ else {
6437
+ await this.#assertNoKnownUnsupportedAccount(session.accountContext, 'Chase cancellation');
6438
+ }
6048
6439
  const exchangeClient = placingClient ?? this.#clientService.getExchangeClient();
6049
6440
  let result;
6050
6441
  try {
@@ -6053,6 +6444,7 @@ export class HyperLiquidProvider {
6053
6444
  });
6054
6445
  }
6055
6446
  catch (error) {
6447
+ this.#recordUnsupportedAccountFromError(error, session.accountContext);
6056
6448
  const message = ensureError(error, 'HyperLiquidProvider.cancelChaseChild').message.toLowerCase();
6057
6449
  if (ALREADY_GONE_CANCEL_MARKERS.some((marker) => message.includes(marker))) {
6058
6450
  return CancelChildOutcome.Gone;
@@ -6060,6 +6452,9 @@ export class HyperLiquidProvider {
6060
6452
  throw error;
6061
6453
  }
6062
6454
  const status = result.response?.data?.statuses?.[0];
6455
+ if (isStatusObject(status) && typeof status.error === 'string') {
6456
+ this.#recordUnsupportedAccountFromError(createStatusError(status.error, result), session.accountContext);
6457
+ }
6063
6458
  const signerFailure = await this.#classifyStatusSignerFailure([status], result);
6064
6459
  if (signerFailure) {
6065
6460
  throw signerFailure;
@@ -6258,7 +6653,16 @@ export class HyperLiquidProvider {
6258
6653
  }
6259
6654
  let { rebalancePromise } = trackedOrder;
6260
6655
  if (!rebalancePromise) {
6261
- rebalancePromise = this.#handleHip3PostOrderRebalance(trackedOrder.hip3Transfer);
6656
+ const { hip3Transfer } = trackedOrder;
6657
+ rebalancePromise = this.#handleHip3PostOrderRebalance({
6658
+ ...hip3Transfer,
6659
+ accountContext: {
6660
+ ...hip3Transfer.accountContext,
6661
+ // TWAP tracking deliberately survives reconnects. Keep its stable
6662
+ // account/network scope, but bind the cleanup write to this lifecycle.
6663
+ lifecycleGeneration: this.#lifecycleGeneration,
6664
+ },
6665
+ });
6262
6666
  trackedOrder.rebalancePromise = rebalancePromise;
6263
6667
  }
6264
6668
  try {
@@ -6511,9 +6915,10 @@ export class HyperLiquidProvider {
6511
6915
  if (!/^\d+$/u.test(params.orderId) || !Number.isSafeInteger(twapId)) {
6512
6916
  throw new Error(PERPS_ERROR_CODES.ORDER_STRATEGY_HANDLE_UNKNOWN);
6513
6917
  }
6514
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
6515
- const network = this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet';
6516
- const userAddress = await this.#walletService.getUserAddressWithDefault();
6918
+ const { accountContext } = await this.#ensureReadyForTrading({
6919
+ requiresBuilderFee: false,
6920
+ });
6921
+ const { network, userAddress } = accountContext;
6517
6922
  const trackingKey = getTwapOrderScopeKey({
6518
6923
  network,
6519
6924
  userAddress,
@@ -6549,10 +6954,18 @@ export class HyperLiquidProvider {
6549
6954
  symbol: params.symbol,
6550
6955
  dexName: parseAssetName(params.symbol).dex,
6551
6956
  });
6552
- const result = await this.#clientService.getExchangeClient().twapCancel({
6553
- a: assetId,
6554
- t: twapId,
6555
- });
6957
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'TWAP cancellation');
6958
+ let result;
6959
+ try {
6960
+ result = await this.#clientService.getExchangeClient().twapCancel({
6961
+ a: assetId,
6962
+ t: twapId,
6963
+ });
6964
+ }
6965
+ catch (error) {
6966
+ this.#recordUnsupportedAccountFromError(error, accountContext);
6967
+ throw error;
6968
+ }
6556
6969
  const status = result.response?.data?.status;
6557
6970
  const cancelOutcome = classifyCancelStatus(status);
6558
6971
  if (cancelOutcome === CancelChildOutcome.Gone && !ownershipAuthenticated) {
@@ -6565,7 +6978,7 @@ export class HyperLiquidProvider {
6565
6978
  const rawError = isStatusObject(status) && typeof status.error === 'string'
6566
6979
  ? status.error
6567
6980
  : 'TWAP cancellation failed';
6568
- return createErrorResult(await this.#mapStatusError(rawError, result), {
6981
+ return createErrorResult(await this.#mapStatusError(rawError, result, accountContext), {
6569
6982
  success: false,
6570
6983
  orderId: params.orderId,
6571
6984
  });
@@ -6581,7 +6994,9 @@ export class HyperLiquidProvider {
6581
6994
  if (!group) {
6582
6995
  throw new Error(PERPS_ERROR_CODES.ORDER_STRATEGY_HANDLE_UNKNOWN);
6583
6996
  }
6584
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
6997
+ const { accountContext } = await this.#ensureReadyForTrading({
6998
+ requiresBuilderFee: false,
6999
+ });
6585
7000
  const assetId = await this.#getAssetIdWithRepair({
6586
7001
  symbol: group.symbol,
6587
7002
  dexName: parseAssetName(group.symbol).dex,
@@ -6595,9 +7010,14 @@ export class HyperLiquidProvider {
6595
7010
  asset: assetId,
6596
7011
  cloid: clientOrderId,
6597
7012
  }));
7013
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Scale cancellation');
6598
7014
  const [orderCancellation, cloidCancellation] = await Promise.all([
6599
- this.#cancelOrderRequestBatch(exchangeClient, cancelRequests),
6600
- this.#cancelOrderCloidRequestBatch(exchangeClient, cancelByCloidRequests),
7015
+ this.#cancelOrderRequestBatch(exchangeClient, cancelRequests, (error) => {
7016
+ this.#recordUnsupportedAccountFromError(error, accountContext);
7017
+ }),
7018
+ this.#cancelOrderCloidRequestBatch(exchangeClient, cancelByCloidRequests, (error) => {
7019
+ this.#recordUnsupportedAccountFromError(error, accountContext);
7020
+ }),
6601
7021
  ]);
6602
7022
  const remaining = orderCancellation.remainingOrderIds.map(String);
6603
7023
  const { remainingClientOrderIds } = cloidCancellation;
@@ -6697,7 +7117,18 @@ export class HyperLiquidProvider {
6697
7117
  }
6698
7118
  return { success: true, orderId: params.orderId };
6699
7119
  }
6700
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
7120
+ this.#assertProviderLifecycleCurrent(session.accountContext.lifecycleGeneration, 'Chase cancellation setup');
7121
+ const currentAddress = await this.#walletService.getUserAddressWithDefault();
7122
+ if (currentAddress.toLowerCase() !==
7123
+ session.accountContext.userAddress.toLowerCase() ||
7124
+ (this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet') !==
7125
+ session.accountContext.network) {
7126
+ throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
7127
+ }
7128
+ await this.#ensureReadyForTrading({
7129
+ requiresBuilderFee: false,
7130
+ accountContext: session.accountContext,
7131
+ });
6701
7132
  // Only a refusal leaves an order behind. A child that had already filled or
6702
7133
  // been cancelled is reported as a rejection too, but nothing of it is
6703
7134
  // resting — treating that as a failure would pin the handle open forever on
@@ -6741,11 +7172,11 @@ export class HyperLiquidProvider {
6741
7172
  *
6742
7173
  * @param exchangeClient - Client that owns the orders.
6743
7174
  * @param requests - Venue cancel-by-CLOID requests.
7175
+ * @param onError - Optional operation-specific failure classifier.
6744
7176
  * @returns Client order IDs that may still be resting.
6745
7177
  */
6746
- async #cancelOrderCloidRequests(exchangeClient, requests) {
6747
- return (await this.#cancelOrderCloidRequestBatch(exchangeClient, requests))
6748
- .remainingClientOrderIds;
7178
+ async #cancelOrderCloidRequests(exchangeClient, requests, onError) {
7179
+ return (await this.#cancelOrderCloidRequestBatch(exchangeClient, requests, onError)).remainingClientOrderIds;
6749
7180
  }
6750
7181
  /**
6751
7182
  * Cancel pending orders by client order ID, reporting a signer that could
@@ -6753,14 +7184,20 @@ export class HyperLiquidProvider {
6753
7184
  *
6754
7185
  * @param exchangeClient - Client that owns the orders.
6755
7186
  * @param requests - Venue cancel-by-CLOID requests.
7187
+ * @param onError - Optional operation-specific failure classifier.
6756
7188
  * @returns Client order IDs that may still be resting, and the signer
6757
7189
  * failure when the signer could not sign the request.
6758
7190
  */
6759
- async #cancelOrderCloidRequestBatch(exchangeClient, requests) {
7191
+ async #cancelOrderCloidRequestBatch(exchangeClient, requests, onError) {
6760
7192
  if (requests.length === 0) {
6761
7193
  return { remainingClientOrderIds: [] };
6762
7194
  }
6763
7195
  const classifyStatuses = async (statuses, answer) => {
7196
+ for (const status of statuses) {
7197
+ if (isStatusObject(status) && typeof status.error === 'string') {
7198
+ await onError?.(createStatusError(status.error, answer));
7199
+ }
7200
+ }
6764
7201
  // A signer failure is classified (and reported) once for the request;
6765
7202
  // entries the venue cancelled are not resting either way.
6766
7203
  const signerFailure = await this.#classifyStatusSignerFailure(statuses, answer);
@@ -6784,6 +7221,7 @@ export class HyperLiquidProvider {
6784
7221
  return await classifyStatuses(statuses, result);
6785
7222
  }
6786
7223
  catch (error) {
7224
+ await onError?.(error);
6787
7225
  // The SDK throws when any entry failed, with every entry's status.
6788
7226
  const statuses = getCancelStatusesFromError(error, requests.length);
6789
7227
  if (statuses) {
@@ -6824,6 +7262,11 @@ export class HyperLiquidProvider {
6824
7262
  };
6825
7263
  }
6826
7264
  const classifyStatuses = async (statuses, answer) => {
7265
+ for (const status of statuses) {
7266
+ if (isStatusObject(status) && typeof status.error === 'string') {
7267
+ await onError?.(createStatusError(status.error, answer));
7268
+ }
7269
+ }
6827
7270
  // A signer failure is classified (and reported) once for the request;
6828
7271
  // entries the venue cancelled still count as cancelled.
6829
7272
  const signerFailure = await this.#classifyStatusSignerFailure(statuses, answer);
@@ -6969,9 +7412,11 @@ export class HyperLiquidProvider {
6969
7412
  * Apply both signer invalidation and account-shape classification to writes.
6970
7413
  *
6971
7414
  * @param error - Failed protection write.
7415
+ * @param accountContext - Account and network that authorized the write.
6972
7416
  * @returns Actionable error retaining the venue message when unmapped.
6973
7417
  */
6974
- async #classifyProtectionWriteError(error) {
7418
+ async #classifyProtectionWriteError(error, accountContext) {
7419
+ this.#recordUnsupportedAccountFromError(error, accountContext);
6975
7420
  return ((await this.#classifySignerFailure(error)) ??
6976
7421
  (isHyperLiquidMultiSigRequiredError(error)
6977
7422
  ? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
@@ -7378,19 +7823,30 @@ export class HyperLiquidProvider {
7378
7823
  // Every refusal is behind us, so shared trading readiness can run now.
7379
7824
  // HyperLiquid's modify action has no builder field and must not request a
7380
7825
  // builder-fee approval.
7381
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
7826
+ const { accountContext } = await this.#ensureReadyForTrading({
7827
+ requiresBuilderFee: false,
7828
+ });
7382
7829
  // Submit modification via SDK. The cloid is deliberately left unmarked:
7383
7830
  // `modify` carries no builder field, as the readiness call above records,
7384
7831
  // so no MetaMask fee is charged on this action and marking it would tell
7385
7832
  // the fill fan-out a reduction applied to an order that paid nothing.
7386
7833
  // The replacement inherits the resting order's own attribution.
7834
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Order modification');
7387
7835
  const exchangeClient = this.#clientService.getExchangeClient();
7388
- const result = await exchangeClient.modify({
7389
- oid: typeof params.orderId === 'string' ? params.orderId : params.orderId,
7390
- order: newOrder,
7391
- });
7392
- if (result.status !== 'ok') {
7393
- throw new Error(`Order modification failed: ${JSON.stringify(result)}`);
7836
+ try {
7837
+ const result = await exchangeClient.modify({
7838
+ oid: typeof params.orderId === 'string'
7839
+ ? params.orderId
7840
+ : params.orderId,
7841
+ order: newOrder,
7842
+ });
7843
+ if (result.status !== 'ok') {
7844
+ throw new Error(`Order modification failed: ${JSON.stringify(result)}`);
7845
+ }
7846
+ }
7847
+ catch (error) {
7848
+ this.#recordUnsupportedAccountFromError(error, accountContext);
7849
+ throw error;
7394
7850
  }
7395
7851
  // `params.orderId` is the order that was just REPLACED, so returning it
7396
7852
  // as OrderResult.orderId (documented as the exchange order ID) names an
@@ -7450,6 +7906,7 @@ export class HyperLiquidProvider {
7450
7906
  // current child instead of leaving a replacement chasing behind the user.
7451
7907
  const owningChase = [...this.#chaseSessions.entries()].find(([, session]) => session.orderId === params.orderId ||
7452
7908
  session.replacingOrderId === params.orderId);
7909
+ let accountContext;
7453
7910
  try {
7454
7911
  if (owningChase) {
7455
7912
  this.#stopChaseSession(owningChase[0]);
@@ -7472,12 +7929,15 @@ export class HyperLiquidProvider {
7472
7929
  if (!coinValidation.isValid) {
7473
7930
  throw new Error(coinValidation.error);
7474
7931
  }
7475
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
7476
- const exchangeClient = this.#clientService.getExchangeClient();
7932
+ ({ accountContext } = await this.#ensureReadyForTrading({
7933
+ requiresBuilderFee: false,
7934
+ }));
7477
7935
  const asset = await this.#getAssetIdWithRepair({
7478
7936
  symbol: params.symbol,
7479
7937
  dexName: parseAssetName(params.symbol).dex,
7480
7938
  });
7939
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Order cancellation');
7940
+ const exchangeClient = this.#clientService.getExchangeClient();
7481
7941
  const result = await exchangeClient.cancel({
7482
7942
  cancels: [
7483
7943
  {
@@ -7500,12 +7960,15 @@ export class HyperLiquidProvider {
7500
7960
  const rawError = isStatusObject(status) && typeof status.error === 'string'
7501
7961
  ? status.error
7502
7962
  : 'Order cancellation failed';
7503
- return createErrorResult(await this.#mapStatusError(rawError, result), {
7963
+ return createErrorResult(await this.#mapStatusError(rawError, result, accountContext), {
7504
7964
  success: false,
7505
7965
  orderId: params.orderId,
7506
7966
  });
7507
7967
  }
7508
7968
  catch (error) {
7969
+ if (accountContext) {
7970
+ this.#recordUnsupportedAccountFromError(error, accountContext);
7971
+ }
7509
7972
  const mappedError = await this.#reportWriteError(error, 'cancelOrder', {
7510
7973
  orderId: params.orderId,
7511
7974
  coin: params.symbol,
@@ -7577,8 +8040,9 @@ export class HyperLiquidProvider {
7577
8040
  if (ordinaryOrders.length > 0) {
7578
8041
  // Cancellation carries no builder context, so it must not prompt for a
7579
8042
  // fee approval that the action cannot use.
7580
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
7581
- const exchangeClient = this.#clientService.getExchangeClient();
8043
+ const { accountContext } = await this.#ensureReadyForTrading({
8044
+ requiresBuilderFee: false,
8045
+ });
7582
8046
  const cancelRequests = await Promise.all(ordinaryOrders.map(async ({ order }) => {
7583
8047
  const asset = await this.#getAssetIdWithRepair({
7584
8048
  symbol: order.symbol,
@@ -7589,6 +8053,8 @@ export class HyperLiquidProvider {
7589
8053
  o: parseInt(order.orderId, 10),
7590
8054
  };
7591
8055
  }));
8056
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch order cancellation');
8057
+ const exchangeClient = this.#clientService.getExchangeClient();
7592
8058
  let statuses;
7593
8059
  let answer;
7594
8060
  try {
@@ -7608,6 +8074,7 @@ export class HyperLiquidProvider {
7608
8074
  answer = getVenueAnswer(error);
7609
8075
  statuses = getCancelStatusesFromError(error, ordinaryOrders.length);
7610
8076
  if (!statuses) {
8077
+ this.#recordUnsupportedAccountFromError(error, accountContext);
7611
8078
  throw error;
7612
8079
  }
7613
8080
  }
@@ -7623,6 +8090,7 @@ export class HyperLiquidProvider {
7623
8090
  : undefined;
7624
8091
  let error = PERPS_ERROR_CODES.BATCH_CANCEL_FAILED;
7625
8092
  if (statusError) {
8093
+ this.#recordUnsupportedAccountFromError(statusError, accountContext);
7626
8094
  error =
7627
8095
  signerFailure && (await this.#isSignerFailure(statusError))
7628
8096
  ? signerFailure.message
@@ -7666,11 +8134,14 @@ export class HyperLiquidProvider {
7666
8134
  async closePositions(params) {
7667
8135
  // Declare outside try block so it's accessible in catch block
7668
8136
  let positionsToClose = [];
8137
+ let accountContext;
7669
8138
  const unavailableResults = [];
7670
8139
  try {
7671
8140
  // Batch preparation needs trading readiness but not builder approval yet.
7672
8141
  // The provider-owned market policy is resolved from the positions below.
7673
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
8142
+ ({ accountContext } = await this.#ensureReadyForTrading({
8143
+ requiresBuilderFee: false,
8144
+ }));
7674
8145
  // Selected symbols only need the DEXes they belong to. Loading every
7675
8146
  // market group would refuse a BTC close when an unrelated HIP-3 DEX
7676
8147
  // cannot be read. closeAll / empty symbols still need a complete
@@ -7741,8 +8212,6 @@ export class HyperLiquidProvider {
7741
8212
  results: unavailableResults,
7742
8213
  };
7743
8214
  }
7744
- // Get exchange client for order submission
7745
- const exchangeClient = this.#clientService.getExchangeClient();
7746
8215
  // Pre-fetch meta for all unique DEXs to avoid N API calls in loop
7747
8216
  const uniqueDexs = [
7748
8217
  ...new Set(positionsToClose.map((pos) => parseAssetName(pos.symbol).dex ?? 'main')),
@@ -7857,10 +8326,16 @@ export class HyperLiquidProvider {
7857
8326
  // policy charges, preserving deterministic behavior for future policies
7858
8327
  // that vary by market or route.
7859
8328
  const chargesMetamaskBuilderFee = feePolicyContexts.some((context) => this.#resolveOrderFeePolicy(context).chargesMetamaskBuilderFee);
7860
- const builder = chargesMetamaskBuilderFee
7861
- ? await this.#getBuilderOrderContext(await this.#ensureReadyForTrading({ requiresBuilderFee: true }))
7862
- : undefined;
8329
+ let builder;
8330
+ if (chargesMetamaskBuilderFee) {
8331
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close setup');
8332
+ const builderFeeSetupContext = await this.#ensureBuilderFeeSetup(undefined, {}, accountContext);
8333
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close setup');
8334
+ builder = await this.#getBuilderOrderContext(builderFeeSetupContext);
8335
+ }
7863
8336
  // Single batch API call
8337
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close submission');
8338
+ const exchangeClient = this.#clientService.getExchangeClient();
7864
8339
  const result = await exchangeClient.order({
7865
8340
  orders: this.#applySubscriptionCloid(orders),
7866
8341
  grouping: 'na',
@@ -7868,6 +8343,11 @@ export class HyperLiquidProvider {
7868
8343
  });
7869
8344
  // Parse response statuses (one per order)
7870
8345
  const { statuses } = result.response.data;
8346
+ for (const status of statuses) {
8347
+ if (hasProperty(status, 'error') && typeof status.error === 'string') {
8348
+ this.#recordUnsupportedAccountFromError(createStatusError(status.error, result), accountContext);
8349
+ }
8350
+ }
7871
8351
  const successCount = statuses.filter((stat) => isStatusObject(stat) &&
7872
8352
  (hasProperty(stat, 'filled') || hasProperty(stat, 'resting'))).length;
7873
8353
  const failureCount = statuses.length -
@@ -7886,6 +8366,7 @@ export class HyperLiquidProvider {
7886
8366
  this.#deps.debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: orderedPositions[i].symbol, freedMargin });
7887
8367
  // Non-blocking: Transfer freed margin back to main DEX
7888
8368
  await this.#autoTransferBackAfterClose({
8369
+ accountContext,
7889
8370
  sourceDex,
7890
8371
  freedMargin,
7891
8372
  });
@@ -7921,6 +8402,9 @@ export class HyperLiquidProvider {
7921
8402
  };
7922
8403
  }
7923
8404
  catch (error) {
8405
+ if (accountContext) {
8406
+ this.#recordUnsupportedAccountFromError(error, accountContext);
8407
+ }
7924
8408
  const signerFailure = await this.#handleSignerFailure(error, 'closePositions', {
7925
8409
  positionCount: positionsToClose.length,
7926
8410
  });
@@ -8004,6 +8488,11 @@ export class HyperLiquidProvider {
8004
8488
  const lifecycle = this.#lifecycleGeneration;
8005
8489
  const isTestnet = this.#clientService.isTestnetMode();
8006
8490
  const userAddress = await this.#walletService.getUserAddressWithDefault();
8491
+ const accountContext = {
8492
+ lifecycleGeneration: lifecycle,
8493
+ network: isTestnet ? 'testnet' : 'mainnet',
8494
+ userAddress,
8495
+ };
8007
8496
  const assertScope = async () => {
8008
8497
  const current = await this.#walletService.getUserAddressWithDefault();
8009
8498
  this.#assertProviderLifecycleCurrent(lifecycle, 'updatePositionTPSL');
@@ -8399,12 +8888,18 @@ export class HyperLiquidProvider {
8399
8888
  const requiresBuilderFee = replacementChargesMetamaskBuilderFee || rollbackRequiresBuilderFee;
8400
8889
  // Approval and builder-context resolution both finish before the
8401
8890
  // pre-cancel. A failure here therefore leaves the old protection intact.
8402
- const builderFeeSetupContext = requiresBuilderFee
8891
+ await assertScope();
8892
+ const tradingActionContext = requiresBuilderFee
8403
8893
  ? await this.#ensureReadyForTrading({
8404
8894
  requiresBuilderFee: true,
8405
8895
  builderFeeApprovalFailureCode: PERPS_ERROR_CODES.TPSL_UPDATE_FAILED,
8896
+ accountContext,
8406
8897
  })
8407
- : await this.#ensureReadyForTrading({ requiresBuilderFee: false });
8898
+ : await this.#ensureReadyForTrading({
8899
+ requiresBuilderFee: false,
8900
+ accountContext,
8901
+ });
8902
+ const { builderFeeSetupContext } = tradingActionContext;
8408
8903
  const builderOrderContext = builderFeeSetupContext
8409
8904
  ? await this.#getBuilderOrderContext(builderFeeSetupContext)
8410
8905
  : undefined;
@@ -8449,9 +8944,10 @@ export class HyperLiquidProvider {
8449
8944
  builderOrderContext && { builder: builderOrderContext }),
8450
8945
  });
8451
8946
  statuses = result.response?.data?.statuses ?? [];
8947
+ this.#recordUnsupportedAccountFromStatuses(statuses, result, accountContext);
8452
8948
  }
8453
8949
  catch (error) {
8454
- const failure = await this.#classifyProtectionWriteError(error);
8950
+ const failure = await this.#classifyProtectionWriteError(error, accountContext);
8455
8951
  restorationFailure = failure;
8456
8952
  statuses =
8457
8953
  this.#readProtectionErrorStatuses(error) ??
@@ -8505,10 +9001,10 @@ export class HyperLiquidProvider {
8505
9001
  await assertCurrentPosition();
8506
9002
  const oldCancellation = await this.#cancelOrderRequestBatch(exchangeClient, cancelRequests, async (error) => {
8507
9003
  if (cancelGuardFailure !== undefined) {
8508
- throw await this.#classifyProtectionWriteError(cancelGuardFailure);
9004
+ throw await this.#classifyProtectionWriteError(cancelGuardFailure, accountContext);
8509
9005
  }
8510
9006
  if (isHyperLiquidMultiSigRequiredError(error)) {
8511
- throw await this.#classifyProtectionWriteError(error);
9007
+ throw await this.#classifyProtectionWriteError(error, accountContext);
8512
9008
  }
8513
9009
  });
8514
9010
  // The signer could not sign the cancel. With nothing cancelled the old
@@ -8606,11 +9102,12 @@ export class HyperLiquidProvider {
8606
9102
  builderOrderContext && { builder: builderOrderContext }),
8607
9103
  });
8608
9104
  placementStatuses = result.response?.data?.statuses ?? [];
9105
+ this.#recordUnsupportedAccountFromStatuses(placementStatuses, result, accountContext);
8609
9106
  responseAccepted = result.status === 'ok';
8610
9107
  }
8611
9108
  catch (error) {
8612
9109
  // Drop and report a rejected agent before restoration signs.
8613
- placementFailure = await this.#classifyProtectionWriteError(error);
9110
+ placementFailure = await this.#classifyProtectionWriteError(error, accountContext);
8614
9111
  await assertScope();
8615
9112
  const statuses = this.#readProtectionErrorStatuses(error);
8616
9113
  if (statuses !== undefined) {
@@ -8726,7 +9223,7 @@ export class HyperLiquidProvider {
8726
9223
  error.message === PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)) {
8727
9224
  // Old protection has already changed. Classification must not
8728
9225
  // throw away this batch's still-live replacement identities.
8729
- cleanupFailure = await this.#classifyProtectionWriteError(error);
9226
+ cleanupFailure = await this.#classifyProtectionWriteError(error, accountContext);
8730
9227
  }
8731
9228
  });
8732
9229
  await assertScope();
@@ -8804,7 +9301,9 @@ export class HyperLiquidProvider {
8804
9301
  this.#deps.debugLogger.log('Closing position:', params);
8805
9302
  // The delegated placeOrder call resolves the builder-fee policy for the
8806
9303
  // concrete close order after validation.
8807
- await this.#ensureReadyForTrading({ requiresBuilderFee: false });
9304
+ const { accountContext } = await this.#ensureReadyForTrading({
9305
+ requiresBuilderFee: false,
9306
+ });
8808
9307
  // A caller snapshot is never authoritative for a reduce-only close. Use
8809
9308
  // the current DEX slice or its HTTP fallback, and fail closed if neither
8810
9309
  // answers. This keeps the size and side on one freshness contract across
@@ -8883,7 +9382,8 @@ export class HyperLiquidProvider {
8883
9382
  // position.
8884
9383
  const isFullClose = closeSizeNum >= absPositionSize;
8885
9384
  // Execute position close with consistent slippage handling
8886
- const result = await this.placeOrder({
9385
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Position close');
9386
+ const result = await this.#placeOrder({
8887
9387
  symbol: params.symbol,
8888
9388
  isBuy,
8889
9389
  size: closeSize,
@@ -8903,7 +9403,7 @@ export class HyperLiquidProvider {
8903
9403
  usdAmount: isFullClose ? undefined : params.usdAmount,
8904
9404
  priceAtCalculation: params.priceAtCalculation,
8905
9405
  maxSlippageBps: params.maxSlippageBps,
8906
- }, 0, false);
9406
+ }, 0, false, accountContext);
8907
9407
  // Return freed margin using native abstraction or programmatic transfer
8908
9408
  if (result.success &&
8909
9409
  isHip3Position &&
@@ -8912,6 +9412,7 @@ export class HyperLiquidProvider {
8912
9412
  this.#deps.debugLogger.log('Position closed successfully, initiating manual auto-transfer back');
8913
9413
  // Non-blocking: Transfer freed margin back to main DEX
8914
9414
  await this.#autoTransferBackAfterClose({
9415
+ accountContext,
8915
9416
  sourceDex: hip3Dex,
8916
9417
  freedMargin,
8917
9418
  });
@@ -8954,11 +9455,13 @@ export class HyperLiquidProvider {
8954
9455
  * - ntli: Amount in micro-units (amount * 1e6)
8955
9456
  */
8956
9457
  async updateMargin(params) {
9458
+ let accountContext;
8957
9459
  try {
8958
9460
  this.#deps.debugLogger.log('Updating position margin:', params);
8959
9461
  const { symbol, amount } = params;
8960
9462
  // Ensure provider is ready
8961
9463
  await this.#ensureReady();
9464
+ accountContext = await this.#assertAccountSupported();
8962
9465
  // Use the target DEX's current slice or one targeted HTTP read before
8963
9466
  // deriving the position side.
8964
9467
  const positions = await this.#getPositionsForOperation(parseAssetName(symbol).dex ?? '');
@@ -8994,6 +9497,7 @@ export class HyperLiquidProvider {
8994
9497
  }
8995
9498
  }
8996
9499
  // Call SDK to update isolated margin
9500
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Margin update');
8997
9501
  const exchangeClient = this.#clientService.getExchangeClient();
8998
9502
  const result = await exchangeClient.updateIsolatedMargin({
8999
9503
  asset: assetId,
@@ -9009,13 +9513,18 @@ export class HyperLiquidProvider {
9009
9513
  };
9010
9514
  }
9011
9515
  catch (error) {
9516
+ const isMultiSigRequired = accountContext
9517
+ ? this.#recordUnsupportedAccountFromError(error, accountContext)
9518
+ : false;
9012
9519
  const signerFailure = await this.#handleSignerFailure(error, 'updateMargin', {
9013
9520
  symbol: params.symbol,
9014
9521
  });
9015
9522
  if (signerFailure) {
9016
9523
  return { success: false, error: signerFailure.message };
9017
9524
  }
9018
- const safeError = ensureError(error, 'HyperLiquidProvider.updateMargin');
9525
+ const safeError = isMultiSigRequired
9526
+ ? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
9527
+ : ensureError(error, 'HyperLiquidProvider.updateMargin');
9019
9528
  this.#deps.logger.error(safeError, this.#getErrorContext('updateMargin', {
9020
9529
  symbol: params.symbol,
9021
9530
  amount: params.amount,
@@ -9904,6 +10413,22 @@ export class HyperLiquidProvider {
9904
10413
  return [];
9905
10414
  }
9906
10415
  }
10416
+ /**
10417
+ * Return whether the active wallet can submit Hyperliquid actions.
10418
+ *
10419
+ * @returns The provider-owned account support result.
10420
+ */
10421
+ async getAccountSupport() {
10422
+ await this.#ensureReady();
10423
+ const context = {
10424
+ lifecycleGeneration: this.#lifecycleGeneration,
10425
+ network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
10426
+ userAddress: await this.#walletService.getUserAddressWithDefault(),
10427
+ };
10428
+ const support = await this.#getAccountSupportForContext(context);
10429
+ await this.#assertAccountContextCurrent(context, 'Account support read');
10430
+ return support;
10431
+ }
9907
10432
  /**
9908
10433
  * Resolve the provider's currently active CAIP account identifier.
9909
10434
  * Used by the MarketDataService REST coalesce layer so cached payloads
@@ -10943,6 +11468,7 @@ export class HyperLiquidProvider {
10943
11468
  * @returns Result with txHash (HyperLiquid internal) and withdrawal ID
10944
11469
  */
10945
11470
  async withdraw(params) {
11471
+ let accountContext;
10946
11472
  try {
10947
11473
  this.#deps.debugLogger.log('HyperLiquidProvider: STARTING WITHDRAWAL', {
10948
11474
  params,
@@ -10995,6 +11521,16 @@ export class HyperLiquidProvider {
10995
11521
  this.#deps.debugLogger.log('HyperLiquidProvider: ASSET SUPPORTED', {
10996
11522
  assetId: params.assetId,
10997
11523
  });
11524
+ const lifecycleGeneration = this.#lifecycleGeneration;
11525
+ const network = this.#clientService.isTestnetMode()
11526
+ ? 'testnet'
11527
+ : 'mainnet';
11528
+ const userAddress = await this.#walletService.getUserAddressWithDefault();
11529
+ accountContext = {
11530
+ lifecycleGeneration,
11531
+ network,
11532
+ userAddress,
11533
+ };
10998
11534
  // Step 3: Determine destination address
10999
11535
  this.#deps.debugLogger.log('HyperLiquidProvider: DETERMINING DESTINATION ADDRESS');
11000
11536
  let destination;
@@ -11005,7 +11541,7 @@ export class HyperLiquidProvider {
11005
11541
  });
11006
11542
  }
11007
11543
  else {
11008
- destination = await this.#walletService.getUserAddressWithDefault();
11544
+ destination = userAddress;
11009
11545
  this.#deps.debugLogger.log('HyperLiquidProvider: USING USER WALLET ADDRESS', {
11010
11546
  destination,
11011
11547
  });
@@ -11013,8 +11549,13 @@ export class HyperLiquidProvider {
11013
11549
  // Step 4: Ensure client is ready
11014
11550
  this.#deps.debugLogger.log('HyperLiquidProvider: ENSURING CLIENT READY');
11015
11551
  await this.#ensureReady();
11016
- await this.#ensureUnifiedAccountEnabled({ allowUserSigning: true });
11017
- const exchangeClient = this.#clientService.getExchangeClient();
11552
+ accountContext = await this.#assertAccountSupported(accountContext);
11553
+ await this.#ensureUnifiedAccountEnabled({
11554
+ allowUserSigning: true,
11555
+ accountContext,
11556
+ });
11557
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Withdrawal');
11558
+ const exchangeClient = this.#getAccountGuardedExchangeClient(accountContext, 'Withdrawal dispatch');
11018
11559
  this.#deps.debugLogger.log('HyperLiquidProvider: CLIENT READY');
11019
11560
  // Step 5: Validate amount against account balance
11020
11561
  this.#deps.debugLogger.log('HyperLiquidProvider: CHECKING ACCOUNT BALANCE');
@@ -11063,6 +11604,7 @@ export class HyperLiquidProvider {
11063
11604
  endpoint: 'withdraw3',
11064
11605
  timestamp: new Date().toISOString(),
11065
11606
  });
11607
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Withdrawal');
11066
11608
  const result = await exchangeClient.withdraw3({
11067
11609
  destination,
11068
11610
  amount: params.amount,
@@ -11089,7 +11631,11 @@ export class HyperLiquidProvider {
11089
11631
  // HyperLiquid's withdraw3 API doesn't return a transaction hash immediately
11090
11632
  };
11091
11633
  }
11092
- const errorMessage = `Withdrawal failed: ${String(result.status)}`;
11634
+ const rejection = new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
11635
+ const isMultiSigRequired = this.#recordUnsupportedAccountFromError(rejection, accountContext);
11636
+ const errorMessage = isMultiSigRequired
11637
+ ? PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED
11638
+ : `Withdrawal failed: ${String(result.status)}`;
11093
11639
  this.#deps.debugLogger.log('HyperLiquidProvider: WITHDRAWAL FAILED', {
11094
11640
  error: errorMessage,
11095
11641
  status: result.status,
@@ -11102,13 +11648,18 @@ export class HyperLiquidProvider {
11102
11648
  };
11103
11649
  }
11104
11650
  catch (error) {
11651
+ const isMultiSigRequired = accountContext
11652
+ ? this.#recordUnsupportedAccountFromError(error, accountContext)
11653
+ : false;
11105
11654
  const signerFailure = await this.#handleSignerFailure(error, 'withdraw', {
11106
11655
  assetId: params.assetId,
11107
11656
  });
11108
11657
  if (signerFailure) {
11109
11658
  return createErrorResult(signerFailure, { success: false });
11110
11659
  }
11111
- const safeError = ensureError(error, 'HyperLiquidProvider.initiateWithdrawal');
11660
+ const safeError = isMultiSigRequired
11661
+ ? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
11662
+ : ensureError(error, 'HyperLiquidProvider.initiateWithdrawal');
11112
11663
  this.#deps.debugLogger.log('HyperLiquidProvider: WITHDRAWAL EXCEPTION', {
11113
11664
  error: safeError.message,
11114
11665
  errorType: safeError.name,
@@ -11124,7 +11675,7 @@ export class HyperLiquidProvider {
11124
11675
  operation: PERPS_ERROR_OPERATION.FinancialOperations,
11125
11676
  action: PERPS_ERROR_ACTION.FinancialWithdrawal,
11126
11677
  }));
11127
- return createErrorResult(error, { success: false });
11678
+ return createErrorResult(safeError, { success: false });
11128
11679
  }
11129
11680
  }
11130
11681
  /**
@@ -11147,6 +11698,19 @@ export class HyperLiquidProvider {
11147
11698
  * });
11148
11699
  */
11149
11700
  async transferBetweenDexs(params) {
11701
+ return await this.#transferBetweenDexs(params);
11702
+ }
11703
+ /**
11704
+ * Transfer collateral with an optional context captured by a parent action.
11705
+ *
11706
+ * @param params - Transfer parameters.
11707
+ * @param expectedContext - Context captured by the parent action.
11708
+ * @param allowStaleLifecycle - Whether a compensating cleanup may run after
11709
+ * provider teardown while the address and network remain unchanged.
11710
+ * @returns The transfer result.
11711
+ */
11712
+ async #transferBetweenDexs(params, expectedContext, allowStaleLifecycle = false) {
11713
+ let accountContext;
11150
11714
  try {
11151
11715
  this.#deps.debugLogger.log('HyperLiquidProvider: STARTING DEX TRANSFER', {
11152
11716
  params,
@@ -11159,14 +11723,23 @@ export class HyperLiquidProvider {
11159
11723
  if (params.sourceDex === params.destinationDex) {
11160
11724
  throw new Error('Source and destination DEX must be different');
11161
11725
  }
11162
- // Get user address
11163
- const userAddress = await this.#walletService.getUserAddressWithDefault();
11726
+ if (allowStaleLifecycle) {
11727
+ if (!expectedContext) {
11728
+ throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
11729
+ }
11730
+ accountContext = expectedContext;
11731
+ await this.#assertRollbackAccountWritable(accountContext);
11732
+ }
11733
+ else {
11734
+ // Ensure client ready
11735
+ await this.#ensureReady();
11736
+ accountContext = await this.#assertAccountSupported(expectedContext);
11737
+ }
11738
+ const transferAccountContext = accountContext;
11739
+ const { userAddress } = accountContext;
11164
11740
  this.#deps.debugLogger.log('HyperLiquidProvider: USER ADDRESS', {
11165
11741
  userAddress,
11166
11742
  });
11167
- // Ensure client ready
11168
- await this.#ensureReady();
11169
- const exchangeClient = this.#clientService.getExchangeClient();
11170
11743
  // Execute transfer using SDK sendAsset()
11171
11744
  // Note: SDK docs say "testnet-only" but it works on mainnet (verified via Phantom)
11172
11745
  this.#deps.debugLogger.log('HyperLiquidProvider: CALLING SEND_ASSET API', {
@@ -11174,11 +11747,21 @@ export class HyperLiquidProvider {
11174
11747
  destinationDex: params.destinationDex || '(main)',
11175
11748
  amount: params.amount,
11176
11749
  });
11750
+ const token = await this.#getUsdcTokenId();
11751
+ if (allowStaleLifecycle) {
11752
+ await this.#assertRollbackAccountWritable(accountContext);
11753
+ }
11754
+ else {
11755
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'DEX transfer');
11756
+ }
11757
+ const exchangeClient = this.#clientService.getExchangeClient(allowStaleLifecycle
11758
+ ? async () => await this.#assertRollbackAccountWritable(transferAccountContext)
11759
+ : async () => await this.#assertNoKnownUnsupportedAccount(transferAccountContext, 'DEX transfer dispatch'));
11177
11760
  const result = await exchangeClient.sendAsset({
11178
11761
  destination: userAddress,
11179
11762
  sourceDex: params.sourceDex,
11180
11763
  destinationDex: params.destinationDex,
11181
- token: await this.#getUsdcTokenId(), // Query correct USDC token ID dynamically
11764
+ token,
11182
11765
  amount: params.amount,
11183
11766
  });
11184
11767
  this.#deps.debugLogger.log('HyperLiquidProvider: SEND_ASSET RESPONSE', {
@@ -11193,14 +11776,22 @@ export class HyperLiquidProvider {
11193
11776
  // User can verify transfer in explorer by timestamp
11194
11777
  };
11195
11778
  }
11779
+ if (this.#recordUnsupportedAccountFromError(new Error(`DEX transfer failed: ${JSON.stringify(result)}`), accountContext)) {
11780
+ throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
11781
+ }
11196
11782
  throw new Error(PERPS_ERROR_CODES.TRANSFER_FAILED);
11197
11783
  }
11198
11784
  catch (error) {
11785
+ const isMultiSigRequired = accountContext
11786
+ ? this.#recordUnsupportedAccountFromError(error, accountContext)
11787
+ : false;
11199
11788
  const signerFailure = await this.#handleSignerFailure(error, 'transferBetweenDexs', { sourceDex: params.sourceDex, destinationDex: params.destinationDex });
11200
11789
  if (signerFailure) {
11201
11790
  return { success: false, error: signerFailure.message };
11202
11791
  }
11203
- const safeError = ensureError(error, 'HyperLiquidProvider.transferToSpot');
11792
+ const safeError = isMultiSigRequired
11793
+ ? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
11794
+ : ensureError(error, 'HyperLiquidProvider.transferToSpot');
11204
11795
  this.#deps.debugLogger.log('❌ HyperLiquidProvider: TRANSFER FAILED', {
11205
11796
  error: safeError.message,
11206
11797
  params,
@@ -11212,6 +11803,27 @@ export class HyperLiquidProvider {
11212
11803
  };
11213
11804
  }
11214
11805
  }
11806
+ /**
11807
+ * Transfer collateral using a context captured by a parent action.
11808
+ *
11809
+ * @param params - Transfer parameters.
11810
+ * @param accountContext - Original action context for the transfer.
11811
+ * @returns The transfer result.
11812
+ */
11813
+ async #transferBetweenDexsForContext(params, accountContext) {
11814
+ return await this.#transferBetweenDexs(params, accountContext);
11815
+ }
11816
+ /**
11817
+ * Return collateral for a completed action after teardown, provided the
11818
+ * selected address and network still own the captured context.
11819
+ *
11820
+ * @param params - Transfer parameters.
11821
+ * @param accountContext - Original action context for the cleanup.
11822
+ * @returns The transfer result.
11823
+ */
11824
+ async #transferBetweenDexsForCleanup(params, accountContext) {
11825
+ return await this.#transferBetweenDexs(params, accountContext, true);
11826
+ }
11215
11827
  /**
11216
11828
  * Subscribe to live price updates
11217
11829
  *
@@ -11494,6 +12106,11 @@ export class HyperLiquidProvider {
11494
12106
  try {
11495
12107
  const lifecycleGeneration = this.#lifecycleGeneration;
11496
12108
  const userAddress = await this.#walletService.getUserAddressWithDefault();
12109
+ const preparationContext = {
12110
+ lifecycleGeneration,
12111
+ network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
12112
+ userAddress,
12113
+ };
11497
12114
  // The result is only for the provider and account it started with.
11498
12115
  const assertPreparationCurrent = async () => {
11499
12116
  this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Trading wallet preparation');
@@ -11505,13 +12122,13 @@ export class HyperLiquidProvider {
11505
12122
  throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
11506
12123
  }
11507
12124
  };
11508
- await this.#ensureReadyForTrading({
12125
+ const { accountContext } = await this.#ensureReadyForTrading({
11509
12126
  requiresBuilderFee: false,
11510
12127
  recheckPendingReferral: true,
12128
+ accountContext: preparationContext,
11511
12129
  });
11512
- const network = this.#clientService.isTestnetMode()
11513
- ? 'testnet'
11514
- : 'mainnet';
12130
+ await assertPreparationCurrent();
12131
+ const { network } = accountContext;
11515
12132
  const isRegistered = await this.#isWalletOnHyperliquid(userAddress, network);
11516
12133
  await assertPreparationCurrent();
11517
12134
  // The venue rejects every write from a wallet with no HyperLiquid account
@@ -11527,7 +12144,7 @@ export class HyperLiquidProvider {
11527
12144
  await this.#ensureBuilderFeeSetup(undefined, {
11528
12145
  reportSignerFailure: true,
11529
12146
  reportRefusal: true,
11530
- });
12147
+ }, accountContext);
11531
12148
  // The builder fee setup ends quietly when the provider disconnects.
11532
12149
  await assertPreparationCurrent();
11533
12150
  if (!this.#walletService.isMainAccountSignerReady()) {
@@ -12171,6 +12788,7 @@ export class HyperLiquidProvider {
12171
12788
  // Clear session caches. Capability and fee reads use the lifecycle
12172
12789
  // generation above to discard any old response that resolves later.
12173
12790
  this.clearFeeCache();
12791
+ this.#accountSupportByContext.clear();
12174
12792
  this.#referralCheckCache.clear();
12175
12793
  this.#builderFeeCheckCache.clear();
12176
12794
  this.#builderFeeRefusals.clear();
@@ -12416,20 +13034,26 @@ export class HyperLiquidProvider {
12416
13034
  *
12417
13035
  * Note: This is network-specific - testnet and mainnet have separate referral states
12418
13036
  * Note: Non-blocking - failures are logged to Sentry but don't prevent trading
13037
+ *
13038
+ * @param accountContext - Account/network/lifecycle captured by the action
13039
+ * that is allowed to authorize a referral write.
12419
13040
  */
12420
- async #ensureReferralSet() {
13041
+ async #ensureReferralSet(accountContext) {
12421
13042
  this.#referralSetupNeedsRetry = false;
12422
13043
  this.#referralAwaitsBuilderCode = false;
12423
- const isTestnet = this.#clientService.isTestnetMode();
12424
- const network = isTestnet ? 'testnet' : 'mainnet';
13044
+ const network = accountContext?.network ??
13045
+ (this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
13046
+ const isTestnet = network === 'testnet';
12425
13047
  const expectedReferralCode = this.#getReferralCode(isTestnet);
12426
13048
  const referrerAddress = this.#getBuilderAddress(isTestnet);
12427
- let userAddress;
12428
- try {
12429
- userAddress = await this.#walletService.getUserAddressWithDefault();
12430
- }
12431
- catch {
12432
- return; // Can't proceed without address
13049
+ let userAddress = accountContext?.userAddress;
13050
+ if (!userAddress) {
13051
+ try {
13052
+ userAddress = await this.#walletService.getUserAddressWithDefault();
13053
+ }
13054
+ catch {
13055
+ return; // Can't proceed without address
13056
+ }
12433
13057
  }
12434
13058
  if (userAddress.toLowerCase() === referrerAddress.toLowerCase()) {
12435
13059
  this.#deps.debugLogger.log('[ensureReferralSet] User is builder, skipping', { network });
@@ -12471,6 +13095,9 @@ export class HyperLiquidProvider {
12471
13095
  return;
12472
13096
  }
12473
13097
  const codeStatus = await this.#getReferralCodeStatus();
13098
+ if (accountContext) {
13099
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral code lookup');
13100
+ }
12474
13101
  if (codeStatus !== 'ready') {
12475
13102
  this.#deps.debugLogger.log('[ensureReferralSet] Builder referral not ready, skipping', { network, codeStatus });
12476
13103
  // Don't cache. A failed lookup (already logged) waits for the next
@@ -12480,7 +13107,10 @@ export class HyperLiquidProvider {
12480
13107
  return;
12481
13108
  }
12482
13109
  // Check if user already has a referral on-chain
12483
- const hasReferral = await this.#checkReferralSet();
13110
+ const hasReferral = await this.#checkReferralSet(userAddress);
13111
+ if (accountContext) {
13112
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral account lookup');
13113
+ }
12484
13114
  if (hasReferral) {
12485
13115
  // Already has referral on-chain
12486
13116
  PerpsSigningCache.setReferral(network, userAddress, {
@@ -12491,7 +13121,7 @@ export class HyperLiquidProvider {
12491
13121
  }
12492
13122
  else {
12493
13123
  this.#deps.debugLogger.log('[ensureReferralSet] Setting referral (will show signing request)', { network, referralCode: expectedReferralCode });
12494
- const result = await this.#setReferralCode();
13124
+ const result = await this.#setReferralCode(accountContext);
12495
13125
  if (result) {
12496
13126
  this.#deps.debugLogger.log('[ensureReferralSet] Referral set successfully', { network });
12497
13127
  PerpsSigningCache.setReferral(network, userAddress, {
@@ -12519,6 +13149,11 @@ export class HyperLiquidProvider {
12519
13149
  completeInFlight();
12520
13150
  return;
12521
13151
  }
13152
+ if (accountContext &&
13153
+ this.#recordUnsupportedAccountFromError(error, accountContext)) {
13154
+ completeInFlight();
13155
+ return;
13156
+ }
12522
13157
  // Safety net: wallet looked registered but the SDK still rejects with
12523
13158
  // "User or API Wallet 0x... does not exist." Do not forward to Sentry.
12524
13159
  // The walletRegistered cache stores positive observations only, so no
@@ -12528,6 +13163,12 @@ export class HyperLiquidProvider {
12528
13163
  completeInFlight();
12529
13164
  return;
12530
13165
  }
13166
+ if (accountContext &&
13167
+ ensureError(error, 'HyperLiquidProvider.ensureReferralSet').message ===
13168
+ PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
13169
+ completeInFlight();
13170
+ throw error;
13171
+ }
12531
13172
  // Cache failure to prevent retries
12532
13173
  PerpsSigningCache.setReferral(network, userAddress, {
12533
13174
  attempted: true,
@@ -12585,18 +13226,20 @@ export class HyperLiquidProvider {
12585
13226
  /**
12586
13227
  * Check if user has a referral code set with HyperLiquid
12587
13228
  *
13229
+ * @param userAddress - Account to inspect, or the currently selected account
13230
+ * when omitted.
12588
13231
  * @returns Promise resolving to true if referral is set, false otherwise
12589
13232
  */
12590
- async #checkReferralSet() {
13233
+ async #checkReferralSet(userAddress) {
12591
13234
  try {
12592
13235
  const infoClient = this.#clientService.getInfoClient();
12593
- const userAddress = await this.#walletService.getUserAddressWithDefault();
13236
+ const referralUserAddress = userAddress ?? (await this.#walletService.getUserAddressWithDefault());
12594
13237
  // Call HyperLiquid API to check if user has a referral set
12595
13238
  const referralData = await infoClient.referral({
12596
- user: userAddress,
13239
+ user: referralUserAddress,
12597
13240
  });
12598
13241
  this.#deps.debugLogger.log('Referral check result:', {
12599
- userAddress,
13242
+ userAddress: referralUserAddress,
12600
13243
  referralData,
12601
13244
  });
12602
13245
  return Boolean(referralData?.referredBy?.code);
@@ -12620,11 +13263,18 @@ export class HyperLiquidProvider {
12620
13263
  /**
12621
13264
  * Set MetaMask as the user's referrer on HyperLiquid
12622
13265
  *
13266
+ * @param accountContext - Account/network/lifecycle captured by the action
13267
+ * that is allowed to authorize this write.
12623
13268
  * @returns A promise that resolves to the boolean result.
12624
13269
  */
12625
- async #setReferralCode() {
13270
+ async #setReferralCode(accountContext) {
12626
13271
  try {
12627
- const exchangeClient = this.#clientService.getExchangeClient();
13272
+ if (accountContext) {
13273
+ await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral setup');
13274
+ }
13275
+ const exchangeClient = accountContext
13276
+ ? this.#getAccountGuardedExchangeClient(accountContext, 'Referral setup dispatch')
13277
+ : this.#clientService.getExchangeClient();
12628
13278
  const referralCode = this.#getReferralCode(this.#clientService.isTestnetMode());
12629
13279
  this.#deps.debugLogger.log('[setReferralCode] Setting referral code', {
12630
13280
  code: referralCode,
@@ -12635,9 +13285,18 @@ export class HyperLiquidProvider {
12635
13285
  code: referralCode,
12636
13286
  });
12637
13287
  this.#deps.debugLogger.log('[setReferralCode] Referral code set result', result);
13288
+ if (result?.status !== 'ok' &&
13289
+ accountContext &&
13290
+ this.#recordUnsupportedAccountFromError(new Error(`Referral setup failed: ${JSON.stringify(result)}`), accountContext)) {
13291
+ return false;
13292
+ }
12638
13293
  return result?.status === 'ok';
12639
13294
  }
12640
13295
  catch (error) {
13296
+ if (accountContext &&
13297
+ this.#recordUnsupportedAccountFromError(error, accountContext)) {
13298
+ throw error;
13299
+ }
12641
13300
  // Retryable (a locked keyring, an unavailable agent, or one the venue
12642
13301
  // rejected): `#ensureReferralSet` retries at the next entry, so it is
12643
13302
  // not an error to report.