@lifi/perps-sdk-provider-hyperliquid 18.1.1 → 20.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +11 -0
  2. package/dist/cjs/HyperliquidProvider.d.ts.map +1 -1
  3. package/dist/cjs/HyperliquidProvider.js +22 -5
  4. package/dist/cjs/HyperliquidProvider.js.map +1 -1
  5. package/dist/cjs/accountConfig.d.ts +2 -2
  6. package/dist/cjs/accountConfig.d.ts.map +1 -1
  7. package/dist/cjs/accountConfig.js +22 -5
  8. package/dist/cjs/accountConfig.js.map +1 -1
  9. package/dist/cjs/services/getAccount.d.ts.map +1 -1
  10. package/dist/cjs/services/getAccount.js +12 -8
  11. package/dist/cjs/services/getAccount.js.map +1 -1
  12. package/dist/cjs/services/getActivity.d.ts.map +1 -1
  13. package/dist/cjs/services/getActivity.js +3 -1
  14. package/dist/cjs/services/getActivity.js.map +1 -1
  15. package/dist/cjs/services/getMarketSettings.d.ts +2 -2
  16. package/dist/cjs/services/getMarketSettings.d.ts.map +1 -1
  17. package/dist/cjs/services/getMarketSettings.js +8 -3
  18. package/dist/cjs/services/getMarketSettings.js.map +1 -1
  19. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  20. package/dist/cjs/signers/signActions.js +1 -1
  21. package/dist/cjs/signers/signActions.js.map +1 -1
  22. package/dist/cjs/types/ledger.d.ts +17 -1
  23. package/dist/cjs/types/ledger.d.ts.map +1 -1
  24. package/dist/cjs/types/ledger.js +10 -1
  25. package/dist/cjs/types/ledger.js.map +1 -1
  26. package/dist/cjs/utils/mapActivity.d.ts.map +1 -1
  27. package/dist/cjs/utils/mapActivity.js +41 -0
  28. package/dist/cjs/utils/mapActivity.js.map +1 -1
  29. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  30. package/dist/cjs/utils/withdrawableBalances.js +3 -1
  31. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  32. package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
  33. package/dist/esm/HyperliquidProvider.js +29 -8
  34. package/dist/esm/HyperliquidProvider.js.map +1 -1
  35. package/dist/esm/accountConfig.d.ts +5 -5
  36. package/dist/esm/accountConfig.d.ts.map +1 -1
  37. package/dist/esm/accountConfig.js +32 -11
  38. package/dist/esm/accountConfig.js.map +1 -1
  39. package/dist/esm/services/getAccount.d.ts.map +1 -1
  40. package/dist/esm/services/getAccount.js +10 -9
  41. package/dist/esm/services/getAccount.js.map +1 -1
  42. package/dist/esm/services/getActivity.d.ts.map +1 -1
  43. package/dist/esm/services/getActivity.js +4 -2
  44. package/dist/esm/services/getActivity.js.map +1 -1
  45. package/dist/esm/services/getMarketSettings.d.ts +5 -3
  46. package/dist/esm/services/getMarketSettings.d.ts.map +1 -1
  47. package/dist/esm/services/getMarketSettings.js +13 -7
  48. package/dist/esm/services/getMarketSettings.js.map +1 -1
  49. package/dist/esm/signers/signActions.d.ts.map +1 -1
  50. package/dist/esm/signers/signActions.js +1 -1
  51. package/dist/esm/signers/signActions.js.map +1 -1
  52. package/dist/esm/types/ledger.d.ts +42 -1
  53. package/dist/esm/types/ledger.d.ts.map +1 -1
  54. package/dist/esm/types/ledger.js +17 -0
  55. package/dist/esm/types/ledger.js.map +1 -1
  56. package/dist/esm/utils/mapActivity.d.ts +1 -0
  57. package/dist/esm/utils/mapActivity.d.ts.map +1 -1
  58. package/dist/esm/utils/mapActivity.js +49 -1
  59. package/dist/esm/utils/mapActivity.js.map +1 -1
  60. package/dist/esm/utils/withdrawableBalances.d.ts +2 -1
  61. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  62. package/dist/esm/utils/withdrawableBalances.js +5 -2
  63. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  64. package/dist/types/HyperliquidProvider.d.ts.map +1 -1
  65. package/dist/types/accountConfig.d.ts +5 -5
  66. package/dist/types/accountConfig.d.ts.map +1 -1
  67. package/dist/types/services/getAccount.d.ts.map +1 -1
  68. package/dist/types/services/getActivity.d.ts.map +1 -1
  69. package/dist/types/services/getMarketSettings.d.ts +5 -3
  70. package/dist/types/services/getMarketSettings.d.ts.map +1 -1
  71. package/dist/types/signers/signActions.d.ts.map +1 -1
  72. package/dist/types/types/ledger.d.ts +42 -1
  73. package/dist/types/types/ledger.d.ts.map +1 -1
  74. package/dist/types/utils/mapActivity.d.ts +1 -0
  75. package/dist/types/utils/mapActivity.d.ts.map +1 -1
  76. package/dist/types/utils/withdrawableBalances.d.ts +2 -1
  77. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  78. package/package.json +3 -3
  79. package/src/HyperliquidProvider.ts +45 -12
  80. package/src/accountConfig.ts +38 -14
  81. package/src/services/getAccount.ts +22 -12
  82. package/src/services/getActivity.ts +5 -1
  83. package/src/services/getMarketSettings.ts +20 -8
  84. package/src/signers/signActions.ts +4 -3
  85. package/src/types/ledger.ts +61 -0
  86. package/src/utils/mapActivity.ts +56 -0
  87. package/src/utils/withdrawableBalances.ts +8 -3
