@metamask-previews/perps-controller 22.0.0-preview-d0145b59e → 22.0.0-preview-896e0b6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/dist/PerpsController-method-action-types.d.ts +13 -1
- package/dist/PerpsController-method-action-types.d.ts.map +1 -1
- package/dist/PerpsController-method-action-types.js.map +1 -1
- package/dist/PerpsController.d.ts +10 -1
- package/dist/PerpsController.d.ts.map +1 -1
- package/dist/PerpsController.js +18 -0
- package/dist/PerpsController.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.d.ts +9 -1
- package/dist/providers/AggregatedPerpsProvider.d.ts.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.js +13 -0
- package/dist/providers/AggregatedPerpsProvider.js.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.ts +7 -1
- package/dist/providers/HyperLiquidProvider.d.ts.map +1 -1
- package/dist/providers/HyperLiquidProvider.js +759 -159
- package/dist/providers/HyperLiquidProvider.js.map +1 -1
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 `
|
|
1740
|
-
*
|
|
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
|
|
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
|
|
1753
|
+
async #isHyperliquidMultiSigAccount(userAddress, infoClient) {
|
|
1750
1754
|
try {
|
|
1751
|
-
const
|
|
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,165 @@ 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
|
+
* Stop an action before signing when Hyperliquid reports a native multi-sig
|
|
1906
|
+
* signer set for the active account.
|
|
1907
|
+
*
|
|
1908
|
+
* @param expectedContext - Existing operation context to validate, or omit
|
|
1909
|
+
* to capture the currently selected account and network.
|
|
1910
|
+
* @returns The account context whose support was checked.
|
|
1911
|
+
*/
|
|
1912
|
+
async #assertAccountSupported(expectedContext) {
|
|
1913
|
+
const context = expectedContext ??
|
|
1914
|
+
{
|
|
1915
|
+
lifecycleGeneration: this.#lifecycleGeneration,
|
|
1916
|
+
network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
|
|
1917
|
+
userAddress: await this.#walletService.getUserAddressWithDefault(),
|
|
1918
|
+
};
|
|
1919
|
+
const support = await this.#getAccountSupportForContext(context);
|
|
1920
|
+
await this.#assertAccountContextCurrent(context, 'Account support check');
|
|
1921
|
+
if (!support.isSupported) {
|
|
1922
|
+
throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
|
|
1923
|
+
}
|
|
1924
|
+
return context;
|
|
1763
1925
|
}
|
|
1764
1926
|
/**
|
|
1765
1927
|
* Attempt to enable HyperLiquid Unified Account mode for HIP-3 orders
|
|
@@ -1772,6 +1934,8 @@ export class HyperLiquidProvider {
|
|
|
1772
1934
|
*
|
|
1773
1935
|
* @param options - Optional configuration.
|
|
1774
1936
|
* @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.
|
|
1937
|
+
* @param options.accountContext - Account/network/lifecycle captured by the
|
|
1938
|
+
* action that is allowed to authorize the migration.
|
|
1775
1939
|
* @private
|
|
1776
1940
|
*/
|
|
1777
1941
|
async #ensureUnifiedAccountEnabled(options) {
|
|
@@ -1790,8 +1954,11 @@ export class HyperLiquidProvider {
|
|
|
1790
1954
|
if (!this.#useUnifiedAccount) {
|
|
1791
1955
|
return; // Feature disabled
|
|
1792
1956
|
}
|
|
1793
|
-
const
|
|
1794
|
-
const
|
|
1957
|
+
const { accountContext } = options ?? {};
|
|
1958
|
+
const userAddress = accountContext?.userAddress ??
|
|
1959
|
+
(await this.#walletService.getUserAddressWithDefault());
|
|
1960
|
+
const network = accountContext?.network ??
|
|
1961
|
+
(this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
|
|
1795
1962
|
// Check global cache first to avoid repeated signing requests
|
|
1796
1963
|
// This is CRITICAL for hardware wallets to prevent repeated signing prompts
|
|
1797
1964
|
// while browsing.
|
|
@@ -1859,6 +2026,9 @@ export class HyperLiquidProvider {
|
|
|
1859
2026
|
currentMode = await infoClient.userAbstraction({
|
|
1860
2027
|
user: userAddress,
|
|
1861
2028
|
});
|
|
2029
|
+
if (accountContext) {
|
|
2030
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account mode lookup');
|
|
2031
|
+
}
|
|
1862
2032
|
if (currentMode === 'unifiedAccount' ||
|
|
1863
2033
|
currentMode === 'portfolioMargin') {
|
|
1864
2034
|
// portfolioMargin is a superset of unifiedAccount — it already supports
|
|
@@ -1902,17 +2072,21 @@ export class HyperLiquidProvider {
|
|
|
1902
2072
|
// surfaced on the Perps tab on every entry (TAT-3214). Probe right
|
|
1903
2073
|
// before the write so accounts that never reach one (already compatible,
|
|
1904
2074
|
// deferred, unknown mode) do not pay the extra round trip.
|
|
1905
|
-
const
|
|
1906
|
-
|
|
2075
|
+
const accountSupport = await this.#getAccountSupportForContext({
|
|
2076
|
+
userAddress,
|
|
2077
|
+
network,
|
|
2078
|
+
infoClient,
|
|
2079
|
+
});
|
|
2080
|
+
if (!accountSupport.isSupported) {
|
|
1907
2081
|
this.#deps.debugLogger.log('[ensureUnifiedAccountEnabled] Multi-sig account, skipping unified account migration', { user: userAddress, network, mode: currentMode });
|
|
1908
2082
|
this.#deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.AccountSetup, {
|
|
1909
2083
|
[PERPS_EVENT_PROPERTY.PREVIOUS_ABSTRACTION_MODE]: currentMode,
|
|
1910
2084
|
[PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.NOT_APPLICABLE,
|
|
1911
2085
|
[PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: 'multi_sig_account',
|
|
1912
2086
|
});
|
|
1913
|
-
// Final state: the
|
|
1914
|
-
// it as attempted with unified mode off.
|
|
1915
|
-
//
|
|
2087
|
+
// Final state: the migration can never succeed for this account, so
|
|
2088
|
+
// cache it as attempted with unified mode off. Action readiness blocks
|
|
2089
|
+
// this account before any later exchange write.
|
|
1916
2090
|
TradingReadinessCache.set(network, userAddress, {
|
|
1917
2091
|
attempted: true,
|
|
1918
2092
|
enabled: false,
|
|
@@ -1933,7 +2107,13 @@ export class HyperLiquidProvider {
|
|
|
1933
2107
|
previousMode: currentMode,
|
|
1934
2108
|
note: 'HyperLiquid will auto-manage collateral for HIP-3 orders',
|
|
1935
2109
|
});
|
|
1936
|
-
|
|
2110
|
+
if (accountContext) {
|
|
2111
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account migration');
|
|
2112
|
+
}
|
|
2113
|
+
const exchangeClient = this.#clientService.getExchangeClient(accountContext
|
|
2114
|
+
? async () => await this.#assertNoKnownUnsupportedAccount(accountContext, 'Unified account migration dispatch')
|
|
2115
|
+
: undefined);
|
|
2116
|
+
await exchangeClient.agentSetAbstraction({
|
|
1937
2117
|
abstraction: HL_ABSTRACTION_WIRE.unifiedAccount,
|
|
1938
2118
|
});
|
|
1939
2119
|
this.#deps.debugLogger.log('✅ HyperLiquidProvider: Unified Account enabled successfully');
|
|
@@ -1988,6 +2168,10 @@ export class HyperLiquidProvider {
|
|
|
1988
2168
|
// account-shape condition, not a failure worth reporting or retrying.
|
|
1989
2169
|
if (isHyperLiquidMultiSigRequiredError(error)) {
|
|
1990
2170
|
this.#deps.debugLogger.log('[ensureUnifiedAccountEnabled] Multi-sig account (race/probe fallback), skipping unified account migration', { user: userAddress, network, mode: currentMode });
|
|
2171
|
+
this.#recordAccountSupport(network, userAddress, {
|
|
2172
|
+
isSupported: false,
|
|
2173
|
+
reason: 'multi_sig_account',
|
|
2174
|
+
});
|
|
1991
2175
|
this.#deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.AccountSetup, {
|
|
1992
2176
|
...(currentMode && {
|
|
1993
2177
|
[PERPS_EVENT_PROPERTY.PREVIOUS_ABSTRACTION_MODE]: currentMode,
|
|
@@ -2020,6 +2204,12 @@ export class HyperLiquidProvider {
|
|
|
2020
2204
|
completeInFlight();
|
|
2021
2205
|
return;
|
|
2022
2206
|
}
|
|
2207
|
+
if (accountContext &&
|
|
2208
|
+
ensureError(error, 'HyperLiquidProvider.ensureUnifiedAccountEnabled')
|
|
2209
|
+
.message === PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
|
|
2210
|
+
completeInFlight();
|
|
2211
|
+
throw error;
|
|
2212
|
+
}
|
|
2023
2213
|
// Agent-path failures and read-only userAbstraction lookup failures
|
|
2024
2214
|
// are not final: signal #ensureReady to drop its memoized promise and
|
|
2025
2215
|
// retry on the next entry instead of pinning the user in the
|
|
@@ -2088,10 +2278,26 @@ export class HyperLiquidProvider {
|
|
|
2088
2278
|
// over. The memoized promise is dropped while the cooldown is active
|
|
2089
2279
|
// (see below), so a later entry retries. Action-time callers are not
|
|
2090
2280
|
// gated.
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2281
|
+
if (!this.#isDisconnected &&
|
|
2282
|
+
Date.now() >= this.#unifiedAccountTransportRetryAt) {
|
|
2283
|
+
if (this.#walletService.requiresSignatureConfirmation()) {
|
|
2284
|
+
await this.#ensureUnifiedAccountEnabled({
|
|
2285
|
+
allowUserSigning: false,
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
else {
|
|
2289
|
+
const accountContext = {
|
|
2290
|
+
lifecycleGeneration: this.#lifecycleGeneration,
|
|
2291
|
+
network: this.#clientService.isTestnetMode()
|
|
2292
|
+
? 'testnet'
|
|
2293
|
+
: 'mainnet',
|
|
2294
|
+
userAddress: await this.#walletService.getUserAddressWithDefault(),
|
|
2295
|
+
};
|
|
2296
|
+
await this.#ensureUnifiedAccountEnabled({
|
|
2297
|
+
allowUserSigning: true,
|
|
2298
|
+
accountContext,
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2095
2301
|
}
|
|
2096
2302
|
})();
|
|
2097
2303
|
// Await initialization - keep the promise so subsequent calls resolve immediately
|
|
@@ -2169,13 +2375,17 @@ export class HyperLiquidProvider {
|
|
|
2169
2375
|
* code (the approval is otherwise non-blocking).
|
|
2170
2376
|
* @param options.reportRefusal - Throw the venue's refusal, even without an
|
|
2171
2377
|
* approval failure code.
|
|
2378
|
+
* @param accountContext - Account/network/lifecycle captured by the action
|
|
2379
|
+
* that is allowed to authorize this approval.
|
|
2172
2380
|
* @returns The account, network, and configured builder for the action.
|
|
2173
2381
|
*/
|
|
2174
|
-
async #ensureBuilderFeeSetup(approvalFailureCode, options = {}) {
|
|
2175
|
-
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
const
|
|
2382
|
+
async #ensureBuilderFeeSetup(approvalFailureCode, options = {}, accountContext) {
|
|
2383
|
+
const lifecycleGeneration = accountContext?.lifecycleGeneration ?? this.#lifecycleGeneration;
|
|
2384
|
+
const network = accountContext?.network ??
|
|
2385
|
+
(this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
|
|
2386
|
+
const isTestnet = network === 'testnet';
|
|
2387
|
+
const userAddress = accountContext?.userAddress ??
|
|
2388
|
+
(await this.#walletService.getUserAddressWithDefault());
|
|
2179
2389
|
const cacheKey = this.#getCacheKey(network, userAddress);
|
|
2180
2390
|
const builderAddress = this.#getBuilderAddress(isTestnet);
|
|
2181
2391
|
const context = {
|
|
@@ -2203,7 +2413,7 @@ export class HyperLiquidProvider {
|
|
|
2203
2413
|
}
|
|
2204
2414
|
let pendingApproval = this.#builderFeeSetupPromises.get(setupKey);
|
|
2205
2415
|
if (!pendingApproval) {
|
|
2206
|
-
pendingApproval = this.#ensureBuilderFeeApproval(context);
|
|
2416
|
+
pendingApproval = this.#ensureBuilderFeeApproval(context, accountContext);
|
|
2207
2417
|
this.#builderFeeSetupPromises.set(setupKey, pendingApproval);
|
|
2208
2418
|
}
|
|
2209
2419
|
try {
|
|
@@ -2211,6 +2421,10 @@ export class HyperLiquidProvider {
|
|
|
2211
2421
|
}
|
|
2212
2422
|
catch (error) {
|
|
2213
2423
|
this.#deps.debugLogger.log('[ensureBuilderFeeSetup] Builder fee approval failed', error);
|
|
2424
|
+
if (accountContext &&
|
|
2425
|
+
this.#recordUnsupportedAccountFromError(error, accountContext)) {
|
|
2426
|
+
throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
|
|
2427
|
+
}
|
|
2214
2428
|
// A signer that could not sign is retryable, not an approval failure.
|
|
2215
2429
|
if (approvalFailureCode || options.reportSignerFailure) {
|
|
2216
2430
|
const signerFailure = await this.#classifySignerFailure(error);
|
|
@@ -2243,10 +2457,15 @@ export class HyperLiquidProvider {
|
|
|
2243
2457
|
async #ensureReadyForTrading(options) {
|
|
2244
2458
|
// First ensure basic initialization is complete
|
|
2245
2459
|
await this.#ensureReady();
|
|
2460
|
+
const accountContext = await this.#assertAccountSupported(options.accountContext);
|
|
2246
2461
|
// The migration was deferred during init to avoid a signing prompt on
|
|
2247
2462
|
// Perps section open. Drive it here, gated by its own cache so
|
|
2248
2463
|
// already-migrated users are not re-prompted.
|
|
2249
|
-
await this.#ensureUnifiedAccountEnabled({
|
|
2464
|
+
await this.#ensureUnifiedAccountEnabled({
|
|
2465
|
+
allowUserSigning: true,
|
|
2466
|
+
accountContext,
|
|
2467
|
+
});
|
|
2468
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Trading setup');
|
|
2250
2469
|
// Reset right before the check, with no await in between, so a failure
|
|
2251
2470
|
// above does not leave the setup for an order to run.
|
|
2252
2471
|
if (options.recheckPendingReferral && this.#referralAwaitsBuilderCode) {
|
|
@@ -2268,7 +2487,7 @@ export class HyperLiquidProvider {
|
|
|
2268
2487
|
}
|
|
2269
2488
|
}
|
|
2270
2489
|
// Set up referral code independently from builder-fee applicability.
|
|
2271
|
-
await this.#ensureReferralSet();
|
|
2490
|
+
await this.#ensureReferralSet(accountContext);
|
|
2272
2491
|
this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Trading setup completion');
|
|
2273
2492
|
// Only mark complete if the main-account signer is ready (signing
|
|
2274
2493
|
// could actually happen) and the referral does not need another
|
|
@@ -2291,10 +2510,14 @@ export class HyperLiquidProvider {
|
|
|
2291
2510
|
}
|
|
2292
2511
|
}
|
|
2293
2512
|
const builderFeeSetupContext = options.requiresBuilderFee
|
|
2294
|
-
? await this.#ensureBuilderFeeSetup(options.builderFeeApprovalFailureCode)
|
|
2513
|
+
? await this.#ensureBuilderFeeSetup(options.builderFeeApprovalFailureCode, {}, accountContext)
|
|
2295
2514
|
: undefined;
|
|
2515
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Trading setup');
|
|
2296
2516
|
this.#deps.debugLogger.log('[ensureReadyForTrading] Trading setup complete');
|
|
2297
|
-
return
|
|
2517
|
+
return {
|
|
2518
|
+
accountContext,
|
|
2519
|
+
...(builderFeeSetupContext && { builderFeeSetupContext }),
|
|
2520
|
+
};
|
|
2298
2521
|
}
|
|
2299
2522
|
/**
|
|
2300
2523
|
* Get current price for a symbol using WebSocket cache first, REST API fallback
|
|
@@ -3228,10 +3451,14 @@ export class HyperLiquidProvider {
|
|
|
3228
3451
|
*
|
|
3229
3452
|
* @param message - The status entry's error.
|
|
3230
3453
|
* @param answer - The venue answer with the entry.
|
|
3454
|
+
* @param accountContext - Account and network that authorized the write.
|
|
3231
3455
|
* @returns The mapped error.
|
|
3232
3456
|
*/
|
|
3233
|
-
async #mapStatusError(message, answer) {
|
|
3457
|
+
async #mapStatusError(message, answer, accountContext) {
|
|
3234
3458
|
const error = createStatusError(message, answer);
|
|
3459
|
+
if (accountContext) {
|
|
3460
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
3461
|
+
}
|
|
3235
3462
|
return (await this.#classifySignerFailure(error)) ?? this.#mapError(error);
|
|
3236
3463
|
}
|
|
3237
3464
|
/**
|
|
@@ -3360,10 +3587,17 @@ export class HyperLiquidProvider {
|
|
|
3360
3587
|
* @param params.network - HyperLiquid network for the approval.
|
|
3361
3588
|
* @param params.userAddress - Account that owns the approval.
|
|
3362
3589
|
* @param params.builderAddress - Builder address being approved.
|
|
3590
|
+
* @param accountContext - Account/network/lifecycle captured by the action
|
|
3591
|
+
* that is allowed to authorize this approval.
|
|
3363
3592
|
*/
|
|
3364
|
-
async #ensureBuilderFeeApproval(params) {
|
|
3365
|
-
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
3593
|
+
async #ensureBuilderFeeApproval(params, accountContext) {
|
|
3594
|
+
const lifecycleGeneration = accountContext?.lifecycleGeneration ?? this.#lifecycleGeneration;
|
|
3366
3595
|
const { network, userAddress, builderAddress } = params;
|
|
3596
|
+
const supportContext = accountContext ?? {
|
|
3597
|
+
lifecycleGeneration,
|
|
3598
|
+
network,
|
|
3599
|
+
userAddress,
|
|
3600
|
+
};
|
|
3367
3601
|
this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Builder fee approval');
|
|
3368
3602
|
const cacheKey = this.#getCacheKey(network, userAddress);
|
|
3369
3603
|
// Check GLOBAL cache first to avoid repeated signing requests across reconnections
|
|
@@ -3401,7 +3635,12 @@ export class HyperLiquidProvider {
|
|
|
3401
3635
|
return;
|
|
3402
3636
|
}
|
|
3403
3637
|
const { isApproved, requiredDecimal } = await this.#checkBuilderFeeStatus(builderAddress, userAddress);
|
|
3404
|
-
|
|
3638
|
+
if (accountContext) {
|
|
3639
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Builder fee approval lookup');
|
|
3640
|
+
}
|
|
3641
|
+
else {
|
|
3642
|
+
this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Builder fee approval');
|
|
3643
|
+
}
|
|
3405
3644
|
if (isApproved) {
|
|
3406
3645
|
// User already has approval on-chain
|
|
3407
3646
|
PerpsSigningCache.setBuilderFee(network, userAddress, {
|
|
@@ -3414,12 +3653,20 @@ export class HyperLiquidProvider {
|
|
|
3414
3653
|
}
|
|
3415
3654
|
else {
|
|
3416
3655
|
this.#deps.debugLogger.log('[ensureBuilderFeeApproval] Approval required (will show signing request)', { builder: builderAddress, requiredDecimal });
|
|
3656
|
+
if (accountContext) {
|
|
3657
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Builder fee approval');
|
|
3658
|
+
}
|
|
3417
3659
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
3418
3660
|
const maxFeeRate = BUILDER_FEE_CONFIG.MaxFeeRate;
|
|
3419
|
-
await exchangeClient.approveBuilderFee({
|
|
3661
|
+
const approvalResult = await exchangeClient.approveBuilderFee({
|
|
3420
3662
|
builder: builderAddress,
|
|
3421
3663
|
maxFeeRate,
|
|
3422
3664
|
});
|
|
3665
|
+
if (approvalResult.status !== 'ok') {
|
|
3666
|
+
const approvalError = new Error(`Builder fee approval failed: ${JSON.stringify(approvalResult)}`);
|
|
3667
|
+
this.#recordUnsupportedAccountFromError(approvalError, supportContext);
|
|
3668
|
+
throw approvalError;
|
|
3669
|
+
}
|
|
3423
3670
|
// Verify approval was successful before caching
|
|
3424
3671
|
const afterApprovalDecimal = await this.#checkBuilderFeeApproval(builderAddress, userAddress);
|
|
3425
3672
|
if (afterApprovalDecimal === null ||
|
|
@@ -3445,6 +3692,7 @@ export class HyperLiquidProvider {
|
|
|
3445
3692
|
completeInFlight();
|
|
3446
3693
|
}
|
|
3447
3694
|
catch (error) {
|
|
3695
|
+
this.#recordUnsupportedAccountFromError(error, supportContext);
|
|
3448
3696
|
if (ensureError(error, 'HyperLiquidProvider.ensureBuilderFeeApproval')
|
|
3449
3697
|
.message === PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
|
|
3450
3698
|
completeInFlight();
|
|
@@ -3641,11 +3889,13 @@ export class HyperLiquidProvider {
|
|
|
3641
3889
|
* @param params - Transfer parameters
|
|
3642
3890
|
* @param params.targetDex - HIP-3 DEX name (e.g., 'xyz')
|
|
3643
3891
|
* @param params.requiredMargin - Required margin with buffer
|
|
3892
|
+
* @param params.accountContext - Account/network/lifecycle captured by the
|
|
3893
|
+
* order that may authorize a transfer.
|
|
3644
3894
|
* @returns Transfer info for rollback, or null if no transfer needed
|
|
3645
3895
|
* @private
|
|
3646
3896
|
*/
|
|
3647
3897
|
async #autoTransferForHip3Order(params) {
|
|
3648
|
-
const { targetDex, requiredMargin } = params;
|
|
3898
|
+
const { targetDex, requiredMargin, accountContext } = params;
|
|
3649
3899
|
// Check target DEX balance
|
|
3650
3900
|
const targetBalance = await this.#getBalanceForDex({ dex: targetDex });
|
|
3651
3901
|
this.#deps.debugLogger.log('HyperLiquidProvider: HIP-3 balance check', {
|
|
@@ -3674,15 +3924,19 @@ export class HyperLiquidProvider {
|
|
|
3674
3924
|
to: targetDex,
|
|
3675
3925
|
amount: transferAmount,
|
|
3676
3926
|
});
|
|
3677
|
-
const
|
|
3927
|
+
const transferParams = {
|
|
3678
3928
|
sourceDex: source.sourceDex,
|
|
3679
3929
|
destinationDex: targetDex,
|
|
3680
3930
|
amount: transferAmount,
|
|
3681
|
-
}
|
|
3931
|
+
};
|
|
3932
|
+
const result = accountContext
|
|
3933
|
+
? await this.#transferBetweenDexsForContext(transferParams, accountContext)
|
|
3934
|
+
: await this.transferBetweenDexs(transferParams);
|
|
3682
3935
|
if (!result.success) {
|
|
3683
|
-
//
|
|
3684
|
-
if (result.error === PERPS_ERROR_CODES.KEYRING_LOCKED
|
|
3685
|
-
|
|
3936
|
+
// Preserve stable account/signer outcomes that callers handle directly.
|
|
3937
|
+
if (result.error === PERPS_ERROR_CODES.KEYRING_LOCKED ||
|
|
3938
|
+
result.error === PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED) {
|
|
3939
|
+
throw new Error(result.error);
|
|
3686
3940
|
}
|
|
3687
3941
|
throw new Error(`Auto-transfer failed: ${result.error ?? 'Unknown error'}`);
|
|
3688
3942
|
}
|
|
@@ -3710,11 +3964,13 @@ export class HyperLiquidProvider {
|
|
|
3710
3964
|
* @param params.freedMargin - Amount of margin released from position close
|
|
3711
3965
|
* @param params.transferAll - (Future) Transfer all available balance instead
|
|
3712
3966
|
* @param params.skipTransfer - (Future) Skip auto-transfer if disabled
|
|
3967
|
+
* @param params.accountContext - Original action context to preserve across
|
|
3968
|
+
* the balance read and transfer.
|
|
3713
3969
|
* @returns Transfer info if successful, null if skipped/failed
|
|
3714
3970
|
* @private
|
|
3715
3971
|
*/
|
|
3716
3972
|
async #autoTransferBackAfterClose(params) {
|
|
3717
|
-
const { sourceDex, freedMargin, transferAll = false, skipTransfer = false, } = params;
|
|
3973
|
+
const { sourceDex, freedMargin, transferAll = false, skipTransfer = false, accountContext, } = params;
|
|
3718
3974
|
// Future: Check user preference to skip auto-transfer
|
|
3719
3975
|
if (skipTransfer) {
|
|
3720
3976
|
this.#deps.debugLogger.log('Auto-transfer back skipped (disabled by config)');
|
|
@@ -3750,11 +4006,14 @@ export class HyperLiquidProvider {
|
|
|
3750
4006
|
to: 'main',
|
|
3751
4007
|
});
|
|
3752
4008
|
// Execute transfer back to main DEX (empty string '' represents main DEX)
|
|
3753
|
-
const
|
|
4009
|
+
const transferParams = {
|
|
3754
4010
|
sourceDex,
|
|
3755
4011
|
destinationDex: '',
|
|
3756
4012
|
amount: transferAmount.toFixed(USDC_DECIMALS),
|
|
3757
|
-
}
|
|
4013
|
+
};
|
|
4014
|
+
const result = accountContext
|
|
4015
|
+
? await this.#transferBetweenDexsForContext(transferParams, accountContext)
|
|
4016
|
+
: await this.transferBetweenDexs(transferParams);
|
|
3758
4017
|
if (!result.success) {
|
|
3759
4018
|
this.#deps.debugLogger.log('❌ Auto-transfer back failed', {
|
|
3760
4019
|
error: result.error,
|
|
@@ -3878,11 +4137,12 @@ export class HyperLiquidProvider {
|
|
|
3878
4137
|
* @param params.transferInfo - The transfer information.
|
|
3879
4138
|
* @param params.transferInfo.amount - The amount value.
|
|
3880
4139
|
* @param params.transferInfo.sourceDex - The source DEX for the transfer.
|
|
4140
|
+
* @param params.accountContext - Original action context for any rebalance.
|
|
3881
4141
|
* @returns Whether the balance check and any required transfer succeeded.
|
|
3882
4142
|
* @private
|
|
3883
4143
|
*/
|
|
3884
4144
|
async #handleHip3PostOrderRebalance(params) {
|
|
3885
|
-
const { dexName, transferInfo } = params;
|
|
4145
|
+
const { accountContext, dexName, transferInfo } = params;
|
|
3886
4146
|
try {
|
|
3887
4147
|
const postOrderBalance = await this.#getBalanceForDex({ dex: dexName });
|
|
3888
4148
|
const transferredAmount = transferInfo.amount;
|
|
@@ -3907,11 +4167,11 @@ export class HyperLiquidProvider {
|
|
|
3907
4167
|
excessAmount: excessAmount.toFixed(2),
|
|
3908
4168
|
destinationDex: transferInfo.sourceDex,
|
|
3909
4169
|
});
|
|
3910
|
-
const transferResult = await this
|
|
4170
|
+
const transferResult = await this.#transferBetweenDexsForContext({
|
|
3911
4171
|
sourceDex: dexName,
|
|
3912
4172
|
destinationDex: transferInfo.sourceDex,
|
|
3913
4173
|
amount: excessAmount.toFixed(USDC_DECIMALS),
|
|
3914
|
-
});
|
|
4174
|
+
}, accountContext);
|
|
3915
4175
|
if (!transferResult.success) {
|
|
3916
4176
|
// The signer could not sign the transfer: retryable, not a defect.
|
|
3917
4177
|
if (transferResult.error === PERPS_ERROR_CODES.KEYRING_LOCKED) {
|
|
@@ -3971,10 +4231,11 @@ export class HyperLiquidProvider {
|
|
|
3971
4231
|
* @param params.transferInfo - The transfer information.
|
|
3972
4232
|
* @param params.transferInfo.amount - The amount value.
|
|
3973
4233
|
* @param params.transferInfo.sourceDex - The source DEX for the transfer.
|
|
4234
|
+
* @param params.accountContext - Original action context for the rollback.
|
|
3974
4235
|
* @private
|
|
3975
4236
|
*/
|
|
3976
4237
|
async #handleHip3OrderRollback(params) {
|
|
3977
|
-
const { dexName, transferInfo } = params;
|
|
4238
|
+
const { accountContext, dexName, transferInfo } = params;
|
|
3978
4239
|
try {
|
|
3979
4240
|
this.#deps.debugLogger.log('HyperLiquidProvider: Rolling back failed order transfer', {
|
|
3980
4241
|
from: dexName,
|
|
@@ -3982,11 +4243,11 @@ export class HyperLiquidProvider {
|
|
|
3982
4243
|
amount: transferInfo.amount.toFixed(USDC_DECIMALS),
|
|
3983
4244
|
reason: 'order_failed',
|
|
3984
4245
|
});
|
|
3985
|
-
const rollbackResult = await this
|
|
4246
|
+
const rollbackResult = await this.#transferBetweenDexsForContext({
|
|
3986
4247
|
sourceDex: dexName, // From HIP-3 DEX
|
|
3987
4248
|
destinationDex: transferInfo.sourceDex, // Back to source
|
|
3988
4249
|
amount: transferInfo.amount.toFixed(USDC_DECIMALS),
|
|
3989
|
-
});
|
|
4250
|
+
}, accountContext);
|
|
3990
4251
|
if (rollbackResult.success) {
|
|
3991
4252
|
this.#deps.debugLogger.log('✅ HyperLiquidProvider: Rollback successful', {
|
|
3992
4253
|
amount: transferInfo.amount.toFixed(USDC_DECIMALS),
|
|
@@ -4160,7 +4421,7 @@ export class HyperLiquidProvider {
|
|
|
4160
4421
|
* @param params - The operation parameters.
|
|
4161
4422
|
*/
|
|
4162
4423
|
async #prepareAssetForTrading(params) {
|
|
4163
|
-
const { symbol, assetId, leverage } = params;
|
|
4424
|
+
const { symbol, assetId, leverage, accountContext } = params;
|
|
4164
4425
|
if (!leverage) {
|
|
4165
4426
|
return;
|
|
4166
4427
|
}
|
|
@@ -4171,14 +4432,21 @@ export class HyperLiquidProvider {
|
|
|
4171
4432
|
requestedLeverage: leverage,
|
|
4172
4433
|
leverageType: marginMode,
|
|
4173
4434
|
});
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4435
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Leverage update');
|
|
4436
|
+
try {
|
|
4437
|
+
const exchangeClient = this.#clientService.getExchangeClient();
|
|
4438
|
+
const leverageResult = await exchangeClient.updateLeverage({
|
|
4439
|
+
asset: assetId,
|
|
4440
|
+
isCross: marginMode === 'cross',
|
|
4441
|
+
leverage,
|
|
4442
|
+
});
|
|
4443
|
+
if (leverageResult.status !== 'ok') {
|
|
4444
|
+
throw new Error(`Failed to update leverage: ${JSON.stringify(leverageResult)}`);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
catch (error) {
|
|
4448
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
4449
|
+
throw error;
|
|
4182
4450
|
}
|
|
4183
4451
|
this.#deps.debugLogger.log('Leverage updated successfully:', {
|
|
4184
4452
|
symbol,
|
|
@@ -4216,7 +4484,7 @@ export class HyperLiquidProvider {
|
|
|
4216
4484
|
* @returns A promise that resolves to the result.
|
|
4217
4485
|
*/
|
|
4218
4486
|
async #handleHip3PreOrder(params) {
|
|
4219
|
-
const { dexName, symbol, orderPrice, positionSize, leverage, isBuy } = params;
|
|
4487
|
+
const { accountContext, dexName, symbol, orderPrice, positionSize, leverage, isBuy, } = params;
|
|
4220
4488
|
// TAT-3304: Only USDC-collateral HIP-3 DEXs are supported for trading.
|
|
4221
4489
|
// Following the USDH sunset, reject orders on any non-USDC-collateral
|
|
4222
4490
|
// DEX here instead of attempting the (now-removed) USDH auto-swap path.
|
|
@@ -4254,6 +4522,7 @@ export class HyperLiquidProvider {
|
|
|
4254
4522
|
const transferInfo = await this.#autoTransferForHip3Order({
|
|
4255
4523
|
targetDex: dexName,
|
|
4256
4524
|
requiredMargin: requiredMarginWithBuffer,
|
|
4525
|
+
accountContext,
|
|
4257
4526
|
});
|
|
4258
4527
|
return { transferInfo };
|
|
4259
4528
|
}
|
|
@@ -4278,7 +4547,7 @@ export class HyperLiquidProvider {
|
|
|
4278
4547
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
4279
4548
|
const builder = params.chargesMetamaskBuilderFee
|
|
4280
4549
|
? await this.#getBuilderOrderContext(params.builderFeeSetupContext ??
|
|
4281
|
-
(await this.#ensureBuilderFeeSetup()))
|
|
4550
|
+
(await this.#ensureBuilderFeeSetup(undefined, {}, params.accountContext)))
|
|
4282
4551
|
: undefined;
|
|
4283
4552
|
this.#deps.debugLogger.log('Submitting order via asset ID routing', {
|
|
4284
4553
|
symbol,
|
|
@@ -4289,6 +4558,7 @@ export class HyperLiquidProvider {
|
|
|
4289
4558
|
isHip3: Boolean(dexName),
|
|
4290
4559
|
});
|
|
4291
4560
|
try {
|
|
4561
|
+
await this.#assertNoKnownUnsupportedAccount(params.accountContext, 'Order submission');
|
|
4292
4562
|
const result = await exchangeClient.order({
|
|
4293
4563
|
orders: this.#applySubscriptionCloid(orders),
|
|
4294
4564
|
grouping,
|
|
@@ -4298,6 +4568,9 @@ export class HyperLiquidProvider {
|
|
|
4298
4568
|
throw new Error(`Order failed: ${JSON.stringify(result)}`);
|
|
4299
4569
|
}
|
|
4300
4570
|
const status = result.response?.data?.statuses?.[0];
|
|
4571
|
+
if (hasProperty(status, 'error') && typeof status.error === 'string') {
|
|
4572
|
+
throw createStatusError(status.error, result);
|
|
4573
|
+
}
|
|
4301
4574
|
// Note: `in` narrows the HyperLiquid SDK discriminated union to the
|
|
4302
4575
|
// branch that has the property; `hasProperty` types the property as
|
|
4303
4576
|
// `unknown`, losing downstream access to `.oid`, `.totalSz`, `.avgPx`.
|
|
@@ -4307,7 +4580,11 @@ export class HyperLiquidProvider {
|
|
|
4307
4580
|
/* eslint-enable no-restricted-syntax */
|
|
4308
4581
|
// Success - auto-rebalance excess funds
|
|
4309
4582
|
if (isHip3Order && transferInfo && dexName) {
|
|
4310
|
-
await this.#handleHip3PostOrderRebalance({
|
|
4583
|
+
await this.#handleHip3PostOrderRebalance({
|
|
4584
|
+
accountContext: params.accountContext,
|
|
4585
|
+
dexName,
|
|
4586
|
+
transferInfo,
|
|
4587
|
+
});
|
|
4311
4588
|
}
|
|
4312
4589
|
return {
|
|
4313
4590
|
success: true,
|
|
@@ -4323,9 +4600,14 @@ export class HyperLiquidProvider {
|
|
|
4323
4600
|
};
|
|
4324
4601
|
}
|
|
4325
4602
|
catch (orderError) {
|
|
4603
|
+
this.#recordUnsupportedAccountFromError(orderError, params.accountContext);
|
|
4326
4604
|
// Failure - rollback transfer
|
|
4327
4605
|
if (transferInfo && dexName) {
|
|
4328
|
-
await this.#handleHip3OrderRollback({
|
|
4606
|
+
await this.#handleHip3OrderRollback({
|
|
4607
|
+
accountContext: params.accountContext,
|
|
4608
|
+
dexName,
|
|
4609
|
+
transferInfo,
|
|
4610
|
+
});
|
|
4329
4611
|
}
|
|
4330
4612
|
throw orderError;
|
|
4331
4613
|
}
|
|
@@ -4380,6 +4662,20 @@ export class HyperLiquidProvider {
|
|
|
4380
4662
|
* @returns A promise that resolves to the result.
|
|
4381
4663
|
*/
|
|
4382
4664
|
async placeOrder(input, _retryCount = 0, reportFailure = true) {
|
|
4665
|
+
return await this.#placeOrder(input, _retryCount, reportFailure);
|
|
4666
|
+
}
|
|
4667
|
+
/**
|
|
4668
|
+
* Place an order while preserving one account context across parent actions
|
|
4669
|
+
* and minimum-value retries.
|
|
4670
|
+
*
|
|
4671
|
+
* @param input - Order parameters.
|
|
4672
|
+
* @param _retryCount - Unused. Preserves the public call shape internally.
|
|
4673
|
+
* @param reportFailure - Whether this call owns provider-level reporting.
|
|
4674
|
+
* @param expectedAccountContext - Context captured by a parent action or the
|
|
4675
|
+
* first minimum-value attempt.
|
|
4676
|
+
* @returns The order result.
|
|
4677
|
+
*/
|
|
4678
|
+
async #placeOrder(input, _retryCount, reportFailure, expectedAccountContext) {
|
|
4383
4679
|
const deferProviderErrorReport = input.deferProviderErrorReport === true;
|
|
4384
4680
|
const params = { ...input };
|
|
4385
4681
|
delete params.deferProviderErrorReport;
|
|
@@ -4390,6 +4686,7 @@ export class HyperLiquidProvider {
|
|
|
4390
4686
|
error: PERPS_ERROR_CODES.ORDER_SCALE_PREVIEW_STALE,
|
|
4391
4687
|
};
|
|
4392
4688
|
}
|
|
4689
|
+
let actionAccountContext = expectedAccountContext;
|
|
4393
4690
|
try {
|
|
4394
4691
|
this.#deps.debugLogger.log('Placing order via HyperLiquid SDK:', params);
|
|
4395
4692
|
// Basic sync validation (backward compatibility)
|
|
@@ -4417,7 +4714,7 @@ export class HyperLiquidProvider {
|
|
|
4417
4714
|
// array, the HIP-3 transfer, and the atomic single-order submit, none of
|
|
4418
4715
|
// which describe what a TWAP, a ladder, or a chase does.
|
|
4419
4716
|
if (isStrategyOrderType(params.orderType)) {
|
|
4420
|
-
return await this.#placeStrategyOrder(params, params.orderType);
|
|
4717
|
+
return await this.#placeStrategyOrder(params, params.orderType, expectedAccountContext);
|
|
4421
4718
|
}
|
|
4422
4719
|
// Extract DEX name for API calls (main DEX = null)
|
|
4423
4720
|
const { dex: dexName } = parseAssetName(params.symbol);
|
|
@@ -4466,9 +4763,11 @@ export class HyperLiquidProvider {
|
|
|
4466
4763
|
// Kept after validation so invalid orders never trigger signature prompts
|
|
4467
4764
|
// (builder-fee approval, DEX abstraction enablement, etc.).
|
|
4468
4765
|
const { chargesMetamaskBuilderFee } = this.#resolveOrderFeePolicy(params);
|
|
4469
|
-
const builderFeeSetupContext = await this.#ensureReadyForTrading({
|
|
4766
|
+
const { accountContext, builderFeeSetupContext } = await this.#ensureReadyForTrading({
|
|
4470
4767
|
requiresBuilderFee: chargesMetamaskBuilderFee,
|
|
4768
|
+
accountContext: actionAccountContext,
|
|
4471
4769
|
});
|
|
4770
|
+
actionAccountContext = accountContext;
|
|
4472
4771
|
// Debug: Log asset map state before order placement
|
|
4473
4772
|
const allMapKeys = Array.from(this.#symbolToAssetId.keys());
|
|
4474
4773
|
const hip3Keys = allMapKeys.filter((key) => key.includes(':'));
|
|
@@ -4526,6 +4825,7 @@ export class HyperLiquidProvider {
|
|
|
4526
4825
|
assetId,
|
|
4527
4826
|
leverage: params.leverage,
|
|
4528
4827
|
marginMode: params.marginMode,
|
|
4828
|
+
accountContext,
|
|
4529
4829
|
});
|
|
4530
4830
|
// 6. Handle HIP-3 balance management (if applicable)
|
|
4531
4831
|
const isHip3Order = dexName !== null;
|
|
@@ -4533,6 +4833,7 @@ export class HyperLiquidProvider {
|
|
|
4533
4833
|
if (isHip3Order && dexName) {
|
|
4534
4834
|
const effectiveLeverage = params.leverage ?? assetInfo.maxLeverage ?? 1;
|
|
4535
4835
|
const hip3Result = await this.#handleHip3PreOrder({
|
|
4836
|
+
accountContext,
|
|
4536
4837
|
dexName,
|
|
4537
4838
|
symbol: params.symbol,
|
|
4538
4839
|
orderPrice,
|
|
@@ -4575,6 +4876,7 @@ export class HyperLiquidProvider {
|
|
|
4575
4876
|
symbol: params.symbol,
|
|
4576
4877
|
assetId,
|
|
4577
4878
|
chargesMetamaskBuilderFee,
|
|
4879
|
+
accountContext,
|
|
4578
4880
|
builderFeeSetupContext,
|
|
4579
4881
|
});
|
|
4580
4882
|
}
|
|
@@ -4607,15 +4909,16 @@ export class HyperLiquidProvider {
|
|
|
4607
4909
|
*
|
|
4608
4910
|
* @param params - Order parameters, already validated.
|
|
4609
4911
|
* @param orderType - The strategy type, narrowed by the caller.
|
|
4912
|
+
* @param expectedAccountContext - Context captured by a parent action.
|
|
4610
4913
|
* @returns A promise that resolves to the result.
|
|
4611
4914
|
*/
|
|
4612
|
-
async #placeStrategyOrder(params, orderType) {
|
|
4915
|
+
async #placeStrategyOrder(params, orderType, expectedAccountContext) {
|
|
4613
4916
|
// Captured before the shared preamble, not after it: that preamble awaits
|
|
4614
4917
|
// asset info, validation, trading readiness and a leverage update, and a
|
|
4615
4918
|
// disconnect during any of them has to be seen before or after submission.
|
|
4616
4919
|
const generation = this.#strategyGeneration;
|
|
4617
4920
|
if (orderType !== 'chase') {
|
|
4618
|
-
const context = await this.#prepareStrategyPlacement(params);
|
|
4921
|
+
const context = await this.#prepareStrategyPlacement(params, expectedAccountContext);
|
|
4619
4922
|
return await this.#submitPreparedStrategy(params, context, async () => {
|
|
4620
4923
|
if (generation !== this.#strategyGeneration) {
|
|
4621
4924
|
throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
|
|
@@ -4648,7 +4951,7 @@ export class HyperLiquidProvider {
|
|
|
4648
4951
|
this.#chasePlacementWaiters.add(placementWaiter);
|
|
4649
4952
|
try {
|
|
4650
4953
|
await this.#chaseTickQueue;
|
|
4651
|
-
const context = await this.#prepareStrategyPlacement(params);
|
|
4954
|
+
const context = await this.#prepareStrategyPlacement(params, expectedAccountContext);
|
|
4652
4955
|
return await this.#submitPreparedStrategy(params, context, async () => await this.#startChaseSession(params, context, generation));
|
|
4653
4956
|
}
|
|
4654
4957
|
finally {
|
|
@@ -4674,24 +4977,36 @@ export class HyperLiquidProvider {
|
|
|
4674
4977
|
* @returns The strategy placement result.
|
|
4675
4978
|
*/
|
|
4676
4979
|
async #submitPreparedStrategy(params, context, submit) {
|
|
4677
|
-
const { dexName, network, transferInfo, userAddress } = context;
|
|
4980
|
+
const { accountContext, dexName, network, transferInfo, userAddress } = context;
|
|
4678
4981
|
let result;
|
|
4679
4982
|
try {
|
|
4680
4983
|
result = await submit();
|
|
4681
4984
|
}
|
|
4682
4985
|
catch (error) {
|
|
4986
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
4683
4987
|
if (dexName && transferInfo) {
|
|
4684
|
-
await this.#handleHip3OrderRollback({
|
|
4988
|
+
await this.#handleHip3OrderRollback({
|
|
4989
|
+
accountContext,
|
|
4990
|
+
dexName,
|
|
4991
|
+
transferInfo,
|
|
4992
|
+
});
|
|
4685
4993
|
}
|
|
4686
4994
|
throw error;
|
|
4687
4995
|
}
|
|
4996
|
+
if (!result.success && result.error) {
|
|
4997
|
+
this.#recordUnsupportedAccountFromError(new Error(result.error), accountContext);
|
|
4998
|
+
}
|
|
4688
4999
|
const resultFilledSize = new BigNumber(result.filledSize ?? 0);
|
|
4689
5000
|
const hasVenueExposure = result.success === true ||
|
|
4690
5001
|
result.orderId !== undefined ||
|
|
4691
5002
|
(result.childOrderIds?.length ?? 0) > 0 ||
|
|
4692
5003
|
(resultFilledSize.isFinite() && resultFilledSize.gt(0));
|
|
4693
5004
|
if (dexName && transferInfo && !hasVenueExposure) {
|
|
4694
|
-
await this.#handleHip3OrderRollback({
|
|
5005
|
+
await this.#handleHip3OrderRollback({
|
|
5006
|
+
accountContext,
|
|
5007
|
+
dexName,
|
|
5008
|
+
transferInfo,
|
|
5009
|
+
});
|
|
4695
5010
|
return result;
|
|
4696
5011
|
}
|
|
4697
5012
|
if (params.orderType === 'twap' && result.orderId) {
|
|
@@ -4702,7 +5017,13 @@ export class HyperLiquidProvider {
|
|
|
4702
5017
|
}), {
|
|
4703
5018
|
symbol: params.symbol,
|
|
4704
5019
|
...(dexName && transferInfo
|
|
4705
|
-
? {
|
|
5020
|
+
? {
|
|
5021
|
+
hip3Transfer: {
|
|
5022
|
+
accountContext,
|
|
5023
|
+
dexName,
|
|
5024
|
+
transferInfo,
|
|
5025
|
+
},
|
|
5026
|
+
}
|
|
4706
5027
|
: {}),
|
|
4707
5028
|
});
|
|
4708
5029
|
return result;
|
|
@@ -4714,7 +5035,11 @@ export class HyperLiquidProvider {
|
|
|
4714
5035
|
transferInfo) {
|
|
4715
5036
|
const session = this.#chaseSessions.get(result.orderId);
|
|
4716
5037
|
if (session) {
|
|
4717
|
-
session.hip3Transfer = {
|
|
5038
|
+
session.hip3Transfer = {
|
|
5039
|
+
accountContext,
|
|
5040
|
+
dexName,
|
|
5041
|
+
transferInfo,
|
|
5042
|
+
};
|
|
4718
5043
|
return result;
|
|
4719
5044
|
}
|
|
4720
5045
|
}
|
|
@@ -4726,7 +5051,11 @@ export class HyperLiquidProvider {
|
|
|
4726
5051
|
return result;
|
|
4727
5052
|
}
|
|
4728
5053
|
if (dexName && transferInfo) {
|
|
4729
|
-
await this.#handleHip3PostOrderRebalance({
|
|
5054
|
+
await this.#handleHip3PostOrderRebalance({
|
|
5055
|
+
accountContext,
|
|
5056
|
+
dexName,
|
|
5057
|
+
transferInfo,
|
|
5058
|
+
});
|
|
4730
5059
|
}
|
|
4731
5060
|
return result;
|
|
4732
5061
|
}
|
|
@@ -4738,9 +5067,10 @@ export class HyperLiquidProvider {
|
|
|
4738
5067
|
* so a strategy order is held to the same rules as an ordinary one.
|
|
4739
5068
|
*
|
|
4740
5069
|
* @param params - Order parameters.
|
|
5070
|
+
* @param expectedAccountContext - Context captured by a parent action.
|
|
4741
5071
|
* @returns The asset and sizing context the strategy handlers submit with.
|
|
4742
5072
|
*/
|
|
4743
|
-
async #prepareStrategyPlacement(params) {
|
|
5073
|
+
async #prepareStrategyPlacement(params, expectedAccountContext) {
|
|
4744
5074
|
const { dex: dexName } = parseAssetName(params.symbol);
|
|
4745
5075
|
const { assetInfo, currentPrice, meta } = await this.#getAssetInfo({
|
|
4746
5076
|
symbol: params.symbol,
|
|
@@ -4803,9 +5133,16 @@ export class HyperLiquidProvider {
|
|
|
4803
5133
|
// Kept after validation so an invalid strategy order never triggers the
|
|
4804
5134
|
// signature prompts in trading setup — same ordering as `placeOrder`.
|
|
4805
5135
|
const { chargesMetamaskBuilderFee } = this.#resolveOrderFeePolicy(params);
|
|
4806
|
-
const
|
|
4807
|
-
? await this.#ensureReadyForTrading({
|
|
4808
|
-
|
|
5136
|
+
const tradingActionContext = chargesMetamaskBuilderFee
|
|
5137
|
+
? await this.#ensureReadyForTrading({
|
|
5138
|
+
requiresBuilderFee: true,
|
|
5139
|
+
accountContext: expectedAccountContext,
|
|
5140
|
+
})
|
|
5141
|
+
: await this.#ensureReadyForTrading({
|
|
5142
|
+
requiresBuilderFee: false,
|
|
5143
|
+
accountContext: expectedAccountContext,
|
|
5144
|
+
});
|
|
5145
|
+
const { accountContext, builderFeeSetupContext } = tradingActionContext;
|
|
4809
5146
|
const assetId = await this.#getAssetIdWithRepair({
|
|
4810
5147
|
symbol: params.symbol,
|
|
4811
5148
|
dexName,
|
|
@@ -4816,19 +5153,19 @@ export class HyperLiquidProvider {
|
|
|
4816
5153
|
assetId,
|
|
4817
5154
|
leverage: params.leverage,
|
|
4818
5155
|
marginMode: params.marginMode,
|
|
5156
|
+
accountContext,
|
|
4819
5157
|
});
|
|
4820
5158
|
const builder = builderFeeSetupContext
|
|
4821
5159
|
? await this.#getBuilderOrderContext(builderFeeSetupContext)
|
|
4822
5160
|
: undefined;
|
|
4823
|
-
const network
|
|
4824
|
-
const userAddress = builderFeeSetupContext?.userAddress ??
|
|
4825
|
-
(await this.#walletService.getUserAddressWithDefault());
|
|
5161
|
+
const { network, userAddress } = accountContext;
|
|
4826
5162
|
let transferInfo = null;
|
|
4827
5163
|
if (dexName) {
|
|
4828
5164
|
const orderPrice = ladder === undefined
|
|
4829
5165
|
? effectivePrice
|
|
4830
5166
|
: Math.max(...ladder.prices.map(Number.parseFloat));
|
|
4831
5167
|
const hip3Result = await this.#handleHip3PreOrder({
|
|
5168
|
+
accountContext,
|
|
4832
5169
|
dexName,
|
|
4833
5170
|
symbol: params.symbol,
|
|
4834
5171
|
orderPrice,
|
|
@@ -4840,6 +5177,7 @@ export class HyperLiquidProvider {
|
|
|
4840
5177
|
transferInfo = hip3Result.transferInfo;
|
|
4841
5178
|
}
|
|
4842
5179
|
return {
|
|
5180
|
+
accountContext,
|
|
4843
5181
|
assetId,
|
|
4844
5182
|
szDecimals: assetInfo.szDecimals,
|
|
4845
5183
|
formattedSize,
|
|
@@ -4927,7 +5265,7 @@ export class HyperLiquidProvider {
|
|
|
4927
5265
|
* @returns A promise that resolves to the result.
|
|
4928
5266
|
*/
|
|
4929
5267
|
async #placeTwapOrder(params, context, generation) {
|
|
4930
|
-
const { assetId, formattedSize } = context;
|
|
5268
|
+
const { accountContext, assetId, formattedSize } = context;
|
|
4931
5269
|
const durationMinutes = params.twapDuration;
|
|
4932
5270
|
if (durationMinutes === undefined) {
|
|
4933
5271
|
throw new Error(PERPS_ERROR_CODES.ORDER_TWAP_DURATION_REQUIRED);
|
|
@@ -4945,6 +5283,7 @@ export class HyperLiquidProvider {
|
|
|
4945
5283
|
durationMinutes,
|
|
4946
5284
|
randomize: params.twapRandomize ?? false,
|
|
4947
5285
|
});
|
|
5286
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'TWAP order submission');
|
|
4948
5287
|
const result = await exchangeClient.twapOrder({
|
|
4949
5288
|
twap: {
|
|
4950
5289
|
a: assetId,
|
|
@@ -4974,6 +5313,7 @@ export class HyperLiquidProvider {
|
|
|
4974
5313
|
if (generation !== this.#strategyGeneration) {
|
|
4975
5314
|
let remainsLive = true;
|
|
4976
5315
|
try {
|
|
5316
|
+
await this.#assertRollbackAccountWritable(accountContext);
|
|
4977
5317
|
const cancelResult = await exchangeClient.twapCancel({
|
|
4978
5318
|
a: assetId,
|
|
4979
5319
|
t: running.twapId,
|
|
@@ -5021,7 +5361,7 @@ export class HyperLiquidProvider {
|
|
|
5021
5361
|
* @returns A promise that resolves to the result.
|
|
5022
5362
|
*/
|
|
5023
5363
|
async #placeScaleOrder(params, context, generation) {
|
|
5024
|
-
const { assetId, formattedSize, ladder, builder } = context;
|
|
5364
|
+
const { accountContext, assetId, formattedSize, ladder, builder } = context;
|
|
5025
5365
|
// Built and validated in `#prepareStrategyPlacement`, before anything was
|
|
5026
5366
|
// signed, so what is submitted here is exactly what the minimums were
|
|
5027
5367
|
// applied to.
|
|
@@ -5062,6 +5402,7 @@ export class HyperLiquidProvider {
|
|
|
5062
5402
|
let result;
|
|
5063
5403
|
let thrownBulkOrderError;
|
|
5064
5404
|
try {
|
|
5405
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Scale order submission');
|
|
5065
5406
|
result = await exchangeClient.order({
|
|
5066
5407
|
orders,
|
|
5067
5408
|
grouping: 'na',
|
|
@@ -5078,6 +5419,7 @@ export class HyperLiquidProvider {
|
|
|
5078
5419
|
}
|
|
5079
5420
|
const rawStatuses = result.response?.data?.statuses;
|
|
5080
5421
|
const statuses = Array.isArray(rawStatuses) ? rawStatuses : [];
|
|
5422
|
+
const isMultiSigRequired = this.#recordUnsupportedAccountFromStatuses(statuses, result, accountContext);
|
|
5081
5423
|
const outcomes = Array.from({ length: count }, (_unused, index) => parseScaleBulkOrderStatus(statuses[index]));
|
|
5082
5424
|
const acceptedCount = outcomes.filter((outcome) => outcome?.kind === 'accepted').length;
|
|
5083
5425
|
const everyStatusClassified = outcomes.every((outcome) => outcome !== undefined);
|
|
@@ -5130,6 +5472,19 @@ export class HyperLiquidProvider {
|
|
|
5130
5472
|
...acceptedResult,
|
|
5131
5473
|
});
|
|
5132
5474
|
const cancelNonRejectedOrders = async () => {
|
|
5475
|
+
try {
|
|
5476
|
+
await this.#assertRollbackAccountWritable(accountContext);
|
|
5477
|
+
}
|
|
5478
|
+
catch (error) {
|
|
5479
|
+
this.#deps.debugLogger.log('Scale placement rollback not signed after its account context changed', {
|
|
5480
|
+
error: ensureError(error, 'HyperLiquidProvider.placeScaleOrder.rollback').message,
|
|
5481
|
+
groupId,
|
|
5482
|
+
});
|
|
5483
|
+
return {
|
|
5484
|
+
orderIds: restingChildOrderIds,
|
|
5485
|
+
clientOrderIds: cleanupClientOrderIds,
|
|
5486
|
+
};
|
|
5487
|
+
}
|
|
5133
5488
|
const [orderIds, pendingClientOrderIds] = await Promise.all([
|
|
5134
5489
|
this.#cancelOrderRequests(exchangeClient, restingChildOrderIds.map((orderId) => ({
|
|
5135
5490
|
a: assetId,
|
|
@@ -5219,7 +5574,9 @@ export class HyperLiquidProvider {
|
|
|
5219
5574
|
childOrderIds: [],
|
|
5220
5575
|
});
|
|
5221
5576
|
}
|
|
5222
|
-
throw new Error(
|
|
5577
|
+
throw new Error(isMultiSigRequired
|
|
5578
|
+
? PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED
|
|
5579
|
+
: PERPS_ERROR_CODES.ORDER_REJECTED);
|
|
5223
5580
|
}
|
|
5224
5581
|
this.#scaleOrderGroups.set(groupId, {
|
|
5225
5582
|
symbol: params.symbol,
|
|
@@ -5240,7 +5597,7 @@ export class HyperLiquidProvider {
|
|
|
5240
5597
|
* @returns A promise that resolves to the result.
|
|
5241
5598
|
*/
|
|
5242
5599
|
async #startChaseSession(params, context, generation) {
|
|
5243
|
-
const { assetId, szDecimals, formattedSize, builder } = context;
|
|
5600
|
+
const { accountContext, assetId, szDecimals, formattedSize, builder } = context;
|
|
5244
5601
|
// Captured now, alongside the builder fee and for the same reason: the fee
|
|
5245
5602
|
// resolution behind it is cleared when the caller's `placeOrder` returns,
|
|
5246
5603
|
// which for a chase is before any replacement runs.
|
|
@@ -5288,6 +5645,7 @@ export class HyperLiquidProvider {
|
|
|
5288
5645
|
reduceOnly: params.reduceOnly ?? false,
|
|
5289
5646
|
builder,
|
|
5290
5647
|
marksSubscriptionCloid,
|
|
5648
|
+
accountContext,
|
|
5291
5649
|
exchangeClient: placingClient,
|
|
5292
5650
|
});
|
|
5293
5651
|
break;
|
|
@@ -5321,6 +5679,7 @@ export class HyperLiquidProvider {
|
|
|
5321
5679
|
const intervalMs = params.chaseIntervalMs ?? CHASE_ORDER_CONFIG.DefaultIntervalMs;
|
|
5322
5680
|
const sessionId = generatePerpsId('chase');
|
|
5323
5681
|
const session = {
|
|
5682
|
+
accountContext,
|
|
5324
5683
|
symbol: params.symbol,
|
|
5325
5684
|
assetId,
|
|
5326
5685
|
isBuy: params.isBuy,
|
|
@@ -5455,9 +5814,12 @@ export class HyperLiquidProvider {
|
|
|
5455
5814
|
* than looked up here so a first placement can keep the instance it signed
|
|
5456
5815
|
* with, which is the only one that can take the order back once `disconnect`
|
|
5457
5816
|
* has dropped the service's reference.
|
|
5817
|
+
* @param params.accountContext - Account/network/lifecycle captured by the
|
|
5818
|
+
* action that owns the chase.
|
|
5458
5819
|
* @returns The resting order's exchange ID.
|
|
5459
5820
|
*/
|
|
5460
5821
|
async #restChaseOrder(params) {
|
|
5822
|
+
await this.#assertNoKnownUnsupportedAccount(params.accountContext, 'Chase order submission');
|
|
5461
5823
|
const result = await params.exchangeClient.order({
|
|
5462
5824
|
orders: this.#applySubscriptionCloid([
|
|
5463
5825
|
{
|
|
@@ -5498,6 +5860,15 @@ export class HyperLiquidProvider {
|
|
|
5498
5860
|
this.#chaseTickQueue = this.#chaseTickQueue
|
|
5499
5861
|
.then(() => this.#runChaseTick(sessionId))
|
|
5500
5862
|
.catch(async (error) => {
|
|
5863
|
+
const failedSession = this.#chaseSessions.get(sessionId);
|
|
5864
|
+
const accountUnsupported = failedSession
|
|
5865
|
+
? this.#recordUnsupportedAccountFromError(error, failedSession.accountContext)
|
|
5866
|
+
: false;
|
|
5867
|
+
if (accountUnsupported) {
|
|
5868
|
+
this.#stopChaseSession(sessionId);
|
|
5869
|
+
this.#deps.debugLogger.log('Chase stopped after account support correction', { sessionId });
|
|
5870
|
+
return;
|
|
5871
|
+
}
|
|
5501
5872
|
// A rejected agent is dropped so the next tick asks for another.
|
|
5502
5873
|
await this.#evictRejectedAgent(error);
|
|
5503
5874
|
// Resolve the shared queue after every failure. Otherwise one
|
|
@@ -5812,8 +6183,7 @@ export class HyperLiquidProvider {
|
|
|
5812
6183
|
reduceOnly: session.reduceOnly,
|
|
5813
6184
|
builder: session.builder,
|
|
5814
6185
|
marksSubscriptionCloid: session.marksSubscriptionCloid,
|
|
5815
|
-
|
|
5816
|
-
// the right one; only the first placement has a teardown to survive.
|
|
6186
|
+
accountContext: session.accountContext,
|
|
5817
6187
|
exchangeClient: this.#clientService.getExchangeClient(),
|
|
5818
6188
|
});
|
|
5819
6189
|
session.size = remaining;
|
|
@@ -6045,6 +6415,9 @@ export class HyperLiquidProvider {
|
|
|
6045
6415
|
}
|
|
6046
6416
|
// Looked up only once there is something to cancel, so a session with
|
|
6047
6417
|
// nothing resting still answers on a provider whose client is already gone.
|
|
6418
|
+
if (!placingClient) {
|
|
6419
|
+
await this.#assertNoKnownUnsupportedAccount(session.accountContext, 'Chase cancellation');
|
|
6420
|
+
}
|
|
6048
6421
|
const exchangeClient = placingClient ?? this.#clientService.getExchangeClient();
|
|
6049
6422
|
let result;
|
|
6050
6423
|
try {
|
|
@@ -6053,6 +6426,7 @@ export class HyperLiquidProvider {
|
|
|
6053
6426
|
});
|
|
6054
6427
|
}
|
|
6055
6428
|
catch (error) {
|
|
6429
|
+
this.#recordUnsupportedAccountFromError(error, session.accountContext);
|
|
6056
6430
|
const message = ensureError(error, 'HyperLiquidProvider.cancelChaseChild').message.toLowerCase();
|
|
6057
6431
|
if (ALREADY_GONE_CANCEL_MARKERS.some((marker) => message.includes(marker))) {
|
|
6058
6432
|
return CancelChildOutcome.Gone;
|
|
@@ -6060,6 +6434,9 @@ export class HyperLiquidProvider {
|
|
|
6060
6434
|
throw error;
|
|
6061
6435
|
}
|
|
6062
6436
|
const status = result.response?.data?.statuses?.[0];
|
|
6437
|
+
if (isStatusObject(status) && typeof status.error === 'string') {
|
|
6438
|
+
this.#recordUnsupportedAccountFromError(createStatusError(status.error, result), session.accountContext);
|
|
6439
|
+
}
|
|
6063
6440
|
const signerFailure = await this.#classifyStatusSignerFailure([status], result);
|
|
6064
6441
|
if (signerFailure) {
|
|
6065
6442
|
throw signerFailure;
|
|
@@ -6258,7 +6635,16 @@ export class HyperLiquidProvider {
|
|
|
6258
6635
|
}
|
|
6259
6636
|
let { rebalancePromise } = trackedOrder;
|
|
6260
6637
|
if (!rebalancePromise) {
|
|
6261
|
-
|
|
6638
|
+
const { hip3Transfer } = trackedOrder;
|
|
6639
|
+
rebalancePromise = this.#handleHip3PostOrderRebalance({
|
|
6640
|
+
...hip3Transfer,
|
|
6641
|
+
accountContext: {
|
|
6642
|
+
...hip3Transfer.accountContext,
|
|
6643
|
+
// TWAP tracking deliberately survives reconnects. Keep its stable
|
|
6644
|
+
// account/network scope, but bind the cleanup write to this lifecycle.
|
|
6645
|
+
lifecycleGeneration: this.#lifecycleGeneration,
|
|
6646
|
+
},
|
|
6647
|
+
});
|
|
6262
6648
|
trackedOrder.rebalancePromise = rebalancePromise;
|
|
6263
6649
|
}
|
|
6264
6650
|
try {
|
|
@@ -6511,9 +6897,10 @@ export class HyperLiquidProvider {
|
|
|
6511
6897
|
if (!/^\d+$/u.test(params.orderId) || !Number.isSafeInteger(twapId)) {
|
|
6512
6898
|
throw new Error(PERPS_ERROR_CODES.ORDER_STRATEGY_HANDLE_UNKNOWN);
|
|
6513
6899
|
}
|
|
6514
|
-
await this.#ensureReadyForTrading({
|
|
6515
|
-
|
|
6516
|
-
|
|
6900
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
6901
|
+
requiresBuilderFee: false,
|
|
6902
|
+
});
|
|
6903
|
+
const { network, userAddress } = accountContext;
|
|
6517
6904
|
const trackingKey = getTwapOrderScopeKey({
|
|
6518
6905
|
network,
|
|
6519
6906
|
userAddress,
|
|
@@ -6549,6 +6936,7 @@ export class HyperLiquidProvider {
|
|
|
6549
6936
|
symbol: params.symbol,
|
|
6550
6937
|
dexName: parseAssetName(params.symbol).dex,
|
|
6551
6938
|
});
|
|
6939
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'TWAP cancellation');
|
|
6552
6940
|
const result = await this.#clientService.getExchangeClient().twapCancel({
|
|
6553
6941
|
a: assetId,
|
|
6554
6942
|
t: twapId,
|
|
@@ -6565,7 +6953,7 @@ export class HyperLiquidProvider {
|
|
|
6565
6953
|
const rawError = isStatusObject(status) && typeof status.error === 'string'
|
|
6566
6954
|
? status.error
|
|
6567
6955
|
: 'TWAP cancellation failed';
|
|
6568
|
-
return createErrorResult(await this.#mapStatusError(rawError, result), {
|
|
6956
|
+
return createErrorResult(await this.#mapStatusError(rawError, result, accountContext), {
|
|
6569
6957
|
success: false,
|
|
6570
6958
|
orderId: params.orderId,
|
|
6571
6959
|
});
|
|
@@ -6581,7 +6969,9 @@ export class HyperLiquidProvider {
|
|
|
6581
6969
|
if (!group) {
|
|
6582
6970
|
throw new Error(PERPS_ERROR_CODES.ORDER_STRATEGY_HANDLE_UNKNOWN);
|
|
6583
6971
|
}
|
|
6584
|
-
await this.#ensureReadyForTrading({
|
|
6972
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
6973
|
+
requiresBuilderFee: false,
|
|
6974
|
+
});
|
|
6585
6975
|
const assetId = await this.#getAssetIdWithRepair({
|
|
6586
6976
|
symbol: group.symbol,
|
|
6587
6977
|
dexName: parseAssetName(group.symbol).dex,
|
|
@@ -6595,9 +6985,14 @@ export class HyperLiquidProvider {
|
|
|
6595
6985
|
asset: assetId,
|
|
6596
6986
|
cloid: clientOrderId,
|
|
6597
6987
|
}));
|
|
6988
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Scale cancellation');
|
|
6598
6989
|
const [orderCancellation, cloidCancellation] = await Promise.all([
|
|
6599
|
-
this.#cancelOrderRequestBatch(exchangeClient, cancelRequests)
|
|
6600
|
-
|
|
6990
|
+
this.#cancelOrderRequestBatch(exchangeClient, cancelRequests, (error) => {
|
|
6991
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
6992
|
+
}),
|
|
6993
|
+
this.#cancelOrderCloidRequestBatch(exchangeClient, cancelByCloidRequests, (error) => {
|
|
6994
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
6995
|
+
}),
|
|
6601
6996
|
]);
|
|
6602
6997
|
const remaining = orderCancellation.remainingOrderIds.map(String);
|
|
6603
6998
|
const { remainingClientOrderIds } = cloidCancellation;
|
|
@@ -6697,7 +7092,18 @@ export class HyperLiquidProvider {
|
|
|
6697
7092
|
}
|
|
6698
7093
|
return { success: true, orderId: params.orderId };
|
|
6699
7094
|
}
|
|
6700
|
-
|
|
7095
|
+
this.#assertProviderLifecycleCurrent(session.accountContext.lifecycleGeneration, 'Chase cancellation setup');
|
|
7096
|
+
const currentAddress = await this.#walletService.getUserAddressWithDefault();
|
|
7097
|
+
if (currentAddress.toLowerCase() !==
|
|
7098
|
+
session.accountContext.userAddress.toLowerCase() ||
|
|
7099
|
+
(this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet') !==
|
|
7100
|
+
session.accountContext.network) {
|
|
7101
|
+
throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
|
|
7102
|
+
}
|
|
7103
|
+
await this.#ensureReadyForTrading({
|
|
7104
|
+
requiresBuilderFee: false,
|
|
7105
|
+
accountContext: session.accountContext,
|
|
7106
|
+
});
|
|
6701
7107
|
// Only a refusal leaves an order behind. A child that had already filled or
|
|
6702
7108
|
// been cancelled is reported as a rejection too, but nothing of it is
|
|
6703
7109
|
// resting — treating that as a failure would pin the handle open forever on
|
|
@@ -6741,11 +7147,11 @@ export class HyperLiquidProvider {
|
|
|
6741
7147
|
*
|
|
6742
7148
|
* @param exchangeClient - Client that owns the orders.
|
|
6743
7149
|
* @param requests - Venue cancel-by-CLOID requests.
|
|
7150
|
+
* @param onError - Optional operation-specific failure classifier.
|
|
6744
7151
|
* @returns Client order IDs that may still be resting.
|
|
6745
7152
|
*/
|
|
6746
|
-
async #cancelOrderCloidRequests(exchangeClient, requests) {
|
|
6747
|
-
return (await this.#cancelOrderCloidRequestBatch(exchangeClient, requests))
|
|
6748
|
-
.remainingClientOrderIds;
|
|
7153
|
+
async #cancelOrderCloidRequests(exchangeClient, requests, onError) {
|
|
7154
|
+
return (await this.#cancelOrderCloidRequestBatch(exchangeClient, requests, onError)).remainingClientOrderIds;
|
|
6749
7155
|
}
|
|
6750
7156
|
/**
|
|
6751
7157
|
* Cancel pending orders by client order ID, reporting a signer that could
|
|
@@ -6753,14 +7159,20 @@ export class HyperLiquidProvider {
|
|
|
6753
7159
|
*
|
|
6754
7160
|
* @param exchangeClient - Client that owns the orders.
|
|
6755
7161
|
* @param requests - Venue cancel-by-CLOID requests.
|
|
7162
|
+
* @param onError - Optional operation-specific failure classifier.
|
|
6756
7163
|
* @returns Client order IDs that may still be resting, and the signer
|
|
6757
7164
|
* failure when the signer could not sign the request.
|
|
6758
7165
|
*/
|
|
6759
|
-
async #cancelOrderCloidRequestBatch(exchangeClient, requests) {
|
|
7166
|
+
async #cancelOrderCloidRequestBatch(exchangeClient, requests, onError) {
|
|
6760
7167
|
if (requests.length === 0) {
|
|
6761
7168
|
return { remainingClientOrderIds: [] };
|
|
6762
7169
|
}
|
|
6763
7170
|
const classifyStatuses = async (statuses, answer) => {
|
|
7171
|
+
for (const status of statuses) {
|
|
7172
|
+
if (isStatusObject(status) && typeof status.error === 'string') {
|
|
7173
|
+
await onError?.(createStatusError(status.error, answer));
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
6764
7176
|
// A signer failure is classified (and reported) once for the request;
|
|
6765
7177
|
// entries the venue cancelled are not resting either way.
|
|
6766
7178
|
const signerFailure = await this.#classifyStatusSignerFailure(statuses, answer);
|
|
@@ -6784,6 +7196,7 @@ export class HyperLiquidProvider {
|
|
|
6784
7196
|
return await classifyStatuses(statuses, result);
|
|
6785
7197
|
}
|
|
6786
7198
|
catch (error) {
|
|
7199
|
+
await onError?.(error);
|
|
6787
7200
|
// The SDK throws when any entry failed, with every entry's status.
|
|
6788
7201
|
const statuses = getCancelStatusesFromError(error, requests.length);
|
|
6789
7202
|
if (statuses) {
|
|
@@ -6824,6 +7237,11 @@ export class HyperLiquidProvider {
|
|
|
6824
7237
|
};
|
|
6825
7238
|
}
|
|
6826
7239
|
const classifyStatuses = async (statuses, answer) => {
|
|
7240
|
+
for (const status of statuses) {
|
|
7241
|
+
if (isStatusObject(status) && typeof status.error === 'string') {
|
|
7242
|
+
await onError?.(createStatusError(status.error, answer));
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
6827
7245
|
// A signer failure is classified (and reported) once for the request;
|
|
6828
7246
|
// entries the venue cancelled still count as cancelled.
|
|
6829
7247
|
const signerFailure = await this.#classifyStatusSignerFailure(statuses, answer);
|
|
@@ -6969,9 +7387,11 @@ export class HyperLiquidProvider {
|
|
|
6969
7387
|
* Apply both signer invalidation and account-shape classification to writes.
|
|
6970
7388
|
*
|
|
6971
7389
|
* @param error - Failed protection write.
|
|
7390
|
+
* @param accountContext - Account and network that authorized the write.
|
|
6972
7391
|
* @returns Actionable error retaining the venue message when unmapped.
|
|
6973
7392
|
*/
|
|
6974
|
-
async #classifyProtectionWriteError(error) {
|
|
7393
|
+
async #classifyProtectionWriteError(error, accountContext) {
|
|
7394
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
6975
7395
|
return ((await this.#classifySignerFailure(error)) ??
|
|
6976
7396
|
(isHyperLiquidMultiSigRequiredError(error)
|
|
6977
7397
|
? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
|
|
@@ -7378,19 +7798,30 @@ export class HyperLiquidProvider {
|
|
|
7378
7798
|
// Every refusal is behind us, so shared trading readiness can run now.
|
|
7379
7799
|
// HyperLiquid's modify action has no builder field and must not request a
|
|
7380
7800
|
// builder-fee approval.
|
|
7381
|
-
await this.#ensureReadyForTrading({
|
|
7801
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
7802
|
+
requiresBuilderFee: false,
|
|
7803
|
+
});
|
|
7382
7804
|
// Submit modification via SDK. The cloid is deliberately left unmarked:
|
|
7383
7805
|
// `modify` carries no builder field, as the readiness call above records,
|
|
7384
7806
|
// so no MetaMask fee is charged on this action and marking it would tell
|
|
7385
7807
|
// the fill fan-out a reduction applied to an order that paid nothing.
|
|
7386
7808
|
// The replacement inherits the resting order's own attribution.
|
|
7809
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Order modification');
|
|
7387
7810
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7811
|
+
try {
|
|
7812
|
+
const result = await exchangeClient.modify({
|
|
7813
|
+
oid: typeof params.orderId === 'string'
|
|
7814
|
+
? params.orderId
|
|
7815
|
+
: params.orderId,
|
|
7816
|
+
order: newOrder,
|
|
7817
|
+
});
|
|
7818
|
+
if (result.status !== 'ok') {
|
|
7819
|
+
throw new Error(`Order modification failed: ${JSON.stringify(result)}`);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
catch (error) {
|
|
7823
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
7824
|
+
throw error;
|
|
7394
7825
|
}
|
|
7395
7826
|
// `params.orderId` is the order that was just REPLACED, so returning it
|
|
7396
7827
|
// as OrderResult.orderId (documented as the exchange order ID) names an
|
|
@@ -7450,6 +7881,7 @@ export class HyperLiquidProvider {
|
|
|
7450
7881
|
// current child instead of leaving a replacement chasing behind the user.
|
|
7451
7882
|
const owningChase = [...this.#chaseSessions.entries()].find(([, session]) => session.orderId === params.orderId ||
|
|
7452
7883
|
session.replacingOrderId === params.orderId);
|
|
7884
|
+
let accountContext;
|
|
7453
7885
|
try {
|
|
7454
7886
|
if (owningChase) {
|
|
7455
7887
|
this.#stopChaseSession(owningChase[0]);
|
|
@@ -7472,12 +7904,15 @@ export class HyperLiquidProvider {
|
|
|
7472
7904
|
if (!coinValidation.isValid) {
|
|
7473
7905
|
throw new Error(coinValidation.error);
|
|
7474
7906
|
}
|
|
7475
|
-
await this.#ensureReadyForTrading({
|
|
7476
|
-
|
|
7907
|
+
({ accountContext } = await this.#ensureReadyForTrading({
|
|
7908
|
+
requiresBuilderFee: false,
|
|
7909
|
+
}));
|
|
7477
7910
|
const asset = await this.#getAssetIdWithRepair({
|
|
7478
7911
|
symbol: params.symbol,
|
|
7479
7912
|
dexName: parseAssetName(params.symbol).dex,
|
|
7480
7913
|
});
|
|
7914
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Order cancellation');
|
|
7915
|
+
const exchangeClient = this.#clientService.getExchangeClient();
|
|
7481
7916
|
const result = await exchangeClient.cancel({
|
|
7482
7917
|
cancels: [
|
|
7483
7918
|
{
|
|
@@ -7500,12 +7935,15 @@ export class HyperLiquidProvider {
|
|
|
7500
7935
|
const rawError = isStatusObject(status) && typeof status.error === 'string'
|
|
7501
7936
|
? status.error
|
|
7502
7937
|
: 'Order cancellation failed';
|
|
7503
|
-
return createErrorResult(await this.#mapStatusError(rawError, result), {
|
|
7938
|
+
return createErrorResult(await this.#mapStatusError(rawError, result, accountContext), {
|
|
7504
7939
|
success: false,
|
|
7505
7940
|
orderId: params.orderId,
|
|
7506
7941
|
});
|
|
7507
7942
|
}
|
|
7508
7943
|
catch (error) {
|
|
7944
|
+
if (accountContext) {
|
|
7945
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
7946
|
+
}
|
|
7509
7947
|
const mappedError = await this.#reportWriteError(error, 'cancelOrder', {
|
|
7510
7948
|
orderId: params.orderId,
|
|
7511
7949
|
coin: params.symbol,
|
|
@@ -7577,8 +8015,9 @@ export class HyperLiquidProvider {
|
|
|
7577
8015
|
if (ordinaryOrders.length > 0) {
|
|
7578
8016
|
// Cancellation carries no builder context, so it must not prompt for a
|
|
7579
8017
|
// fee approval that the action cannot use.
|
|
7580
|
-
await this.#ensureReadyForTrading({
|
|
7581
|
-
|
|
8018
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
8019
|
+
requiresBuilderFee: false,
|
|
8020
|
+
});
|
|
7582
8021
|
const cancelRequests = await Promise.all(ordinaryOrders.map(async ({ order }) => {
|
|
7583
8022
|
const asset = await this.#getAssetIdWithRepair({
|
|
7584
8023
|
symbol: order.symbol,
|
|
@@ -7589,6 +8028,8 @@ export class HyperLiquidProvider {
|
|
|
7589
8028
|
o: parseInt(order.orderId, 10),
|
|
7590
8029
|
};
|
|
7591
8030
|
}));
|
|
8031
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch order cancellation');
|
|
8032
|
+
const exchangeClient = this.#clientService.getExchangeClient();
|
|
7592
8033
|
let statuses;
|
|
7593
8034
|
let answer;
|
|
7594
8035
|
try {
|
|
@@ -7623,6 +8064,7 @@ export class HyperLiquidProvider {
|
|
|
7623
8064
|
: undefined;
|
|
7624
8065
|
let error = PERPS_ERROR_CODES.BATCH_CANCEL_FAILED;
|
|
7625
8066
|
if (statusError) {
|
|
8067
|
+
this.#recordUnsupportedAccountFromError(statusError, accountContext);
|
|
7626
8068
|
error =
|
|
7627
8069
|
signerFailure && (await this.#isSignerFailure(statusError))
|
|
7628
8070
|
? signerFailure.message
|
|
@@ -7666,11 +8108,14 @@ export class HyperLiquidProvider {
|
|
|
7666
8108
|
async closePositions(params) {
|
|
7667
8109
|
// Declare outside try block so it's accessible in catch block
|
|
7668
8110
|
let positionsToClose = [];
|
|
8111
|
+
let accountContext;
|
|
7669
8112
|
const unavailableResults = [];
|
|
7670
8113
|
try {
|
|
7671
8114
|
// Batch preparation needs trading readiness but not builder approval yet.
|
|
7672
8115
|
// The provider-owned market policy is resolved from the positions below.
|
|
7673
|
-
await this.#ensureReadyForTrading({
|
|
8116
|
+
({ accountContext } = await this.#ensureReadyForTrading({
|
|
8117
|
+
requiresBuilderFee: false,
|
|
8118
|
+
}));
|
|
7674
8119
|
// Selected symbols only need the DEXes they belong to. Loading every
|
|
7675
8120
|
// market group would refuse a BTC close when an unrelated HIP-3 DEX
|
|
7676
8121
|
// cannot be read. closeAll / empty symbols still need a complete
|
|
@@ -7741,8 +8186,6 @@ export class HyperLiquidProvider {
|
|
|
7741
8186
|
results: unavailableResults,
|
|
7742
8187
|
};
|
|
7743
8188
|
}
|
|
7744
|
-
// Get exchange client for order submission
|
|
7745
|
-
const exchangeClient = this.#clientService.getExchangeClient();
|
|
7746
8189
|
// Pre-fetch meta for all unique DEXs to avoid N API calls in loop
|
|
7747
8190
|
const uniqueDexs = [
|
|
7748
8191
|
...new Set(positionsToClose.map((pos) => parseAssetName(pos.symbol).dex ?? 'main')),
|
|
@@ -7857,10 +8300,16 @@ export class HyperLiquidProvider {
|
|
|
7857
8300
|
// policy charges, preserving deterministic behavior for future policies
|
|
7858
8301
|
// that vary by market or route.
|
|
7859
8302
|
const chargesMetamaskBuilderFee = feePolicyContexts.some((context) => this.#resolveOrderFeePolicy(context).chargesMetamaskBuilderFee);
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
8303
|
+
let builder;
|
|
8304
|
+
if (chargesMetamaskBuilderFee) {
|
|
8305
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close setup');
|
|
8306
|
+
const builderFeeSetupContext = await this.#ensureBuilderFeeSetup(undefined, {}, accountContext);
|
|
8307
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close setup');
|
|
8308
|
+
builder = await this.#getBuilderOrderContext(builderFeeSetupContext);
|
|
8309
|
+
}
|
|
7863
8310
|
// Single batch API call
|
|
8311
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Batch close submission');
|
|
8312
|
+
const exchangeClient = this.#clientService.getExchangeClient();
|
|
7864
8313
|
const result = await exchangeClient.order({
|
|
7865
8314
|
orders: this.#applySubscriptionCloid(orders),
|
|
7866
8315
|
grouping: 'na',
|
|
@@ -7868,6 +8317,11 @@ export class HyperLiquidProvider {
|
|
|
7868
8317
|
});
|
|
7869
8318
|
// Parse response statuses (one per order)
|
|
7870
8319
|
const { statuses } = result.response.data;
|
|
8320
|
+
for (const status of statuses) {
|
|
8321
|
+
if (hasProperty(status, 'error') && typeof status.error === 'string') {
|
|
8322
|
+
this.#recordUnsupportedAccountFromError(createStatusError(status.error, result), accountContext);
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
7871
8325
|
const successCount = statuses.filter((stat) => isStatusObject(stat) &&
|
|
7872
8326
|
(hasProperty(stat, 'filled') || hasProperty(stat, 'resting'))).length;
|
|
7873
8327
|
const failureCount = statuses.length -
|
|
@@ -7886,6 +8340,7 @@ export class HyperLiquidProvider {
|
|
|
7886
8340
|
this.#deps.debugLogger.log('Position closed successfully, initiating manual auto-transfer back', { symbol: orderedPositions[i].symbol, freedMargin });
|
|
7887
8341
|
// Non-blocking: Transfer freed margin back to main DEX
|
|
7888
8342
|
await this.#autoTransferBackAfterClose({
|
|
8343
|
+
accountContext,
|
|
7889
8344
|
sourceDex,
|
|
7890
8345
|
freedMargin,
|
|
7891
8346
|
});
|
|
@@ -7921,6 +8376,9 @@ export class HyperLiquidProvider {
|
|
|
7921
8376
|
};
|
|
7922
8377
|
}
|
|
7923
8378
|
catch (error) {
|
|
8379
|
+
if (accountContext) {
|
|
8380
|
+
this.#recordUnsupportedAccountFromError(error, accountContext);
|
|
8381
|
+
}
|
|
7924
8382
|
const signerFailure = await this.#handleSignerFailure(error, 'closePositions', {
|
|
7925
8383
|
positionCount: positionsToClose.length,
|
|
7926
8384
|
});
|
|
@@ -8004,6 +8462,11 @@ export class HyperLiquidProvider {
|
|
|
8004
8462
|
const lifecycle = this.#lifecycleGeneration;
|
|
8005
8463
|
const isTestnet = this.#clientService.isTestnetMode();
|
|
8006
8464
|
const userAddress = await this.#walletService.getUserAddressWithDefault();
|
|
8465
|
+
const accountContext = {
|
|
8466
|
+
lifecycleGeneration: lifecycle,
|
|
8467
|
+
network: isTestnet ? 'testnet' : 'mainnet',
|
|
8468
|
+
userAddress,
|
|
8469
|
+
};
|
|
8007
8470
|
const assertScope = async () => {
|
|
8008
8471
|
const current = await this.#walletService.getUserAddressWithDefault();
|
|
8009
8472
|
this.#assertProviderLifecycleCurrent(lifecycle, 'updatePositionTPSL');
|
|
@@ -8399,12 +8862,18 @@ export class HyperLiquidProvider {
|
|
|
8399
8862
|
const requiresBuilderFee = replacementChargesMetamaskBuilderFee || rollbackRequiresBuilderFee;
|
|
8400
8863
|
// Approval and builder-context resolution both finish before the
|
|
8401
8864
|
// pre-cancel. A failure here therefore leaves the old protection intact.
|
|
8402
|
-
|
|
8865
|
+
await assertScope();
|
|
8866
|
+
const tradingActionContext = requiresBuilderFee
|
|
8403
8867
|
? await this.#ensureReadyForTrading({
|
|
8404
8868
|
requiresBuilderFee: true,
|
|
8405
8869
|
builderFeeApprovalFailureCode: PERPS_ERROR_CODES.TPSL_UPDATE_FAILED,
|
|
8870
|
+
accountContext,
|
|
8406
8871
|
})
|
|
8407
|
-
: await this.#ensureReadyForTrading({
|
|
8872
|
+
: await this.#ensureReadyForTrading({
|
|
8873
|
+
requiresBuilderFee: false,
|
|
8874
|
+
accountContext,
|
|
8875
|
+
});
|
|
8876
|
+
const { builderFeeSetupContext } = tradingActionContext;
|
|
8408
8877
|
const builderOrderContext = builderFeeSetupContext
|
|
8409
8878
|
? await this.#getBuilderOrderContext(builderFeeSetupContext)
|
|
8410
8879
|
: undefined;
|
|
@@ -8449,9 +8918,10 @@ export class HyperLiquidProvider {
|
|
|
8449
8918
|
builderOrderContext && { builder: builderOrderContext }),
|
|
8450
8919
|
});
|
|
8451
8920
|
statuses = result.response?.data?.statuses ?? [];
|
|
8921
|
+
this.#recordUnsupportedAccountFromStatuses(statuses, result, accountContext);
|
|
8452
8922
|
}
|
|
8453
8923
|
catch (error) {
|
|
8454
|
-
const failure = await this.#classifyProtectionWriteError(error);
|
|
8924
|
+
const failure = await this.#classifyProtectionWriteError(error, accountContext);
|
|
8455
8925
|
restorationFailure = failure;
|
|
8456
8926
|
statuses =
|
|
8457
8927
|
this.#readProtectionErrorStatuses(error) ??
|
|
@@ -8505,10 +8975,10 @@ export class HyperLiquidProvider {
|
|
|
8505
8975
|
await assertCurrentPosition();
|
|
8506
8976
|
const oldCancellation = await this.#cancelOrderRequestBatch(exchangeClient, cancelRequests, async (error) => {
|
|
8507
8977
|
if (cancelGuardFailure !== undefined) {
|
|
8508
|
-
throw await this.#classifyProtectionWriteError(cancelGuardFailure);
|
|
8978
|
+
throw await this.#classifyProtectionWriteError(cancelGuardFailure, accountContext);
|
|
8509
8979
|
}
|
|
8510
8980
|
if (isHyperLiquidMultiSigRequiredError(error)) {
|
|
8511
|
-
throw await this.#classifyProtectionWriteError(error);
|
|
8981
|
+
throw await this.#classifyProtectionWriteError(error, accountContext);
|
|
8512
8982
|
}
|
|
8513
8983
|
});
|
|
8514
8984
|
// The signer could not sign the cancel. With nothing cancelled the old
|
|
@@ -8606,11 +9076,12 @@ export class HyperLiquidProvider {
|
|
|
8606
9076
|
builderOrderContext && { builder: builderOrderContext }),
|
|
8607
9077
|
});
|
|
8608
9078
|
placementStatuses = result.response?.data?.statuses ?? [];
|
|
9079
|
+
this.#recordUnsupportedAccountFromStatuses(placementStatuses, result, accountContext);
|
|
8609
9080
|
responseAccepted = result.status === 'ok';
|
|
8610
9081
|
}
|
|
8611
9082
|
catch (error) {
|
|
8612
9083
|
// Drop and report a rejected agent before restoration signs.
|
|
8613
|
-
placementFailure = await this.#classifyProtectionWriteError(error);
|
|
9084
|
+
placementFailure = await this.#classifyProtectionWriteError(error, accountContext);
|
|
8614
9085
|
await assertScope();
|
|
8615
9086
|
const statuses = this.#readProtectionErrorStatuses(error);
|
|
8616
9087
|
if (statuses !== undefined) {
|
|
@@ -8726,7 +9197,7 @@ export class HyperLiquidProvider {
|
|
|
8726
9197
|
error.message === PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)) {
|
|
8727
9198
|
// Old protection has already changed. Classification must not
|
|
8728
9199
|
// throw away this batch's still-live replacement identities.
|
|
8729
|
-
cleanupFailure = await this.#classifyProtectionWriteError(error);
|
|
9200
|
+
cleanupFailure = await this.#classifyProtectionWriteError(error, accountContext);
|
|
8730
9201
|
}
|
|
8731
9202
|
});
|
|
8732
9203
|
await assertScope();
|
|
@@ -8804,7 +9275,9 @@ export class HyperLiquidProvider {
|
|
|
8804
9275
|
this.#deps.debugLogger.log('Closing position:', params);
|
|
8805
9276
|
// The delegated placeOrder call resolves the builder-fee policy for the
|
|
8806
9277
|
// concrete close order after validation.
|
|
8807
|
-
await this.#ensureReadyForTrading({
|
|
9278
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
9279
|
+
requiresBuilderFee: false,
|
|
9280
|
+
});
|
|
8808
9281
|
// A caller snapshot is never authoritative for a reduce-only close. Use
|
|
8809
9282
|
// the current DEX slice or its HTTP fallback, and fail closed if neither
|
|
8810
9283
|
// answers. This keeps the size and side on one freshness contract across
|
|
@@ -8883,7 +9356,8 @@ export class HyperLiquidProvider {
|
|
|
8883
9356
|
// position.
|
|
8884
9357
|
const isFullClose = closeSizeNum >= absPositionSize;
|
|
8885
9358
|
// Execute position close with consistent slippage handling
|
|
8886
|
-
|
|
9359
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Position close');
|
|
9360
|
+
const result = await this.#placeOrder({
|
|
8887
9361
|
symbol: params.symbol,
|
|
8888
9362
|
isBuy,
|
|
8889
9363
|
size: closeSize,
|
|
@@ -8903,7 +9377,7 @@ export class HyperLiquidProvider {
|
|
|
8903
9377
|
usdAmount: isFullClose ? undefined : params.usdAmount,
|
|
8904
9378
|
priceAtCalculation: params.priceAtCalculation,
|
|
8905
9379
|
maxSlippageBps: params.maxSlippageBps,
|
|
8906
|
-
}, 0, false);
|
|
9380
|
+
}, 0, false, accountContext);
|
|
8907
9381
|
// Return freed margin using native abstraction or programmatic transfer
|
|
8908
9382
|
if (result.success &&
|
|
8909
9383
|
isHip3Position &&
|
|
@@ -8912,6 +9386,7 @@ export class HyperLiquidProvider {
|
|
|
8912
9386
|
this.#deps.debugLogger.log('Position closed successfully, initiating manual auto-transfer back');
|
|
8913
9387
|
// Non-blocking: Transfer freed margin back to main DEX
|
|
8914
9388
|
await this.#autoTransferBackAfterClose({
|
|
9389
|
+
accountContext,
|
|
8915
9390
|
sourceDex: hip3Dex,
|
|
8916
9391
|
freedMargin,
|
|
8917
9392
|
});
|
|
@@ -8954,11 +9429,13 @@ export class HyperLiquidProvider {
|
|
|
8954
9429
|
* - ntli: Amount in micro-units (amount * 1e6)
|
|
8955
9430
|
*/
|
|
8956
9431
|
async updateMargin(params) {
|
|
9432
|
+
let accountContext;
|
|
8957
9433
|
try {
|
|
8958
9434
|
this.#deps.debugLogger.log('Updating position margin:', params);
|
|
8959
9435
|
const { symbol, amount } = params;
|
|
8960
9436
|
// Ensure provider is ready
|
|
8961
9437
|
await this.#ensureReady();
|
|
9438
|
+
accountContext = await this.#assertAccountSupported();
|
|
8962
9439
|
// Use the target DEX's current slice or one targeted HTTP read before
|
|
8963
9440
|
// deriving the position side.
|
|
8964
9441
|
const positions = await this.#getPositionsForOperation(parseAssetName(symbol).dex ?? '');
|
|
@@ -8994,6 +9471,7 @@ export class HyperLiquidProvider {
|
|
|
8994
9471
|
}
|
|
8995
9472
|
}
|
|
8996
9473
|
// Call SDK to update isolated margin
|
|
9474
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Margin update');
|
|
8997
9475
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
8998
9476
|
const result = await exchangeClient.updateIsolatedMargin({
|
|
8999
9477
|
asset: assetId,
|
|
@@ -9009,13 +9487,18 @@ export class HyperLiquidProvider {
|
|
|
9009
9487
|
};
|
|
9010
9488
|
}
|
|
9011
9489
|
catch (error) {
|
|
9490
|
+
const isMultiSigRequired = accountContext
|
|
9491
|
+
? this.#recordUnsupportedAccountFromError(error, accountContext)
|
|
9492
|
+
: false;
|
|
9012
9493
|
const signerFailure = await this.#handleSignerFailure(error, 'updateMargin', {
|
|
9013
9494
|
symbol: params.symbol,
|
|
9014
9495
|
});
|
|
9015
9496
|
if (signerFailure) {
|
|
9016
9497
|
return { success: false, error: signerFailure.message };
|
|
9017
9498
|
}
|
|
9018
|
-
const safeError =
|
|
9499
|
+
const safeError = isMultiSigRequired
|
|
9500
|
+
? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
|
|
9501
|
+
: ensureError(error, 'HyperLiquidProvider.updateMargin');
|
|
9019
9502
|
this.#deps.logger.error(safeError, this.#getErrorContext('updateMargin', {
|
|
9020
9503
|
symbol: params.symbol,
|
|
9021
9504
|
amount: params.amount,
|
|
@@ -9904,6 +10387,22 @@ export class HyperLiquidProvider {
|
|
|
9904
10387
|
return [];
|
|
9905
10388
|
}
|
|
9906
10389
|
}
|
|
10390
|
+
/**
|
|
10391
|
+
* Return whether the active wallet can submit Hyperliquid actions.
|
|
10392
|
+
*
|
|
10393
|
+
* @returns The provider-owned account support result.
|
|
10394
|
+
*/
|
|
10395
|
+
async getAccountSupport() {
|
|
10396
|
+
await this.#ensureReady();
|
|
10397
|
+
const context = {
|
|
10398
|
+
lifecycleGeneration: this.#lifecycleGeneration,
|
|
10399
|
+
network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
|
|
10400
|
+
userAddress: await this.#walletService.getUserAddressWithDefault(),
|
|
10401
|
+
};
|
|
10402
|
+
const support = await this.#getAccountSupportForContext(context);
|
|
10403
|
+
await this.#assertAccountContextCurrent(context, 'Account support read');
|
|
10404
|
+
return support;
|
|
10405
|
+
}
|
|
9907
10406
|
/**
|
|
9908
10407
|
* Resolve the provider's currently active CAIP account identifier.
|
|
9909
10408
|
* Used by the MarketDataService REST coalesce layer so cached payloads
|
|
@@ -10943,6 +11442,7 @@ export class HyperLiquidProvider {
|
|
|
10943
11442
|
* @returns Result with txHash (HyperLiquid internal) and withdrawal ID
|
|
10944
11443
|
*/
|
|
10945
11444
|
async withdraw(params) {
|
|
11445
|
+
let accountContext;
|
|
10946
11446
|
try {
|
|
10947
11447
|
this.#deps.debugLogger.log('HyperLiquidProvider: STARTING WITHDRAWAL', {
|
|
10948
11448
|
params,
|
|
@@ -10995,6 +11495,16 @@ export class HyperLiquidProvider {
|
|
|
10995
11495
|
this.#deps.debugLogger.log('HyperLiquidProvider: ASSET SUPPORTED', {
|
|
10996
11496
|
assetId: params.assetId,
|
|
10997
11497
|
});
|
|
11498
|
+
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
11499
|
+
const network = this.#clientService.isTestnetMode()
|
|
11500
|
+
? 'testnet'
|
|
11501
|
+
: 'mainnet';
|
|
11502
|
+
const userAddress = await this.#walletService.getUserAddressWithDefault();
|
|
11503
|
+
accountContext = {
|
|
11504
|
+
lifecycleGeneration,
|
|
11505
|
+
network,
|
|
11506
|
+
userAddress,
|
|
11507
|
+
};
|
|
10998
11508
|
// Step 3: Determine destination address
|
|
10999
11509
|
this.#deps.debugLogger.log('HyperLiquidProvider: DETERMINING DESTINATION ADDRESS');
|
|
11000
11510
|
let destination;
|
|
@@ -11005,7 +11515,7 @@ export class HyperLiquidProvider {
|
|
|
11005
11515
|
});
|
|
11006
11516
|
}
|
|
11007
11517
|
else {
|
|
11008
|
-
destination =
|
|
11518
|
+
destination = userAddress;
|
|
11009
11519
|
this.#deps.debugLogger.log('HyperLiquidProvider: USING USER WALLET ADDRESS', {
|
|
11010
11520
|
destination,
|
|
11011
11521
|
});
|
|
@@ -11013,7 +11523,12 @@ export class HyperLiquidProvider {
|
|
|
11013
11523
|
// Step 4: Ensure client is ready
|
|
11014
11524
|
this.#deps.debugLogger.log('HyperLiquidProvider: ENSURING CLIENT READY');
|
|
11015
11525
|
await this.#ensureReady();
|
|
11016
|
-
await this.#
|
|
11526
|
+
accountContext = await this.#assertAccountSupported(accountContext);
|
|
11527
|
+
await this.#ensureUnifiedAccountEnabled({
|
|
11528
|
+
allowUserSigning: true,
|
|
11529
|
+
accountContext,
|
|
11530
|
+
});
|
|
11531
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Withdrawal');
|
|
11017
11532
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
11018
11533
|
this.#deps.debugLogger.log('HyperLiquidProvider: CLIENT READY');
|
|
11019
11534
|
// Step 5: Validate amount against account balance
|
|
@@ -11063,6 +11578,7 @@ export class HyperLiquidProvider {
|
|
|
11063
11578
|
endpoint: 'withdraw3',
|
|
11064
11579
|
timestamp: new Date().toISOString(),
|
|
11065
11580
|
});
|
|
11581
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Withdrawal');
|
|
11066
11582
|
const result = await exchangeClient.withdraw3({
|
|
11067
11583
|
destination,
|
|
11068
11584
|
amount: params.amount,
|
|
@@ -11089,7 +11605,11 @@ export class HyperLiquidProvider {
|
|
|
11089
11605
|
// HyperLiquid's withdraw3 API doesn't return a transaction hash immediately
|
|
11090
11606
|
};
|
|
11091
11607
|
}
|
|
11092
|
-
const
|
|
11608
|
+
const rejection = new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
|
|
11609
|
+
const isMultiSigRequired = this.#recordUnsupportedAccountFromError(rejection, accountContext);
|
|
11610
|
+
const errorMessage = isMultiSigRequired
|
|
11611
|
+
? PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED
|
|
11612
|
+
: `Withdrawal failed: ${String(result.status)}`;
|
|
11093
11613
|
this.#deps.debugLogger.log('HyperLiquidProvider: WITHDRAWAL FAILED', {
|
|
11094
11614
|
error: errorMessage,
|
|
11095
11615
|
status: result.status,
|
|
@@ -11102,13 +11622,18 @@ export class HyperLiquidProvider {
|
|
|
11102
11622
|
};
|
|
11103
11623
|
}
|
|
11104
11624
|
catch (error) {
|
|
11625
|
+
const isMultiSigRequired = accountContext
|
|
11626
|
+
? this.#recordUnsupportedAccountFromError(error, accountContext)
|
|
11627
|
+
: false;
|
|
11105
11628
|
const signerFailure = await this.#handleSignerFailure(error, 'withdraw', {
|
|
11106
11629
|
assetId: params.assetId,
|
|
11107
11630
|
});
|
|
11108
11631
|
if (signerFailure) {
|
|
11109
11632
|
return createErrorResult(signerFailure, { success: false });
|
|
11110
11633
|
}
|
|
11111
|
-
const safeError =
|
|
11634
|
+
const safeError = isMultiSigRequired
|
|
11635
|
+
? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
|
|
11636
|
+
: ensureError(error, 'HyperLiquidProvider.initiateWithdrawal');
|
|
11112
11637
|
this.#deps.debugLogger.log('HyperLiquidProvider: WITHDRAWAL EXCEPTION', {
|
|
11113
11638
|
error: safeError.message,
|
|
11114
11639
|
errorType: safeError.name,
|
|
@@ -11124,7 +11649,7 @@ export class HyperLiquidProvider {
|
|
|
11124
11649
|
operation: PERPS_ERROR_OPERATION.FinancialOperations,
|
|
11125
11650
|
action: PERPS_ERROR_ACTION.FinancialWithdrawal,
|
|
11126
11651
|
}));
|
|
11127
|
-
return createErrorResult(
|
|
11652
|
+
return createErrorResult(safeError, { success: false });
|
|
11128
11653
|
}
|
|
11129
11654
|
}
|
|
11130
11655
|
/**
|
|
@@ -11147,6 +11672,17 @@ export class HyperLiquidProvider {
|
|
|
11147
11672
|
* });
|
|
11148
11673
|
*/
|
|
11149
11674
|
async transferBetweenDexs(params) {
|
|
11675
|
+
return await this.#transferBetweenDexs(params);
|
|
11676
|
+
}
|
|
11677
|
+
/**
|
|
11678
|
+
* Transfer collateral with an optional context captured by a parent action.
|
|
11679
|
+
*
|
|
11680
|
+
* @param params - Transfer parameters.
|
|
11681
|
+
* @param expectedContext - Context captured by the parent action.
|
|
11682
|
+
* @returns The transfer result.
|
|
11683
|
+
*/
|
|
11684
|
+
async #transferBetweenDexs(params, expectedContext) {
|
|
11685
|
+
let accountContext;
|
|
11150
11686
|
try {
|
|
11151
11687
|
this.#deps.debugLogger.log('HyperLiquidProvider: STARTING DEX TRANSFER', {
|
|
11152
11688
|
params,
|
|
@@ -11159,14 +11695,13 @@ export class HyperLiquidProvider {
|
|
|
11159
11695
|
if (params.sourceDex === params.destinationDex) {
|
|
11160
11696
|
throw new Error('Source and destination DEX must be different');
|
|
11161
11697
|
}
|
|
11162
|
-
//
|
|
11163
|
-
|
|
11698
|
+
// Ensure client ready
|
|
11699
|
+
await this.#ensureReady();
|
|
11700
|
+
accountContext = await this.#assertAccountSupported(expectedContext);
|
|
11701
|
+
const { userAddress } = accountContext;
|
|
11164
11702
|
this.#deps.debugLogger.log('HyperLiquidProvider: USER ADDRESS', {
|
|
11165
11703
|
userAddress,
|
|
11166
11704
|
});
|
|
11167
|
-
// Ensure client ready
|
|
11168
|
-
await this.#ensureReady();
|
|
11169
|
-
const exchangeClient = this.#clientService.getExchangeClient();
|
|
11170
11705
|
// Execute transfer using SDK sendAsset()
|
|
11171
11706
|
// Note: SDK docs say "testnet-only" but it works on mainnet (verified via Phantom)
|
|
11172
11707
|
this.#deps.debugLogger.log('HyperLiquidProvider: CALLING SEND_ASSET API', {
|
|
@@ -11174,6 +11709,8 @@ export class HyperLiquidProvider {
|
|
|
11174
11709
|
destinationDex: params.destinationDex || '(main)',
|
|
11175
11710
|
amount: params.amount,
|
|
11176
11711
|
});
|
|
11712
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'DEX transfer');
|
|
11713
|
+
const exchangeClient = this.#clientService.getExchangeClient();
|
|
11177
11714
|
const result = await exchangeClient.sendAsset({
|
|
11178
11715
|
destination: userAddress,
|
|
11179
11716
|
sourceDex: params.sourceDex,
|
|
@@ -11193,14 +11730,22 @@ export class HyperLiquidProvider {
|
|
|
11193
11730
|
// User can verify transfer in explorer by timestamp
|
|
11194
11731
|
};
|
|
11195
11732
|
}
|
|
11733
|
+
if (this.#recordUnsupportedAccountFromError(new Error(`DEX transfer failed: ${JSON.stringify(result)}`), accountContext)) {
|
|
11734
|
+
throw new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED);
|
|
11735
|
+
}
|
|
11196
11736
|
throw new Error(PERPS_ERROR_CODES.TRANSFER_FAILED);
|
|
11197
11737
|
}
|
|
11198
11738
|
catch (error) {
|
|
11739
|
+
const isMultiSigRequired = accountContext
|
|
11740
|
+
? this.#recordUnsupportedAccountFromError(error, accountContext)
|
|
11741
|
+
: false;
|
|
11199
11742
|
const signerFailure = await this.#handleSignerFailure(error, 'transferBetweenDexs', { sourceDex: params.sourceDex, destinationDex: params.destinationDex });
|
|
11200
11743
|
if (signerFailure) {
|
|
11201
11744
|
return { success: false, error: signerFailure.message };
|
|
11202
11745
|
}
|
|
11203
|
-
const safeError =
|
|
11746
|
+
const safeError = isMultiSigRequired
|
|
11747
|
+
? new Error(PERPS_ERROR_CODES.EXCHANGE_MULTI_SIG_REQUIRED)
|
|
11748
|
+
: ensureError(error, 'HyperLiquidProvider.transferToSpot');
|
|
11204
11749
|
this.#deps.debugLogger.log('❌ HyperLiquidProvider: TRANSFER FAILED', {
|
|
11205
11750
|
error: safeError.message,
|
|
11206
11751
|
params,
|
|
@@ -11212,6 +11757,16 @@ export class HyperLiquidProvider {
|
|
|
11212
11757
|
};
|
|
11213
11758
|
}
|
|
11214
11759
|
}
|
|
11760
|
+
/**
|
|
11761
|
+
* Transfer collateral using a context captured by a parent action.
|
|
11762
|
+
*
|
|
11763
|
+
* @param params - Transfer parameters.
|
|
11764
|
+
* @param accountContext - Original action context for the transfer.
|
|
11765
|
+
* @returns The transfer result.
|
|
11766
|
+
*/
|
|
11767
|
+
async #transferBetweenDexsForContext(params, accountContext) {
|
|
11768
|
+
return await this.#transferBetweenDexs(params, accountContext);
|
|
11769
|
+
}
|
|
11215
11770
|
/**
|
|
11216
11771
|
* Subscribe to live price updates
|
|
11217
11772
|
*
|
|
@@ -11494,6 +12049,11 @@ export class HyperLiquidProvider {
|
|
|
11494
12049
|
try {
|
|
11495
12050
|
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
11496
12051
|
const userAddress = await this.#walletService.getUserAddressWithDefault();
|
|
12052
|
+
const preparationContext = {
|
|
12053
|
+
lifecycleGeneration,
|
|
12054
|
+
network: this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet',
|
|
12055
|
+
userAddress,
|
|
12056
|
+
};
|
|
11497
12057
|
// The result is only for the provider and account it started with.
|
|
11498
12058
|
const assertPreparationCurrent = async () => {
|
|
11499
12059
|
this.#assertProviderLifecycleCurrent(lifecycleGeneration, 'Trading wallet preparation');
|
|
@@ -11505,13 +12065,13 @@ export class HyperLiquidProvider {
|
|
|
11505
12065
|
throw new Error(PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE);
|
|
11506
12066
|
}
|
|
11507
12067
|
};
|
|
11508
|
-
await this.#ensureReadyForTrading({
|
|
12068
|
+
const { accountContext } = await this.#ensureReadyForTrading({
|
|
11509
12069
|
requiresBuilderFee: false,
|
|
11510
12070
|
recheckPendingReferral: true,
|
|
12071
|
+
accountContext: preparationContext,
|
|
11511
12072
|
});
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
: 'mainnet';
|
|
12073
|
+
await assertPreparationCurrent();
|
|
12074
|
+
const { network } = accountContext;
|
|
11515
12075
|
const isRegistered = await this.#isWalletOnHyperliquid(userAddress, network);
|
|
11516
12076
|
await assertPreparationCurrent();
|
|
11517
12077
|
// The venue rejects every write from a wallet with no HyperLiquid account
|
|
@@ -11527,7 +12087,7 @@ export class HyperLiquidProvider {
|
|
|
11527
12087
|
await this.#ensureBuilderFeeSetup(undefined, {
|
|
11528
12088
|
reportSignerFailure: true,
|
|
11529
12089
|
reportRefusal: true,
|
|
11530
|
-
});
|
|
12090
|
+
}, accountContext);
|
|
11531
12091
|
// The builder fee setup ends quietly when the provider disconnects.
|
|
11532
12092
|
await assertPreparationCurrent();
|
|
11533
12093
|
if (!this.#walletService.isMainAccountSignerReady()) {
|
|
@@ -12171,6 +12731,7 @@ export class HyperLiquidProvider {
|
|
|
12171
12731
|
// Clear session caches. Capability and fee reads use the lifecycle
|
|
12172
12732
|
// generation above to discard any old response that resolves later.
|
|
12173
12733
|
this.clearFeeCache();
|
|
12734
|
+
this.#accountSupportByContext.clear();
|
|
12174
12735
|
this.#referralCheckCache.clear();
|
|
12175
12736
|
this.#builderFeeCheckCache.clear();
|
|
12176
12737
|
this.#builderFeeRefusals.clear();
|
|
@@ -12416,20 +12977,26 @@ export class HyperLiquidProvider {
|
|
|
12416
12977
|
*
|
|
12417
12978
|
* Note: This is network-specific - testnet and mainnet have separate referral states
|
|
12418
12979
|
* Note: Non-blocking - failures are logged to Sentry but don't prevent trading
|
|
12980
|
+
*
|
|
12981
|
+
* @param accountContext - Account/network/lifecycle captured by the action
|
|
12982
|
+
* that is allowed to authorize a referral write.
|
|
12419
12983
|
*/
|
|
12420
|
-
async #ensureReferralSet() {
|
|
12984
|
+
async #ensureReferralSet(accountContext) {
|
|
12421
12985
|
this.#referralSetupNeedsRetry = false;
|
|
12422
12986
|
this.#referralAwaitsBuilderCode = false;
|
|
12423
|
-
const
|
|
12424
|
-
|
|
12987
|
+
const network = accountContext?.network ??
|
|
12988
|
+
(this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet');
|
|
12989
|
+
const isTestnet = network === 'testnet';
|
|
12425
12990
|
const expectedReferralCode = this.#getReferralCode(isTestnet);
|
|
12426
12991
|
const referrerAddress = this.#getBuilderAddress(isTestnet);
|
|
12427
|
-
let userAddress;
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12992
|
+
let userAddress = accountContext?.userAddress;
|
|
12993
|
+
if (!userAddress) {
|
|
12994
|
+
try {
|
|
12995
|
+
userAddress = await this.#walletService.getUserAddressWithDefault();
|
|
12996
|
+
}
|
|
12997
|
+
catch {
|
|
12998
|
+
return; // Can't proceed without address
|
|
12999
|
+
}
|
|
12433
13000
|
}
|
|
12434
13001
|
if (userAddress.toLowerCase() === referrerAddress.toLowerCase()) {
|
|
12435
13002
|
this.#deps.debugLogger.log('[ensureReferralSet] User is builder, skipping', { network });
|
|
@@ -12471,6 +13038,9 @@ export class HyperLiquidProvider {
|
|
|
12471
13038
|
return;
|
|
12472
13039
|
}
|
|
12473
13040
|
const codeStatus = await this.#getReferralCodeStatus();
|
|
13041
|
+
if (accountContext) {
|
|
13042
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral code lookup');
|
|
13043
|
+
}
|
|
12474
13044
|
if (codeStatus !== 'ready') {
|
|
12475
13045
|
this.#deps.debugLogger.log('[ensureReferralSet] Builder referral not ready, skipping', { network, codeStatus });
|
|
12476
13046
|
// Don't cache. A failed lookup (already logged) waits for the next
|
|
@@ -12480,7 +13050,10 @@ export class HyperLiquidProvider {
|
|
|
12480
13050
|
return;
|
|
12481
13051
|
}
|
|
12482
13052
|
// Check if user already has a referral on-chain
|
|
12483
|
-
const hasReferral = await this.#checkReferralSet();
|
|
13053
|
+
const hasReferral = await this.#checkReferralSet(userAddress);
|
|
13054
|
+
if (accountContext) {
|
|
13055
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral account lookup');
|
|
13056
|
+
}
|
|
12484
13057
|
if (hasReferral) {
|
|
12485
13058
|
// Already has referral on-chain
|
|
12486
13059
|
PerpsSigningCache.setReferral(network, userAddress, {
|
|
@@ -12491,7 +13064,7 @@ export class HyperLiquidProvider {
|
|
|
12491
13064
|
}
|
|
12492
13065
|
else {
|
|
12493
13066
|
this.#deps.debugLogger.log('[ensureReferralSet] Setting referral (will show signing request)', { network, referralCode: expectedReferralCode });
|
|
12494
|
-
const result = await this.#setReferralCode();
|
|
13067
|
+
const result = await this.#setReferralCode(accountContext);
|
|
12495
13068
|
if (result) {
|
|
12496
13069
|
this.#deps.debugLogger.log('[ensureReferralSet] Referral set successfully', { network });
|
|
12497
13070
|
PerpsSigningCache.setReferral(network, userAddress, {
|
|
@@ -12519,6 +13092,11 @@ export class HyperLiquidProvider {
|
|
|
12519
13092
|
completeInFlight();
|
|
12520
13093
|
return;
|
|
12521
13094
|
}
|
|
13095
|
+
if (accountContext &&
|
|
13096
|
+
this.#recordUnsupportedAccountFromError(error, accountContext)) {
|
|
13097
|
+
completeInFlight();
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
12522
13100
|
// Safety net: wallet looked registered but the SDK still rejects with
|
|
12523
13101
|
// "User or API Wallet 0x... does not exist." Do not forward to Sentry.
|
|
12524
13102
|
// The walletRegistered cache stores positive observations only, so no
|
|
@@ -12528,6 +13106,12 @@ export class HyperLiquidProvider {
|
|
|
12528
13106
|
completeInFlight();
|
|
12529
13107
|
return;
|
|
12530
13108
|
}
|
|
13109
|
+
if (accountContext &&
|
|
13110
|
+
ensureError(error, 'HyperLiquidProvider.ensureReferralSet').message ===
|
|
13111
|
+
PERPS_ERROR_CODES.PROVIDER_LIFECYCLE_STALE) {
|
|
13112
|
+
completeInFlight();
|
|
13113
|
+
throw error;
|
|
13114
|
+
}
|
|
12531
13115
|
// Cache failure to prevent retries
|
|
12532
13116
|
PerpsSigningCache.setReferral(network, userAddress, {
|
|
12533
13117
|
attempted: true,
|
|
@@ -12585,18 +13169,20 @@ export class HyperLiquidProvider {
|
|
|
12585
13169
|
/**
|
|
12586
13170
|
* Check if user has a referral code set with HyperLiquid
|
|
12587
13171
|
*
|
|
13172
|
+
* @param userAddress - Account to inspect, or the currently selected account
|
|
13173
|
+
* when omitted.
|
|
12588
13174
|
* @returns Promise resolving to true if referral is set, false otherwise
|
|
12589
13175
|
*/
|
|
12590
|
-
async #checkReferralSet() {
|
|
13176
|
+
async #checkReferralSet(userAddress) {
|
|
12591
13177
|
try {
|
|
12592
13178
|
const infoClient = this.#clientService.getInfoClient();
|
|
12593
|
-
const
|
|
13179
|
+
const referralUserAddress = userAddress ?? (await this.#walletService.getUserAddressWithDefault());
|
|
12594
13180
|
// Call HyperLiquid API to check if user has a referral set
|
|
12595
13181
|
const referralData = await infoClient.referral({
|
|
12596
|
-
user:
|
|
13182
|
+
user: referralUserAddress,
|
|
12597
13183
|
});
|
|
12598
13184
|
this.#deps.debugLogger.log('Referral check result:', {
|
|
12599
|
-
userAddress,
|
|
13185
|
+
userAddress: referralUserAddress,
|
|
12600
13186
|
referralData,
|
|
12601
13187
|
});
|
|
12602
13188
|
return Boolean(referralData?.referredBy?.code);
|
|
@@ -12620,10 +13206,15 @@ export class HyperLiquidProvider {
|
|
|
12620
13206
|
/**
|
|
12621
13207
|
* Set MetaMask as the user's referrer on HyperLiquid
|
|
12622
13208
|
*
|
|
13209
|
+
* @param accountContext - Account/network/lifecycle captured by the action
|
|
13210
|
+
* that is allowed to authorize this write.
|
|
12623
13211
|
* @returns A promise that resolves to the boolean result.
|
|
12624
13212
|
*/
|
|
12625
|
-
async #setReferralCode() {
|
|
13213
|
+
async #setReferralCode(accountContext) {
|
|
12626
13214
|
try {
|
|
13215
|
+
if (accountContext) {
|
|
13216
|
+
await this.#assertNoKnownUnsupportedAccount(accountContext, 'Referral setup');
|
|
13217
|
+
}
|
|
12627
13218
|
const exchangeClient = this.#clientService.getExchangeClient();
|
|
12628
13219
|
const referralCode = this.#getReferralCode(this.#clientService.isTestnetMode());
|
|
12629
13220
|
this.#deps.debugLogger.log('[setReferralCode] Setting referral code', {
|
|
@@ -12635,9 +13226,18 @@ export class HyperLiquidProvider {
|
|
|
12635
13226
|
code: referralCode,
|
|
12636
13227
|
});
|
|
12637
13228
|
this.#deps.debugLogger.log('[setReferralCode] Referral code set result', result);
|
|
13229
|
+
if (result?.status !== 'ok' &&
|
|
13230
|
+
accountContext &&
|
|
13231
|
+
this.#recordUnsupportedAccountFromError(new Error(`Referral setup failed: ${JSON.stringify(result)}`), accountContext)) {
|
|
13232
|
+
return false;
|
|
13233
|
+
}
|
|
12638
13234
|
return result?.status === 'ok';
|
|
12639
13235
|
}
|
|
12640
13236
|
catch (error) {
|
|
13237
|
+
if (accountContext &&
|
|
13238
|
+
this.#recordUnsupportedAccountFromError(error, accountContext)) {
|
|
13239
|
+
throw error;
|
|
13240
|
+
}
|
|
12641
13241
|
// Retryable (a locked keyring, an unavailable agent, or one the venue
|
|
12642
13242
|
// rejected): `#ensureReferralSet` retries at the next entry, so it is
|
|
12643
13243
|
// not an error to report.
|