From 1735cec174da626b464ac3d2c8964b924cfc0e8b Mon Sep 17 00:00:00 2001 From: nakazanie-ton Date: Fri, 17 Apr 2026 14:32:05 +0700 Subject: [PATCH] feat: canonicalize high efficiency mode naming --- CHANGELOG.md | 10 +- .../2.7. High Efficiency Mode.md | 252 ++++++++++++++++++ docs/2. Core Concepts/README.md | 1 + .../4. Api Reference/4.3. User Interaction.md | 67 ++++- src/api/math.ts | 14 +- src/api/parser.ts | 22 +- src/types/User.ts | 4 +- tests/check_user_lp_copy.ts | 6 +- tests/math/heModeApi.test.ts | 141 ++++++++++ tests/supply_withdraw_classic_he.ts | 8 +- 10 files changed, 487 insertions(+), 38 deletions(-) create mode 100644 docs/2. Core Concepts/2.7. High Efficiency Mode.md create mode 100644 tests/math/heModeApi.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c3315ac6..538e0a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - **High Efficiency (HE) mode** — pools can define HE asset groups via `poolAssetsHEConfig`, each with a `heCategory` number. `activeHeCategory` activates when all borrows belong to a single HE category **and** the total borrow exceeds the standard collateral limit. The HE borrow limit is calculated from supply assets within the same HE category using `heCollateralFactor` and `heLiquidationThreshold` instead of standard values, allowing higher LTV within the category. - `activeHeCategory` field in `UserData` — `-1` means HE is inactive, positive integer is the active category - `predictedHeCategory` field in `UserData` — HE category that would activate on next borrow (based on current supply), used to show available HE borrow limit in UI - - `borrowLimitsWithEmode` field in `UserData` — per-asset max borrow amounts under HE limits - - `availableToBorrowWithEmode` field in `UserData` — total available to borrow under HE limits + - `borrowLimitsWithHeMode` field in `UserData` — per-asset max borrow amounts under HE limits + - `availableToBorrowWithHeMode` field in `UserData` — total available to borrow under HE limits - New functions: - `determineHeCategory()` — derives active HE category from current borrows - `exceedsStandardBorrowLimit()` — checks whether total borrow exceeds standard collateral limit - - `getAvailableToBorrowWithEMode()` — available-to-borrow under HE limits; shows HE limit whenever all borrows are within a single HE category, regardless of whether standard limit is already exceeded - - `calculateRepayToExitEMode()` — how much to repay to drop back to standard mode + - `getAvailableToBorrowWithHeMode()` — available-to-borrow under HE limits; shows HE limit whenever all borrows are within a single HE category, regardless of whether standard limit is already exceeded + - `calculateRepayToExitHeMode()` — how much to repay to drop back to standard mode - New types: `PoolAssetHEConfig` (`title`, `assets`, `heCategory`) +### Changed + - Breaking rename of the public High Efficiency API from legacy `EMode` names to canonical `HeMode` names in exports and `UserData` fields ### Fixed - `predictHealthFactor` now applies HE thresholds only when borrow exceeds the standard collateral limit - `ClassicCollector` now fetches prices from all sources in parallel via `Promise.any` instead of sequentially diff --git a/docs/2. Core Concepts/2.7. High Efficiency Mode.md b/docs/2. Core Concepts/2.7. High Efficiency Mode.md new file mode 100644 index 00000000..fd84d8dc --- /dev/null +++ b/docs/2. Core Concepts/2.7. High Efficiency Mode.md @@ -0,0 +1,252 @@ +# High Efficiency (HE) mode + +## Table of Contents +1. [Overview](#overview) +2. [Pool-Level Configuration](#pool-level-configuration) +3. [Activation Rules](#activation-rules) +4. [What Changes When HE Becomes Active](#what-changes-when-he-becomes-active) +5. [Concrete Scenarios](#concrete-scenarios) +6. [UserData Fields](#userdata-fields) +7. [Common Misunderstandings](#common-misunderstandings) +8. [Integration Guidance](#integration-guidance) + +## Overview + +In this repository, the canonical user-facing name is `High Efficiency (HE) mode`. + +The purpose of HE mode is to allow higher capital efficiency for groups of closely related assets inside the same pool. The implementation is category-based: + +- The pool defines HE groups through `poolAssetsHEConfig` +- Each asset carries `heCategory`, `heCollateralFactor`, and `heLiquidationThreshold` +- Borrow-side category detection and supply-side benefit are related, but not identical + +This last point is the main source of confusion: + +- The HE category is determined from the borrowed assets +- The HE benefit is applied only to supplied assets that belong to the same category + +So HE is not simply "enabled because the user supplied an HE asset". It depends on both sides of the position. + +## Pool-Level Configuration + +At the pool level, categories are defined by `poolAssetsHEConfig`. + +Example from `MAINNET_CLASSIC_HE_POOL_CONFIG`: + +| HE category | Assets | +| --- | --- | +| `1` | `TON`, `tsTON` | +| `2` | `TUSDT`, `TUSDe` | + +This category list is the basis for all HE calculations in the SDK. + +**Section sources** +- [mainnet.ts](file://src/constants/pools/mainnet.ts#L255-L259) +- [Master.ts](file://src/types/Master.ts#L26-L38) + +## Activation Rules + +The SDK treats "HE-aware borrowing capacity" and "active HE mode" as two different states. + +### HE-aware borrowing capacity + +`getAvailableToBorrowWithHeMode()` can already show a larger borrowing limit before HE is fully active: + +- If the user has no debt, the function finds the best HE category supported by current supplied assets +- If the user already has debt and all borrowed assets belong to one HE category, the function uses that category for HE-aware borrow-limit calculation +- If debt is mixed across categories, it falls back to standard mode + +### Active HE mode + +`activeHeCategory` becomes positive only when both conditions are true: + +1. All borrowed assets belong to the same positive HE category +2. Total debt exceeds the standard borrow limit + +Until both are true, the account is still treated as standard for health and liquidation calculations. + +**Section sources** +- [math.ts](file://src/api/math.ts#L185-L245) +- [math.ts](file://src/api/math.ts#L367-L483) +- [math.ts](file://src/api/math.ts#L694-L738) +- [parser.ts](file://src/api/parser.ts#L394-L405) + +## What Changes When HE Becomes Active + +When HE becomes active for category `N`: + +- Borrowing power uses `heCollateralFactor` for supplied assets in category `N` +- Health and liquidation calculations use `heLiquidationThreshold` for supplied assets in category `N` +- Supplied assets outside category `N` still use their standard coefficients + +This means HE is selective. Even with an active category, only matching supplied collateral receives the HE benefit. + +**Section sources** +- [math.ts](file://src/api/math.ts#L384-L432) +- [math.ts](file://src/api/math.ts#L722-L732) + +## Concrete Scenarios + +Assume the pool categories are: + +- Category `1`: `TON`, `tsTON` +- Category `2`: `TUSDT`, `TUSDe` + +The scenarios below describe the SDK behavior without inventing specific LTV numbers. + +### Scenario 1: Supply `TON`, no debt yet + +Position: + +- Supply: `TON` +- Borrow: none + +Result: + +- `predictedHeCategory = 1` +- `activeHeCategory = -1` +- `availableToBorrowWithHeMode` can be larger than `availableToBorrow` + +Why: + +- The user has no debt, so the SDK looks at supplied assets and finds the best HE category available for a future borrow +- HE is not active yet because there is no debt position + +### Scenario 2: Supply `TON`, borrow a small amount of `tsTON` + +Position: + +- Supply: `TON` +- Borrow: `tsTON` +- Total debt is still within the standard borrow limit + +Result: + +- All debt belongs to category `1` +- `predictedHeCategory = 1` +- `activeHeCategory = -1` +- `availableToBorrowWithHeMode` is calculated for category `1` +- Health and liquidation still use standard thresholds + +Why: + +- `determineHeCategory()` looks only at debt, so it sees a single HE category +- But the account has not crossed the standard borrow limit yet +- Therefore the SDK exposes HE-aware borrowing headroom, while the position is still standard for risk calculations + +This is the most non-obvious state in the current implementation. + +### Scenario 3: Supply `TON`, keep borrowing `tsTON` until the standard limit is exceeded + +Position: + +- Supply: `TON` +- Borrow: `tsTON` +- Total debt is now above the standard borrow limit + +Result: + +- `predictedHeCategory = 1` +- `activeHeCategory = 1` +- Borrowing power for supplied `TON` can use `heCollateralFactor` +- Health and liquidation for supplied `TON` can use `heLiquidationThreshold` + +Why: + +- Debt is still entirely inside category `1` +- The account has now crossed the standard borrow limit +- This is the point where HE becomes truly active + +### Scenario 4: Supply `TON`, borrow `TUSDT` + +Position: + +- Supply: `TON` (category `1`) +- Borrow: `TUSDT` (category `2`) + +Result: + +- Debt-side category is `2` +- If this is the only debt, `determineHeCategory()` sees a single category and can return `2` +- But supplied `TON` is not in category `2` +- As a result, category `2` does not get extra HE benefit from the supplied `TON` + +Practical effect: + +- If the account is still within the standard limit, `activeHeCategory = -1` +- If the account exceeds the standard limit, `activeHeCategory` can become `2` +- Even then, the supplied `TON` still uses standard coefficients, because HE benefits apply only to supplied assets in the active category + +This is another easy place to misunderstand the feature. A single-category debt is not enough to guarantee a useful HE boost. + +### Scenario 5: Supply `TON`, borrow both `tsTON` and `TUSDT` + +Position: + +- Supply: `TON` +- Borrow: `tsTON` and `TUSDT` + +Result: + +- Debt spans categories `1` and `2` +- `determineHeCategory()` returns `-1` +- `activeHeCategory = -1` +- `availableToBorrowWithHeMode` falls back to the standard calculation + +Why: + +- Mixed borrow categories disable HE immediately + +**Section sources** +- [math.ts](file://src/api/math.ts#L209-L245) +- [math.ts](file://src/api/math.ts#L367-L483) +- [math.ts](file://src/api/math.ts#L694-L738) +- [tests/supply_withdraw_classic_he.ts](file://tests/supply_withdraw_classic_he.ts#L27-L29) + +## UserData Fields + +The HE-related fields in `UserDataActive` have different purposes: + +| Field | Meaning | +| --- | --- | +| `availableToBorrowWithHeMode` | Borrowing capacity from the HE-aware calculation | +| `borrowLimitsWithHeMode` | Per-asset borrow limits derived from `availableToBorrowWithHeMode` | +| `predictedHeCategory` | The category used by the HE-aware borrow calculation | +| `activeHeCategory` | The category that is actually active for risk calculations, or `-1` if HE is inactive | + +The important distinction: + +- `predictedHeCategory` answers: "Which category is the HE-aware borrow calculation using?" +- `activeHeCategory` answers: "Is the account currently in active HE mode for health/liquidation?" + +When there is no debt, `predictedHeCategory` behaves like a true forward-looking category for the next borrow. Once debt exists, it can instead reflect the current single-category debt being used by `getAvailableToBorrowWithHeMode()`. + +**Section sources** +- [User.ts](file://src/types/User.ts#L62-L78) +- [parser.ts](file://src/api/parser.ts#L394-L500) + +## Common Misunderstandings + +- HE is not activated by supplied collateral alone. Debt composition matters. +- The HE category is determined from the borrowed assets, not from the supplied assets. +- The HE benefit applies only to supplied assets in the same category as the debt-side HE category. +- `predictedHeCategory` does not mean HE is currently active. +- `activeHeCategory = -1` can coexist with a larger `availableToBorrowWithHeMode`. +- Leaving active HE mode does not require changing debt category. Repaying until the position fits inside the standard borrow limit is enough. + +For the last point, use `calculateRepayToExitHeMode()`. + +**Section sources** +- [math.ts](file://src/api/math.ts#L248-L365) +- [math.ts](file://src/api/math.ts#L367-L483) + +## Integration Guidance + +For front-end and integration logic: + +- Use `activeHeCategory` as the authoritative signal that health and liquidation are currently using HE thresholds +- Use `availableToBorrowWithHeMode` and `borrowLimitsWithHeMode` as UI guidance for HE-aware borrowing headroom +- Explain `predictedHeCategory` to users as a potential or calculation category, not as proof that the account is already in active HE mode +- If you show both standard and HE-aware limits, label them clearly to avoid implying that HE is already active + +This wording matches the current SDK logic and avoids the most common UI misunderstanding: showing HE borrow headroom as if it were already the live liquidation regime. diff --git a/docs/2. Core Concepts/README.md b/docs/2. Core Concepts/README.md index 89c5773c..2aab63d3 100644 --- a/docs/2. Core Concepts/README.md +++ b/docs/2. Core Concepts/README.md @@ -10,3 +10,4 @@ This section contains documentation for Core Concepts. - [2.4. Subaccounts](./2.4. Subaccounts.md) - [2.5. Price Validation And Medianization](./2.5. Price Validation And Medianization.md) - [2.6. Jetton Vs Ton Asset Handling](./2.6. Jetton Vs Ton Asset Handling.md) +- [2.7. High Efficiency Mode](./2.7. High Efficiency Mode.md) diff --git a/docs/4. Api Reference/4.3. User Interaction.md b/docs/4. Api Reference/4.3. User Interaction.md index cd83dd09..69185d04 100644 --- a/docs/4. Api Reference/4.3. User Interaction.md +++ b/docs/4. Api Reference/4.3. User Interaction.md @@ -7,12 +7,13 @@ 2. [Initialization and Provider Setup](#initialization-and-provider-setup) 3. [User State Synchronization](#user-state-synchronization) 4. [Data Parsing and Type Mapping](#data-parsing-and-type-mapping) -5. [Integration with Oracle Parsers](#integration-with-oracle-parsers) -6. [Usage Patterns and Health Monitoring](#usage-patterns-and-health-monitoring) -7. [Principal Balances and Operation Eligibility](#principal-balances-and-operation-eligibility) -8. [Error Scenarios](#error-scenarios) -9. [Master Contract Collaboration](#master-contract-collaboration) -10. [State Transition Diagram](#state-transition-diagram) +5. [High Efficiency (HE) Mode Fields](#high-efficiency-he-mode-fields) +6. [Integration with Oracle Parsers](#integration-with-oracle-parsers) +7. [Usage Patterns and Health Monitoring](#usage-patterns-and-health-monitoring) +8. [Principal Balances and Operation Eligibility](#principal-balances-and-operation-eligibility) +9. [Error Scenarios](#error-scenarios) +10. [Master Contract Collaboration](#master-contract-collaboration) +11. [State Transition Diagram](#state-transition-diagram) ## Initialization and Provider Setup @@ -96,14 +97,18 @@ class UserData { +fullyParsed : boolean +withdrawalLimits : Dictionary~bigint, bigint~ +borrowLimits : Dictionary~bigint, bigint~ ++borrowLimitsWithHeMode : Dictionary~bigint, bigint~ +supplyBalance : bigint +borrowBalance : bigint +availableToBorrow : bigint ++availableToBorrowWithHeMode : bigint +limitUsedPercent : number +limitUsed : bigint +healthFactor : number +liquidationData : LiquidationData +havePrincipalWithoutPrice : boolean ++predictedHeCategory : number ++activeHeCategory : number } class UserLiteData { <> @@ -128,14 +133,18 @@ class UserDataActive { <> +withdrawalLimits : Dictionary~bigint, bigint~ +borrowLimits : Dictionary~bigint, bigint~ ++borrowLimitsWithHeMode : Dictionary~bigint, bigint~ +supplyBalance : bigint +borrowBalance : bigint +availableToBorrow : bigint ++availableToBorrowWithHeMode : bigint +limitUsedPercent : number +limitUsed : bigint +healthFactor : number +liquidationData : LiquidationData +havePrincipalWithoutPrice : boolean ++predictedHeCategory : number ++activeHeCategory : number } class UserDataInactive { <> @@ -173,11 +182,55 @@ The `parseUserData` function computes: - **Health Factor**: Ratio of collateral value to debt limit - **Liquidation Status**: Whether user is eligible for liquidation - **Operation Limits**: Maximum withdraw/borrow amounts +- **HE-aware Borrow Metrics**: Borrow capacity and per-asset limits under HE-aware calculation **Section sources** - [parser.ts](file://src/api/parser.ts#L358-L457) - [User.ts](file://src/types/User.ts#L1-L119) +## High Efficiency (HE) Mode Fields + +The canonical user-facing name is `High Efficiency (HE) mode`. + +`parseUserData()` exposes both standard borrowing fields and HE-aware borrowing fields: + +- `availableToBorrow` +- `borrowLimits` +- `availableToBorrowWithHeMode` +- `borrowLimitsWithHeMode` +- `predictedHeCategory` +- `activeHeCategory` + +These fields are easy to confuse, because they represent two different layers of logic: + +- The HE-aware borrow calculation answers: "How much could this account borrow if HE rules for one category are applied?" +- The active HE state answers: "Is the account already using HE thresholds for health and liquidation?" + +Field-by-field meaning: + +| Field | Meaning | +| --- | --- | +| `availableToBorrow` | Standard borrow headroom using standard collateral factors | +| `borrowLimits` | Per-asset standard borrow limits | +| `availableToBorrowWithHeMode` | HE-aware borrow headroom from `getAvailableToBorrowWithHeMode()` | +| `borrowLimitsWithHeMode` | Per-asset limits derived from the HE-aware headroom | +| `predictedHeCategory` | The category returned by the HE-aware borrow calculation | +| `activeHeCategory` | The category that is actually active for risk calculations, or `-1` if inactive | + +Important behavioral detail: + +- `predictedHeCategory` can be positive while `activeHeCategory` is still `-1` +- This happens when the account has a valid single-category HE borrowing path, but total debt has not yet exceeded the standard borrow limit +- In that state, the UI can show HE-aware borrow headroom, but health and liquidation still follow standard thresholds + +For a full walkthrough with concrete position examples, see [High Efficiency (HE) mode](../2. Core Concepts/2.7. High Efficiency Mode.md). + +**Section sources** +- [parser.ts](file://src/api/parser.ts#L394-L500) +- [math.ts](file://src/api/math.ts#L367-L483) +- [math.ts](file://src/api/math.ts#L694-L738) +- [User.ts](file://src/types/User.ts#L62-L78) + ## Integration with Oracle Parsers While `UserContract` itself does not directly use oracle parsers, it relies on price data that is typically obtained through `OracleParser` implementations. @@ -424,4 +477,4 @@ The user contract transitions between `Active` and `Inactive` states based on bl - [AbstractMaster.ts](file://src/contracts/AbstractMaster.ts#L1-L422) - [AbstractOracleParser.ts](file://src/api/parsers/AbstractOracleParser.ts#L1-L16) - [ClassicOracleParser.ts](file://src/api/parsers/ClassicOracleParser.ts#L1-L20) -- [PythOracleParser.ts](file://src/api/parsers/PythOracleParser.ts#L1-L34) \ No newline at end of file +- [PythOracleParser.ts](file://src/api/parsers/PythOracleParser.ts#L1-L34) diff --git a/src/api/math.ts b/src/api/math.ts index faaac5c2..b708db9a 100644 --- a/src/api/math.ts +++ b/src/api/math.ts @@ -245,7 +245,7 @@ export function determineHeCategory( return heCategory > 0 ? heCategory : -1; } -export function calculateRepayToExitEMode( +export function calculateRepayToExitHeMode( assetsConfig: ExtendedAssetsConfig, assetsData: ExtendedAssetsData, principals: Dictionary, @@ -364,7 +364,7 @@ export function calculateRepayToExitEMode( }; } -export function getAvailableToBorrowWithEMode( +export function getAvailableToBorrowWithHeMode( assetsConfig: ExtendedAssetsConfig, assetsData: ExtendedAssetsData, principals: Dictionary, @@ -439,10 +439,10 @@ export function getAvailableToBorrowWithEMode( }; } - const availableToBorrowWithoutEmode = calculateForHeCategory(0); + const availableToBorrowWithoutHeMode = calculateForHeCategory(0); if (!checkNotInDebtAtAll(principals)) { return { - availableToBorrow: availableToBorrowWithoutEmode, + availableToBorrow: availableToBorrowWithoutHeMode, heCategory: 0, }; } @@ -468,7 +468,7 @@ export function getAvailableToBorrowWithEMode( } let bestHeCategory = 0; - let bestAvailableToBorrow = availableToBorrowWithoutEmode; + let bestAvailableToBorrow = availableToBorrowWithoutHeMode; for (const heCategory of availableHeCategories) { const availableToBorrow = calculateForHeCategory(heCategory); if (availableToBorrow > bestAvailableToBorrow) { @@ -558,7 +558,7 @@ export function calculateMaximumWithdrawAmount( if (assetConfig.collateralFactor == 0n) { maxAmountToReclaim = oldPresentValue.amount; } else if (price > 0) { - const { availableToBorrow: borrowable, heCategory } = getAvailableToBorrowWithEMode( + const { availableToBorrow: borrowable, heCategory } = getAvailableToBorrowWithHeMode( assetsConfig, assetsData, principals, @@ -593,7 +593,7 @@ export function calculateMaximumWithdrawAmount( const price = prices.get(assetId) as bigint; return ( - (getAvailableToBorrowWithEMode( + (getAvailableToBorrowWithHeMode( assetsConfig, assetsData, principals, diff --git a/src/api/parser.ts b/src/api/parser.ts index 14115c18..d02615c5 100644 --- a/src/api/parser.ts +++ b/src/api/parser.ts @@ -23,7 +23,7 @@ import { exceedsStandardBorrowLimit, getAssetLiquidityMinusReserves, getAvailableToBorrow, - getAvailableToBorrowWithEMode, + getAvailableToBorrowWithHeMode, presentValue, } from './math'; import { OracleParser } from './parsers/AbstractOracleParser'; @@ -338,7 +338,7 @@ export function parseUserData( const withdrawalLimits = Dictionary.empty(); const borrowLimits = Dictionary.empty(); - const borrowLimitsWithEmode = Dictionary.empty(); + const borrowLimitsWithHeMode = Dictionary.empty(); let supplyBalance = 0n; let borrowBalance = 0n; @@ -391,8 +391,8 @@ export function parseUserData( prices, masterConstants, ); - const { availableToBorrow: availableToBorrowWithEmode, heCategory: predictedHeCategory } = - getAvailableToBorrowWithEMode( + const { availableToBorrow: availableToBorrowWithHeMode, heCategory: predictedHeCategory } = + getAvailableToBorrowWithHeMode( assetsConfig, assetsData, userLiteData.realPrincipals, @@ -431,7 +431,7 @@ export function parseUserData( if (!prices.has(asset.assetId)) { borrowLimits.set(asset.assetId, 0n); - borrowLimitsWithEmode.set(asset.assetId, 0n); + borrowLimitsWithHeMode.set(asset.assetId, 0n); continue; } @@ -446,25 +446,25 @@ export function parseUserData( ), ); - borrowLimitsWithEmode.set( + borrowLimitsWithHeMode.set( asset.assetId, bigIntMax( 0n, bigIntMin( - (availableToBorrowWithEmode * 10n ** assetConfig.decimals) / prices.get(asset.assetId)!, + (availableToBorrowWithHeMode * 10n ** assetConfig.decimals) / prices.get(asset.assetId)!, assetLiquidityMinusReserves, ), ), ); } - const limitUsed = borrowBalance + availableToBorrowWithEmode; + const limitUsed = borrowBalance + availableToBorrowWithHeMode; const limitUsedPercent = limitUsed === 0n ? 0 : Number( BigInt(1e9) - - (availableToBorrowWithEmode * BigInt(1e9)) / (borrowBalance + availableToBorrowWithEmode), + (availableToBorrowWithHeMode * BigInt(1e9)) / (borrowBalance + availableToBorrowWithHeMode), ) / 1e7; let healthFactor = 1; @@ -486,11 +486,11 @@ export function parseUserData( ...userLiteData, withdrawalLimits: withdrawalLimits, borrowLimits: borrowLimits, - borrowLimitsWithEmode: borrowLimitsWithEmode, + borrowLimitsWithHeMode: borrowLimitsWithHeMode, supplyBalance: supplyBalance, borrowBalance: borrowBalance, availableToBorrow: availableToBorrow, - availableToBorrowWithEmode: availableToBorrowWithEmode, + availableToBorrowWithHeMode: availableToBorrowWithHeMode, predictedHeCategory: predictedHeCategory, limitUsedPercent: limitUsedPercent, limitUsed: limitUsed, diff --git a/src/types/User.ts b/src/types/User.ts index 62b1f326..3a99f268 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -62,12 +62,12 @@ export type UserLiteData = { export type UserDataActive = UserLiteData & { withdrawalLimits: Dictionary; borrowLimits: Dictionary; - borrowLimitsWithEmode: Dictionary; + borrowLimitsWithHeMode: Dictionary; repayLimits?: Dictionary; supplyBalance: bigint; borrowBalance: bigint; availableToBorrow: bigint; - availableToBorrowWithEmode: bigint; + availableToBorrowWithHeMode: bigint; limitUsedPercent: number; limitUsed: bigint; healthFactor: number; diff --git a/tests/check_user_lp_copy.ts b/tests/check_user_lp_copy.ts index 4bcd35bb..fec3c3d3 100644 --- a/tests/check_user_lp_copy.ts +++ b/tests/check_user_lp_copy.ts @@ -30,13 +30,13 @@ async function main() { console.log('predictedHeCategory:', data.predictedHeCategory); console.log('activeHeCategory:', data.activeHeCategory); - console.log('availableToBorrowWithEmode:', data.availableToBorrowWithEmode?.toString()); + console.log('availableToBorrowWithHeMode:', data.availableToBorrowWithHeMode?.toString()); console.log('\nAll borrow limits:'); for (const asset of MAINNET_CLASSIC_HE_POOL_CONFIG.poolAssetsConfig) { - const emode = data.borrowLimitsWithEmode?.get(asset.assetId); + const heMode = data.borrowLimitsWithHeMode?.get(asset.assetId); const normal = data.borrowLimits?.get(asset.assetId); - console.log(` ${asset.name}: normal=${normal?.toString()} emode=${emode?.toString()}`); + console.log(` ${asset.name}: normal=${normal?.toString()} heMode=${heMode?.toString()}`); } console.log('\nPrincipals:'); diff --git a/tests/math/heModeApi.test.ts b/tests/math/heModeApi.test.ts new file mode 100644 index 00000000..66459ea3 --- /dev/null +++ b/tests/math/heModeApi.test.ts @@ -0,0 +1,141 @@ +import { Cell, Dictionary } from '@ton/core'; +import * as sdk from '../../src'; +import { + AssetConfig, + ExtendedAssetData, + FakeCollector, + MASTER_CONSTANTS, + NULL_ADDRESS, + PoolConfig, + UserLiteData, + TON_MAINNET, + TSTON_MAINNET, +} from '../../src'; + +function createAssetConfig(overrides: Partial = {}): AssetConfig { + return { + jwAddress: 0n, + decimals: 0n, + collateralFactor: 7000n, + liquidationThreshold: 7500n, + liquidationBonus: 10500n, + baseBorrowRate: 0n, + borrowRateSlopeLow: 0n, + borrowRateSlopeHigh: 0n, + supplyRateSlopeLow: 0n, + supplyRateSlopeHigh: 0n, + targetUtilization: 0n, + originationFee: 0n, + dust: 0n, + maxTotalSupply: 0n, + reserveFactor: 0n, + liquidationReserveFactor: 0n, + minPrincipalForRewards: 0n, + baseTrackingSupplySpeed: 0n, + baseTrackingBorrowSpeed: 0n, + borrowCap: 0n, + heCategory: 1, + heCollateralFactor: 9000n, + heLiquidationThreshold: 9500n, + ...overrides, + }; +} + +function createExtendedAssetData(): ExtendedAssetData { + return { + sRate: MASTER_CONSTANTS.FACTOR_SCALE, + bRate: MASTER_CONSTANTS.FACTOR_SCALE, + totalSupply: 1000n, + totalBorrow: 0n, + lastAccrual: 0n, + balance: 1000n, + trackingSupplyIndex: 0n, + trackingBorrowIndex: 0n, + awaitedSupply: 0n, + supplyInterest: 0n, + borrowInterest: 0n, + supplyApy: 0, + borrowApy: 0, + }; +} + +function createHeFixture() { + const assetsConfig = Dictionary.empty() + .set(TON_MAINNET.assetId, createAssetConfig()) + .set(TSTON_MAINNET.assetId, createAssetConfig()); + + const assetsData = Dictionary.empty() + .set(TON_MAINNET.assetId, createExtendedAssetData()) + .set(TSTON_MAINNET.assetId, createExtendedAssetData()); + + const principals = Dictionary.empty().set(TON_MAINNET.assetId, 100n).set(TSTON_MAINNET.assetId, -80n); + const prices = Dictionary.empty().set(TON_MAINNET.assetId, 1n).set(TSTON_MAINNET.assetId, 1n); + const poolConfig: PoolConfig = { + masterAddress: NULL_ADDRESS, + masterVersion: 0, + masterConstants: MASTER_CONSTANTS, + poolAssetsConfig: [TON_MAINNET, TSTON_MAINNET], + poolAssetsHEConfig: [{ title: 'ton', assets: [TON_MAINNET, TSTON_MAINNET], heCategory: 1 }], + lendingCode: Cell.EMPTY, + collector: new FakeCollector(Dictionary.empty()), + }; + const userLiteData: UserLiteData = { + type: 'active', + codeVersion: 0, + masterAddress: NULL_ADDRESS, + ownerAddress: NULL_ADDRESS, + principals, + realPrincipals: principals, + state: 0, + balances: Dictionary.empty(), + trackingSupplyIndex: 0n, + trackingBorrowIndex: 0n, + dutchAuctionStart: 0, + backupCell: Cell.EMPTY, + rewards: Dictionary.empty(), + backupCell1: null, + backupCell2: null, + fullyParsed: false, + }; + + return { assetsConfig, assetsData, principals, prices, poolConfig, userLiteData }; +} + +describe('HeMode API surface', () => { + test('package root exposes only canonical HeMode function names', () => { + expect(sdk.getAvailableToBorrowWithHeMode).toBeDefined(); + expect(sdk.calculateRepayToExitHeMode).toBeDefined(); + + const sdkExports = sdk as Record; + expect(sdkExports.getAvailableToBorrowWithEMode).toBeUndefined(); + expect(sdkExports.calculateRepayToExitEMode).toBeUndefined(); + }); + + test('parseUserData returns renamed HeMode fields and omits legacy Emode fields', () => { + const { assetsConfig, assetsData, principals, prices, poolConfig, userLiteData } = createHeFixture(); + + const borrowHeadroom = sdk.getAvailableToBorrowWithHeMode( + assetsConfig, + assetsData, + principals, + prices, + MASTER_CONSTANTS, + poolConfig, + ); + expect(borrowHeadroom).toEqual({ availableToBorrow: 10n, heCategory: 1 }); + + const parsed = sdk.parseUserData(userLiteData, assetsData, assetsConfig, prices, poolConfig); + expect(parsed.type).toBe('active'); + if (parsed.type !== 'active') { + throw new Error('Expected active user data'); + } + + expect(parsed.availableToBorrowWithHeMode).toEqual(10n); + expect(parsed.borrowLimitsWithHeMode.get(TON_MAINNET.assetId)).toEqual(10n); + expect(parsed.borrowLimitsWithHeMode.get(TSTON_MAINNET.assetId)).toEqual(10n); + + const parsedRecord = parsed as Record; + expect(parsedRecord.availableToBorrowWithEmode).toBeUndefined(); + expect(parsedRecord.borrowLimitsWithEmode).toBeUndefined(); + }); +}); diff --git a/tests/supply_withdraw_classic_he.ts b/tests/supply_withdraw_classic_he.ts index 80f9afd3..abfabb4e 100644 --- a/tests/supply_withdraw_classic_he.ts +++ b/tests/supply_withdraw_classic_he.ts @@ -14,7 +14,7 @@ import { USDT_MAINNET, calculateHealthParams, determineHeCategory, - getAvailableToBorrowWithEMode, + getAvailableToBorrowWithHeMode, presentValue, } from '../src'; @@ -25,7 +25,7 @@ const POOL_CONFIG = TESTNET_CLASSIC_HE_POOL_CONFIG; const MAX_WITHDRAW_AMOUNT = 0xffffffffffffffffn; // Pool assets: TON (HE cat 1), tsTON (HE cat 1), USDT (HE cat 2), USDe (HE cat 2) -// HE mode activates when ALL borrowed assets belong to the same HE category (> 0) +// High Efficiency (HE) mode activates when ALL borrowed assets belong to the same HE category (> 0) // This gives higher collateral factors and liquidation thresholds const TON_CLIENT = new TonClient({ @@ -108,7 +108,7 @@ function printUserHealth(user: ReturnType>, mas } // Show available to borrow with HE - const { availableToBorrow, heCategory: borrowHeCategory } = getAvailableToBorrowWithEMode( + const { availableToBorrow, heCategory: borrowHeCategory } = getAvailableToBorrowWithHeMode( master.data!.assetsConfig, master.data!.assetsData, user.data.principals, @@ -204,7 +204,7 @@ async function supplyTON() { * * For withdraw to create a borrow position (and activate HE mode): * 1. Supply collateral (e.g. TON, HE cat 1) - * 2. Withdraw/borrow a correlated asset (e.g. tsTON, HE cat 1) → HE mode active + * 2. Withdraw/borrow a correlated asset (e.g. tsTON, HE cat 1) → High Efficiency (HE) mode active * OR withdraw a non-correlated asset (e.g. USDT, HE cat 2) → standard mode * * With HE mode active, you get higher CF/LT, meaning you can borrow more.