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