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