@@ -30,6 +30,7 @@ import {
30
30
  type AccountConfigSetting,
31
31
  type AccountResponse,
32
32
  type AccountSummary,
33
+ type ActionResult,
33
34
  type ActionStep,
34
35
  ActionType,
35
36
  type ActivitiesResponse,
@@ -45,8 +46,8 @@ import {
45
46
  type PortfolioHistoryResponse,
46
47
  type Position,
47
48
  type PositionsResponse,
48
- type ProviderAction,
49
49
  type Quote,
50
+ type SetupAction,
50
51
  type SignedActionStep,
51
52
  type SigningMethod,
52
53
  } from '@lifi/perps-types'
@@ -229,8 +230,6 @@ export function hyperliquidProvider(
229
230
  return {
230
231
  type: PROVIDER_KEY,
231
232
 
232
- internalSetupActions: [ActionType.SET_REFERRAL],
233
-
234
233
  // REVOKE_AGENT stages a step only when every named agent slot is taken,
235
234
  // so `checkSetup` omits it from `ProviderSetup.checklist` instead of
236
235
  // rendering it as a satisfied step.
@@ -254,7 +253,7 @@ export function hyperliquidProvider(
254
253
  resolveActionRequest: async (
255
254
  action: ActionType,
256
255
  address: Address,
257
- signers: PerpsSigner[]
256
+ signer: PerpsSigner
258
257
  ): Promise<ActionSignerContribution> => {
259
258
  // APPROVE_AGENT is user-signed: the user authorises the agent, so the
260
259
  // agent address rides as a param (not signerAddress). The agent is
@@ -264,10 +263,30 @@ export function hyperliquidProvider(
264
263
  const agentAddress = await resolveApproveAgentAddress(address)
265
264
  return { params: { agentAddress } }
266
265
  }
267
- // SDK-signed actions (trades, account-mode) carry the agent as the
268
- // on-wire signerAddress. User-signed actions (builder-fee, withdrawal)
269
- // contribute nothing — core submits under the user's own address.
270
- if (signers.includes(PerpsSigner.SDK)) {
266
+ // REVOKE_SESSION_AGENT targets the stored agent by address and never
267
+ // provisions one: with no stored agent there is nothing to revoke.
268
+ if (action === ActionType.REVOKE_SESSION_AGENT) {
269
+ try {
270
+ const agent = await agentStore.get(address)
271
+ return { params: { agentAddress: agent.address } }
272
+ } catch (error) {
273
+ if (
274
+ error instanceof PerpsError &&
275
+ error.message === PerpsErrorMessage.AgentNotFound
276
+ ) {
277
+ throw new PerpsError(
278
+ PerpsErrorCode.SDKError,
279
+ `Nothing to revoke for '${action}' on '${PROVIDER_KEY}': no session agent is stored for ${address}.`
280
+ )
281
+ }
282
+ throw error
283
+ }
284
+ }
285
+ // SDK-signed actions (trades) carry the agent as the on-wire
286
+ // signerAddress. User-signed actions (builder-fee, account mode,
287
+ // withdrawal) contribute nothing — core submits under the user's own
288
+ // address.
289
+ if (signer === PerpsSigner.SDK) {
271
290
  const agent = await agentStore.get(address)
272
291
  return { signerAddress: agent.address }
273
292
  }
@@ -282,6 +301,21 @@ export function hyperliquidProvider(
282
301
  ): Promise<SignedActionStep[]> =>
283
302
  hyperliquidSignActions(agentStore, method, steps, address, ctx),
284
303
 
304
+ // A confirmed session-agent revoke leaves the stored key unusable.
305
+ onExecuteResults: async (
306
+ address: Address,
307
+ results: ActionResult[]
308
+ ): Promise<void> => {
309
+ if (
310
+ results.some(
311
+ (result) =>
312
+ result.success && result.action === ActionType.REVOKE_SESSION_AGENT
313
+ )
314
+ ) {
315
+ await agentStore.remove(address)
316
+ }
317
+ },
318
+
285
319
  getAccount: (
286
320
  params: ProviderGetAccountParams,
287
321
  opts?: SDKRequestOptions
@@ -329,7 +363,7 @@ export function hyperliquidProvider(
329
363
  getMarketSettings: (
330
364
  params: ProviderGetMarketSettingsParams,
331
365
  opts?: SDKRequestOptions
332
- ): Promise<MarketSettings | undefined> =>
366
+ ): Promise<MarketSettings> =>
333
367
  getMarketSettings(
334
368
  contextRef.require(),
335
369
  { address: params.address, market: params.market },
@@ -456,8 +490,7 @@ export function hyperliquidProvider(
456
490
 
457
491
  projectConfig: (
458
492
  config: AccountConfig,
459
- setup: ProviderAction[],
460
- options: ProviderAction[]
493
+ setup: SetupAction[]
461
494
  ): AccountConfigSetting[] => {
462
495
  if (config.provider !== PROVIDER_KEY) {
463
496
  throw new PerpsError(
@@ -466,7 +499,7 @@ export function hyperliquidProvider(
466
499
  `'${config.provider}'.`
467
500
  )
468
501
  }
469
- return projectHyperliquidConfigSettings(config, setup, options)
502
+ return projectHyperliquidConfigSettings(config, setup)
470
503
  },
471
504
  }
472
505
  }
@@ -1,10 +1,18 @@
1
- import { PerpsError } from '@lifi/perps-sdk'
1
+ import { isSetupOptionFor, PerpsError } from '@lifi/perps-sdk'
2
2
  import type {
3
3
  AccountConfigSetting,
4
4
  HyperliquidAccountConfig,
5
- ProviderAction,
5
+ SetupAction,
6
6
  } from '@lifi/perps-types'
7
7
  import { ActionType, PerpsErrorCode } from '@lifi/perps-types'
8
+ import { HlAbstractionMode } from './types/account.js'
9
+
10
+ // `null`, `'default'` and `'disabled'` are the same off state; an option may
11
+ // bind either spelling of it.
12
+ const ABSTRACTION_OFF_VALUES: ReadonlySet<string> = new Set([
13
+ HlAbstractionMode.DEFAULT,
14
+ HlAbstractionMode.DISABLED,
15
+ ])
8
16
 
9
17
  function assertNever(value: never): never {
10
18
  throw new Error(
@@ -24,14 +32,14 @@ function assertNever(value: never): never {
24
32
  * | REVOKE_AGENT | [] (no parameters)
25
33
  * | APPROVE_BUILDER_FEE | [] (no parameters)
26
34
  * | SET_REFERRAL | [] (no parameters)
27
- * | ACCOUNT_MODE | [{ name: 'mode', value: config.abstractionMode }]
35
+ * | ACCOUNT_MODE | [{ name: 'mode', value: config.abstractionMode }] (off → bound off spelling)
28
36
  *
29
37
  * The switch is exhaustive over `ActionType` so enum additions force a
30
38
  * compile error in the `default` arm. ActionTypes that are not valid on
31
- * `Provider.setup` / `Provider.options` for Hyperliquid throw at runtime.
39
+ * `Provider.setup` for Hyperliquid throw at runtime.
32
40
  */
33
41
  function projectHyperliquidDescriptor(
34
- descriptor: ProviderAction,
42
+ descriptor: SetupAction,
35
43
  config: HyperliquidAccountConfig
36
44
  ): AccountConfigSetting {
37
45
  switch (descriptor.type) {
@@ -41,11 +49,25 @@ function projectHyperliquidDescriptor(
41
49
  case ActionType.SET_REFERRAL:
42
50
  return { type: descriptor.type, values: [] }
43
51
 
44
- case ActionType.ACCOUNT_MODE:
52
+ case ActionType.ACCOUNT_MODE: {
53
+ const mode = config.abstractionMode
54
+ const bound = (descriptor.options ?? []).flatMap((option) =>
55
+ isSetupOptionFor(option, ActionType.ACCOUNT_MODE)
56
+ ? [option.params.mode]
57
+ : []
58
+ )
59
+ // The off state projects as whichever off spelling an option binds, so
60
+ // the value always matches the option it is satisfied by.
61
+ const matched =
62
+ mode === null || ABSTRACTION_OFF_VALUES.has(mode)
63
+ ? bound.find((value) => ABSTRACTION_OFF_VALUES.has(value))
64
+ : bound.find((value) => value === mode)
45
65
  return {
46
66
  type: descriptor.type,
47
- values: [{ name: 'mode', value: config.abstractionMode }],
67
+ values: [{ name: 'mode', value: matched ?? mode }],
68
+ satisfied: matched !== undefined,
48
69
  }
70
+ }
49
71
 
50
72
  case ActionType.APPROVE_INTEGRATOR:
51
73
  case ActionType.ACCOUNT_TYPE:
@@ -73,11 +95,14 @@ function projectHyperliquidDescriptor(
73
95
  case ActionType.META_ONBOARD:
74
96
  case ActionType.META_CREATE_REFERRAL_CODE:
75
97
  case ActionType.SYNC_FEE_ATTRIBUTION:
98
+ case ActionType.REVOKE_BUILDER_FEE:
99
+ case ActionType.REVOKE_SESSION_AGENT:
100
+ case ActionType.REVOKE_INTEGRATOR:
76
101
  throw new PerpsError(
77
102
  PerpsErrorCode.SDKError,
78
103
  `Hyperliquid account-config mapper has no projection for ` +
79
104
  `descriptor type '${descriptor.type}' — this ActionType is not ` +
80
- `valid on Provider.setup / Provider.options for Hyperliquid.`
105
+ `valid on Provider.setup for Hyperliquid.`
81
106
  )
82
107
 
83
108
  default:
@@ -86,18 +111,17 @@ function projectHyperliquidDescriptor(
86
111
  }
87
112
 
88
113
  /**
89
- * Project the union of Hyperliquid setup + options descriptors against the
90
- * typed `HyperliquidAccountConfig`. Produces exactly one
91
- * `AccountConfigSetting` per descriptor, in `setup`-then-`options` order.
114
+ * Project the Hyperliquid setup descriptors against the typed
115
+ * `HyperliquidAccountConfig`. Produces exactly one `AccountConfigSetting` per
116
+ * descriptor, in `Provider.setup` order.
92
117
  *
93
118
  * @public
94
119
  */
95
120
  export function projectHyperliquidConfigSettings(
96
121
  config: HyperliquidAccountConfig,
97
- setup: ProviderAction[],
98
- options: ProviderAction[]
122
+ setup: SetupAction[]
99
123
  ): AccountConfigSetting[] {
100
- return [...setup, ...options].map((descriptor) =>
124
+ return setup.map((descriptor) =>
101
125
  projectHyperliquidDescriptor(descriptor, config)
102
126
  )
103
127
  }
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  getMarketRegistry,
3
3
  getMarketsContext,
4
+ maxOf,
5
+ minOf,
4
6
  type ProviderGetAccountParams,
5
7
  type SDKRequestOptions,
6
8
  stringToFloat,
@@ -14,7 +16,7 @@ import type {
14
16
  HyperliquidDexAccountState,
15
17
  Position,
16
18
  } from '@lifi/perps-types'
17
- import type Big from 'big.js'
19
+ import Big from 'big.js'
18
20
  import { PROVIDER_KEY } from '../constants.js'
19
21
  import type { HyperliquidContext } from '../context.js'
20
22
  import type {
@@ -66,13 +68,6 @@ interface BalancePartition {
66
68
  collateralBalances: Balance[]
67
69
  }
68
70
 
69
- const transferableWithin = (venueFigure: Big, units: Big): string => {
70
- if (venueFigure.lt(0)) {
71
- return '0'
72
- }
73
- return (venueFigure.gt(units) ? units : venueFigure).toFixed()
74
- }
75
-
76
71
  const buildBalances = (
77
72
  abstraction: HlAbstractionMode | null,
78
73
  spotState: HlSpotClearinghouseState,
@@ -84,7 +79,19 @@ const buildBalances = (
84
79
  const { balances, collateralBalances } = partitionSpotBalances(
85
80
  spotState.balances
86
81
  .filter((b) => !assetIsOutcome(b.coin))
87
- .map((b) => spotBalance(spotAssetFromToken(b), b.total, priceById)),
82
+ .map((b) => {
83
+ const total = toWireBig(b.total, 'spotClearinghouseState.total')
84
+ return {
85
+ ...spotBalance(spotAssetFromToken(b), b.total, priceById),
86
+ transferable: minOf(
87
+ maxOf(
88
+ total.minus(toWireBig(b.hold, 'spotClearinghouseState.hold')),
89
+ new Big(0)
90
+ ),
91
+ total
92
+ ).toFixed(),
93
+ }
94
+ }),
88
95
  quoteAssetIds
89
96
  )
90
97
 
@@ -107,10 +114,13 @@ const buildBalances = (
107
114
  units: value.toFixed(),
108
115
  valueUsd: value.toFixed(),
109
116
  price: '1',
110
- transferable: transferableWithin(
111
- toWireBig(state.withdrawable, 'clearinghouseState.withdrawable'),
117
+ transferable: minOf(
118
+ maxOf(
119
+ toWireBig(state.withdrawable, 'clearinghouseState.withdrawable'),
120
+ new Big(0)
121
+ ),
112
122
  value
113
- ),
123
+ ).toFixed(),
114
124
  })
115
125
  }
116
126
  }
@@ -24,7 +24,9 @@ import type {
24
24
  HlUserNonFundingLedgerUpdates,
25
25
  } from '../types/index.js'
26
26
  import {
27
+ isBorrowLendDelta,
27
28
  isCollateralTransferDelta,
29
+ isCStakingTransferDelta,
28
30
  isDepositDelta,
29
31
  isLiquidationDelta,
30
32
  isSendAssetDelta,
@@ -130,7 +132,9 @@ const fetchActivityData = async (
130
132
  (isSpotTransferDelta(entry.delta) ||
131
133
  isSendAssetDelta(entry.delta) ||
132
134
  isCollateralTransferDelta(entry.delta) ||
133
- isVaultTransferDelta(entry.delta))))
135
+ isVaultTransferDelta(entry.delta) ||
136
+ isCStakingTransferDelta(entry.delta) ||
137
+ isBorrowLendDelta(entry.delta))))
134
138
  ) {
135
139
  return []
136
140
  }
@@ -1,11 +1,12 @@
1
- import type { SDKRequestOptions } from '@lifi/perps-sdk'
1
+ import { PerpsError, type SDKRequestOptions } from '@lifi/perps-sdk'
2
2
  import {
3
3
  MarginMode,
4
4
  type MarketRef,
5
5
  type MarketSettings,
6
+ PerpsErrorCode,
6
7
  } from '@lifi/perps-types'
7
8
  import type { Address } from 'viem'
8
- import { SPOT_MARKET_ID } from '../constants.js'
9
+ import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
9
10
  import type { HyperliquidContext } from '../context.js'
10
11
  import { fetchActiveAssetData } from './activeAssetData.js'
11
12
 
@@ -22,17 +23,23 @@ export interface GetMarketSettingsParams {
22
23
  /**
23
24
  * The user's venue-stored margin mode and leverage for one perps market,
24
25
  * read from `activeAssetData` — present whether or not a position is open.
25
- * @throws {PerpsError} On Hyperliquid REST error, network, or parsing failures.
26
+ * @throws {PerpsError} `ValidationError` for a spot market; `SDKError` when
27
+ * the response carries no positive leverage; any other `PerpsError` on
28
+ * Hyperliquid REST error, network, or parsing failures.
26
29
  * @public
27
30
  */
28
31
  export const getMarketSettings = async (
29
32
  context: HyperliquidContext,
30
33
  params: GetMarketSettingsParams,
31
34
  options?: SDKRequestOptions
32
- ): Promise<MarketSettings | undefined> => {
33
- // Spot markets carry no leverage state on the venue.
35
+ ): Promise<MarketSettings> => {
34
36
  if (params.market.categoryId === SPOT_MARKET_ID) {
35
- return undefined
37
+ const error = new PerpsError(
38
+ PerpsErrorCode.ValidationError,
39
+ `Hyperliquid market '${params.market.marketId}' is a spot market and carries no leverage setting`
40
+ )
41
+ error.tool = PROVIDER_KEY
42
+ throw error
36
43
  }
37
44
  const data = await fetchActiveAssetData(
38
45
  context,
@@ -41,8 +48,13 @@ export const getMarketSettings = async (
41
48
  options
42
49
  )
43
50
  const leverage = data.leverage
44
- if (!leverage) {
45
- return undefined
51
+ if (!leverage || !(leverage.value > 0)) {
52
+ const error = new PerpsError(
53
+ PerpsErrorCode.SDKError,
54
+ `Hyperliquid returned no leverage for market '${params.market.marketId}'`
55
+ )
56
+ error.tool = PROVIDER_KEY
57
+ throw error
46
58
  }
47
59
  return {
48
60
  marginMode:
@@ -39,9 +39,10 @@ export async function hyperliquidSignActions(
39
39
  )
40
40
  }
41
41
 
42
- const sign = ctx?.signers?.includes(PerpsSigner.USER)
43
- ? await userWalletSigner(ctx)
44
- : await agentSigner(agentStore, address)
42
+ const sign =
43
+ ctx?.signer === PerpsSigner.USER
44
+ ? await userWalletSigner(ctx)
45
+ : await agentSigner(agentStore, address)
45
46
 
46
47
  return Promise.all(
47
48
  (steps as Eip712ActionStep[]).map(
@@ -130,6 +130,38 @@ export type HlLiquidationDelta = {
130
130
  liquidatedPositions?: HlLiquidatedPosition[]
131
131
  }
132
132
 
133
+ /**
134
+ * Hyperliquid `cStakingTransfer` ledger delta. Moves `token` between the
135
+ * account's spot balance and its staking balance; `isDeposit` is `true` for a
136
+ * move into staking. `amount` is unsigned. A completed unstake row carries the
137
+ * zero hash.
138
+ * @public
139
+ */
140
+ export type HlCStakingTransferDelta = {
141
+ type: 'cStakingTransfer'
142
+ token: string
143
+ amount: string
144
+ isDeposit: boolean
145
+ }
146
+
147
+ /** Borrow-lend market action named by a `borrowLend` ledger delta. @public */
148
+ export type HlBorrowLendOperation = 'supply' | 'withdraw' | 'borrow' | 'repay'
149
+
150
+ /**
151
+ * Hyperliquid `borrowLend` ledger delta. Moves `token` between the account's
152
+ * spot balance and the borrow-lend market; `operation` gives the direction and
153
+ * `amount` is unsigned. `interestAmount` is the accrued interest that
154
+ * `amount` includes on a `withdraw` or `repay`, and `"0.0"` otherwise.
155
+ * @public
156
+ */
157
+ export type HlBorrowLendDelta = {
158
+ type: 'borrowLend'
159
+ token: string
160
+ operation: HlBorrowLendOperation
161
+ amount: string
162
+ interestAmount: string
163
+ }
164
+
133
165
  /**
134
166
  * Union of known Hyperliquid non-funding ledger deltas plus an open fallback
135
167
  * for endpoint variants the provider does not map.
@@ -143,6 +175,8 @@ export type HlLedgerDelta =
143
175
  | HlDepositDelta
144
176
  | HlWithdrawDelta
145
177
  | HlLiquidationDelta
178
+ | HlCStakingTransferDelta
179
+ | HlBorrowLendDelta
146
180
  | {
147
181
  type: string
148
182
  usdc?: string
@@ -202,6 +236,33 @@ export const isLiquidationDelta = (
202
236
  delta: HlLedgerDelta
203
237
  ): delta is HlLiquidationDelta => delta.type === 'liquidation'
204
238
 
239
+ /**
240
+ * Type guard for `HlCStakingTransferDelta`. `isDeposit` is part of the guard:
241
+ * a row without a boolean `isDeposit` names no direction for the movement.
242
+ * @public
243
+ */
244
+ export const isCStakingTransferDelta = (
245
+ delta: HlLedgerDelta
246
+ ): delta is HlCStakingTransferDelta =>
247
+ delta.type === 'cStakingTransfer' &&
248
+ 'isDeposit' in delta &&
249
+ typeof delta.isDeposit === 'boolean'
250
+
251
+ const HL_BORROW_LEND_OPERATIONS: ReadonlySet<unknown> =
252
+ new Set<HlBorrowLendOperation>(['supply', 'withdraw', 'borrow', 'repay'])
253
+
254
+ /**
255
+ * Type guard for `HlBorrowLendDelta`. The operation is part of the guard: an
256
+ * unknown operation names no direction for the movement.
257
+ * @public
258
+ */
259
+ export const isBorrowLendDelta = (
260
+ delta: HlLedgerDelta
261
+ ): delta is HlBorrowLendDelta =>
262
+ delta.type === 'borrowLend' &&
263
+ 'operation' in delta &&
264
+ HL_BORROW_LEND_OPERATIONS.has(delta.operation)
265
+
205
266
  /**
206
267
  * One timestamped non-funding ledger update. `time` is milliseconds since
207
268
  * epoch and `hash` is the upstream transaction identifier.
@@ -17,13 +17,17 @@ import type {
17
17
  } from '@lifi/perps-types'
18
18
  import { ActivityType, PerpsErrorCode } from '@lifi/perps-types'
19
19
  import Big from 'big.js'
20
+ import { zeroHash } from 'viem'
20
21
  import type {
22
+ HlBorrowLendOperation,
21
23
  HlFundingUpdate,
22
24
  HlLedgerUpdate,
23
25
  HlUserFill,
24
26
  } from '../types/index.js'
25
27
  import {
28
+ isBorrowLendDelta,
26
29
  isCollateralTransferDelta,
30
+ isCStakingTransferDelta,
27
31
  isDepositDelta,
28
32
  isLiquidationDelta,
29
33
  isSendAssetDelta,
@@ -47,6 +51,17 @@ const HL_NATIVE_TOKEN_SYMBOL = 'HYPE'
47
51
  // Hyperliquid reserves spot token index 0 for USDC.
48
52
  const HL_COLLATERAL_ASSET_ID = '0'
49
53
 
54
+ /**
55
+ * Direction relative to the account's spot balance, the same frame as an
56
+ * outbound `vaultDeposit`.
57
+ */
58
+ const BORROW_LEND_DIRECTION: Record<HlBorrowLendOperation, 'IN' | 'OUT'> = {
59
+ supply: 'OUT',
60
+ withdraw: 'IN',
61
+ borrow: 'IN',
62
+ repay: 'OUT',
63
+ }
64
+
50
65
  /**
51
66
  * A ledger row moves a spot asset — a perp contract cannot be sent, received
52
67
  * or withdrawn — so the delta's token symbol resolves inside the spot asset
@@ -69,6 +84,7 @@ const resolveLedgerAsset = (symbol: string, registry: AssetRegistry): Asset => {
69
84
 
70
85
  /**
71
86
  * Map supported ledger entries; exclude same-account moves and unsupported types.
87
+ * Staking and borrow-lend moves map with the queried account as counterparty.
72
88
  * Missing assets throw; unresolved liquidation markets omit only those positions.
73
89
  * @public
74
90
  */
@@ -143,6 +159,46 @@ export const mapLedgerEntry = (
143
159
  } satisfies TransferActivity
144
160
  }
145
161
 
162
+ if (isCStakingTransferDelta(delta) || isBorrowLendDelta(delta)) {
163
+ // A completed unstake is a system event with the zero hash and no venue
164
+ // transaction, so the id is synthesized and no explorer link applies.
165
+ const hasTransaction = entry.hash !== zeroHash
166
+ const direction = isCStakingTransferDelta(delta)
167
+ ? delta.isDeposit
168
+ ? 'OUT'
169
+ : 'IN'
170
+ : BORROW_LEND_DIRECTION[delta.operation]
171
+ const action = isCStakingTransferDelta(delta) ? direction : delta.operation
172
+ return {
173
+ ...base,
174
+ ...(hasTransaction
175
+ ? {}
176
+ : {
177
+ id: `${delta.type}:${delta.token}:${action}:${delta.amount}:${base.timestamp}`,
178
+ }),
179
+ type: ActivityType.TRANSFER,
180
+ direction,
181
+ counterpartyAddress: queriedAddress.toLowerCase(),
182
+ asset: resolveLedgerAsset(delta.token, assetRegistry),
183
+ amount: delta.amount,
184
+ meta: isCStakingTransferDelta(delta)
185
+ ? { transferType: delta.type }
186
+ : {
187
+ transferType: delta.type,
188
+ operation: delta.operation,
189
+ interestAmount: delta.interestAmount,
190
+ },
191
+ ...(hasTransaction
192
+ ? {
193
+ explorerLink: explorerTxUrl(
194
+ ExplorerChainId.HYPERLIQUID,
195
+ entry.hash
196
+ ),
197
+ }
198
+ : {}),
199
+ } satisfies TransferActivity
200
+ }
201
+
146
202
  if (isVaultTransferDelta(delta)) {
147
203
  const queried = queriedAddress.toLowerCase()
148
204
  const vault = delta.vault.toLowerCase()
@@ -17,7 +17,8 @@ import { toWireBig } from './decimal.js'
17
17
  *
18
18
  * @param quoteAssetId - `Asset.id` the perps-route row is keyed by.
19
19
  * @param withdrawalFee - Flat venue fee in quote-asset units, set on every
20
- * quote-asset row. Absent leaves every row without a fee.
20
+ * quote-asset row. Hyperliquid deducts it from the requested amount. Absent
21
+ * leaves every row without a fee.
21
22
  * @public
22
23
  */
23
24
  export const hyperliquidWithdrawableBalances = (
@@ -32,8 +33,12 @@ export const hyperliquidWithdrawableBalances = (
32
33
  withdrawalFee === undefined
33
34
  ? undefined
34
35
  : toWireBig(withdrawalFee, 'providers.withdrawalFeeUsd').toFixed()
35
- const feeFor = (assetId: string) =>
36
- fee === undefined || assetId !== quoteAssetId ? {} : { withdrawalFee: fee }
36
+ const feeFor = (
37
+ assetId: string
38
+ ): Pick<ProviderWithdrawableBalance, 'withdrawalFee' | 'isFeeDeducted'> =>
39
+ fee === undefined || assetId !== quoteAssetId
40
+ ? {}
41
+ : { withdrawalFee: fee, isFeeDeducted: true }
37
42
 
38
43
  if (isUnifiedAbstraction(abstraction)) {
39
44
  for (const balance of spotState.balances) {