diff --git a/src/api/math.ts b/src/api/math.ts index d04971f2..971186d3 100644 --- a/src/api/math.ts +++ b/src/api/math.ts @@ -1,6 +1,5 @@ -import { Dictionary } from '@ton/core'; -import { UNDEFINED_ASSET } from '../constants/assets'; -import { +import type { Dictionary } from '@ton/core'; +import type { AgregatedBalances, AssetApy, AssetConfig, @@ -12,14 +11,15 @@ import { MasterConstants, PoolConfig, } from '../types/Master'; +import { UNDEFINED_ASSET } from '../constants/assets'; import { BalanceChangeType, BalanceType, - HealthParamsArgs, - LiquidationData, - PredictAPYArgs, - PredictHealthFactorArgs, - UserBalance, + type HealthParamsArgs, + type LiquidationData, + type PredictAPYArgs, + type PredictHealthFactorArgs, + type UserBalance, } from '../types/User'; import { addReserve, isBadDebt } from './liquidation'; @@ -177,6 +177,111 @@ export function checkNotInDebtAtAll(principals: Dictionary): boo return principals.values().every((x) => x >= 0n); } +/** + * Determines the active High Efficiency (e-mode) category for a user based on their debt positions. + * Returns the heCategory if all borrowed assets belong to the same e-mode group (heCategory > 0). + * Returns -1 if: + * - User has debts in multiple e-mode groups + * - User has debt in an asset with heCategory = 0 + * - User has no debt + * + * @param principals user principals dictionary + * @param assetsConfig assets config dictionary + * @returns heCategory number if e-mode is active, -1 otherwise + */ +export function determineHeCategory( + principals: Dictionary, + assetsConfig: ExtendedAssetsConfig, +): number { + let heCategory = -1; + + for (const [assetId, principal] of principals) { + if (principal >= 0n) { + continue; + } + + const assetConfig = assetsConfig.get(assetId); + if (!assetConfig) { + return -1; + } + + const assetHeCategory = assetConfig.heCategory; + if (assetHeCategory <= 0) { + return -1; + } + + if (heCategory === -1) { + heCategory = assetHeCategory; + } else if (heCategory !== assetHeCategory) { + return -1; + } + } + + return heCategory; +} + +/** + * Calculates borrow limit with e-mode support. + * For supply assets matching the active heCategory, uses heCollateralFactor/heLiquidationThreshold. + * For other assets, uses normal collateralFactor/liquidationThreshold. + * + * @param principals user principals dictionary + * @param prices asset prices dictionary + * @param assetsConfig assets config dictionary + * @param assetsData assets data dictionary + * @param masterConstants pool constants + * @param useCollateralFactor if true, uses collateralFactor; if false, uses liquidationThreshold + * @returns { limit, heCategory } - borrow limit and active heCategory + */ +export function calculateBorrowLimitWithEMode( + principals: Dictionary, + prices: Dictionary, + assetsConfig: ExtendedAssetsConfig, + assetsData: ExtendedAssetsData, + masterConstants: MasterConstants, + useCollateralFactor: boolean = true, +): { limit: bigint; heCategory: number } { + const heCategory = determineHeCategory(principals, assetsConfig); + let limit = 0n; + + for (const [assetId, principal] of principals) { + if (principal <= 0n) { + continue; + } + + if (!prices.has(assetId)) { + return { limit: 0n, heCategory: -1 }; + } + + const assetConfig = assetsConfig.get(assetId); + const assetData = assetsData.get(assetId); + if (!assetConfig || !assetData) { + continue; + } + + const price = prices.get(assetId)!; + const assetScale = 10n ** assetConfig.decimals; + const presentValueAmount = calculatePresentValue(assetData.sRate, principal, masterConstants); + + let factor: bigint; + if (useCollateralFactor) { + factor = + heCategory > 0 && assetConfig.heCategory === heCategory + ? BigInt(assetConfig.heCollateralFactor) + : assetConfig.collateralFactor; + } else { + factor = + heCategory > 0 && assetConfig.heCategory === heCategory + ? BigInt(assetConfig.heLiquidationThreshold) + : assetConfig.liquidationThreshold; + } + + limit += mulDiv(mulDiv(presentValueAmount, price, assetScale), factor, masterConstants.ASSET_COEFFICIENT_SCALE); + } + + return { limit, heCategory }; +} + export function getAgregatedBalances( assetsData: ExtendedAssetsData, assetsConfig: ExtendedAssetsConfig, @@ -210,6 +315,10 @@ export function getAgregatedBalances( return { totalSupply: user_total_supply, totalBorrow: user_total_borrow }; } +/** + * Calculates maximum withdraw amount with e-mode support. + * Uses heCollateralFactor for assets matching the active e-mode category. + */ export function calculateMaximumWithdrawAmount( assetsConfig: ExtendedAssetsConfig, assetsData: ExtendedAssetsData, @@ -233,18 +342,29 @@ export function calculateMaximumWithdrawAmount( return 0n; } - const borrowable = getAvailableToBorrow(assetsConfig, assetsData, principals, prices, masterConstants); + const { availableToBorrow: borrowable, heCategory } = getAvailableToBorrowWithEMode( + assetsConfig, + assetsData, + principals, + prices, + masterConstants, + ); const price = prices.get(assetId) as bigint; + let collateralFactor = assetConfig.collateralFactor; + if (heCategory > 0 && assetConfig.heCategory === heCategory) { + collateralFactor = BigInt(assetConfig.heCollateralFactor); + } + let maxAmountToReclaim = 0n; - if (assetConfig.collateralFactor == 0n) { + if (collateralFactor == 0n) { maxAmountToReclaim = oldPresentValue.amount; } else if (price > 0) { maxAmountToReclaim = bigIntMax( 0n, mulDiv( - mulDiv(borrowable, masterConstants.ASSET_COEFFICIENT_SCALE, assetConfig.collateralFactor), + mulDiv(borrowable, masterConstants.ASSET_COEFFICIENT_SCALE, collateralFactor), 10n ** assetConfig.decimals, price, ) - @@ -271,51 +391,70 @@ export function calculateMaximumWithdrawAmount( return withdrawAmountMax; } -export function getAvailableToBorrow( +/** + * Calculates available amount to borrow with e-mode support. + * Uses heCollateralFactor for assets matching the active e-mode category. + * + * @returns { availableToBorrow, heCategory } - available amount and active heCategory + */ +export function getAvailableToBorrowWithEMode( assetsConfig: ExtendedAssetsConfig, assetsData: ExtendedAssetsData, principals: Dictionary, prices: Dictionary, masterConstants: MasterConstants, -): bigint { - let borrowLimit = 0n; - let borrowAmount = 0n; - - for (const assetID of principals.keys()) { - const principal = principals.get(assetID) as bigint; +): { availableToBorrow: bigint; heCategory: number } { + const { limit: borrowLimit, heCategory } = calculateBorrowLimitWithEMode( + principals, + prices, + assetsConfig, + assetsData, + masterConstants, + true, + ); - if (principal == 0n) { + let borrowAmount = 0n; + for (const [assetId, principal] of principals) { + if (principal >= 0n) { continue; } - if (!prices.has(assetID)) { - return 0n; + if (!prices.has(assetId)) { + return { availableToBorrow: 0n, heCategory: -1 }; } - const assetConfig = assetsConfig.get(assetID) as AssetConfig; - const assetData = assetsData.get(assetID) as ExtendedAssetData; - const price = prices.get(assetID) as bigint; - - if (principal < 0n) { - borrowAmount += mulDiv( - calculatePresentValue(assetData.bRate, -principal, masterConstants), - price, - 10n ** assetConfig.decimals, - ); - } else if (principal > 0n) { - borrowLimit += mulDiv( - mulDiv( - calculatePresentValue(assetData.sRate, principal, masterConstants), - price, - 10n ** assetConfig.decimals, - ), - assetConfig.collateralFactor, - masterConstants.ASSET_COEFFICIENT_SCALE, - ); + const assetConfig = assetsConfig.get(assetId); + const assetData = assetsData.get(assetId); + if (!assetConfig || !assetData) { + continue; } + + const price = prices.get(assetId)!; + borrowAmount += mulDivC( + calculatePresentValue(assetData.bRate, -principal, masterConstants), + price, + 10n ** assetConfig.decimals, + ); } - return borrowLimit - borrowAmount; + return { availableToBorrow: borrowLimit - borrowAmount, heCategory }; +} + +export function getAvailableToBorrow( + assetsConfig: ExtendedAssetsConfig, + assetsData: ExtendedAssetsData, + principals: Dictionary, + prices: Dictionary, + masterConstants: MasterConstants, +): bigint { + const { availableToBorrow } = getAvailableToBorrowWithEMode( + assetsConfig, + assetsData, + principals, + prices, + masterConstants, + ); + return availableToBorrow; } /** @@ -350,7 +489,8 @@ export function presentValue( } /** - * Calculates health parameters of the specified user account based on its parameters + * Calculates health parameters of the specified user account based on its parameters. + * Supports e-mode: uses heLiquidationThreshold for assets matching the active e-mode category. * @param parameters */ export function calculateHealthParams(parameters: HealthParamsArgs) { @@ -358,6 +498,8 @@ export function calculateHealthParams(parameters: HealthParamsArgs) { const { ASSET_LIQUIDATION_THRESHOLD_SCALE } = poolConfig.masterConstants; + const heCategory = determineHeCategory(principals, assetsConfig); + let totalSupply = 0n; let totalDebt = 0n; let totalLimit = 0n; @@ -381,7 +523,11 @@ export function calculateHealthParams(parameters: HealthParamsArgs) { const assetWorth = (assetBalance.amount * assetPrice) / assetScale; if (assetBalance.type === BalanceType.supply) { totalSupply += assetWorth; - totalLimit += (assetWorth * assetConfig.liquidationThreshold) / ASSET_LIQUIDATION_THRESHOLD_SCALE; + const liquidationThreshold = + heCategory > 0 && assetConfig.heCategory === heCategory + ? BigInt(assetConfig.heLiquidationThreshold) + : assetConfig.liquidationThreshold; + totalLimit += (assetWorth * liquidationThreshold) / ASSET_LIQUIDATION_THRESHOLD_SCALE; } else if (assetBalance.type === BalanceType.borrow && assetConfig.dust < assetBalance.amount) { totalDebt += assetWorth; } @@ -399,13 +545,15 @@ export function calculateHealthParams(parameters: HealthParamsArgs) { totalDebt, totalLimit, totalSupply, + heCategory, isLiquidatable: _isLiquidable, isBadDebt: _isBadDebt, }; } /** - * Calculates liquidation data for greatest loan and collateral assets + * Calculates liquidation data for greatest loan and collateral assets. + * Supports e-mode: uses heLiquidationThreshold for assets matching the active e-mode category. * @param assetsConfig assets config dictionary * @param assetsData assets data dictionary * @param principals principals dictionary @@ -428,6 +576,7 @@ export function calculateLiquidationData( let totalLimit = 0n; const { ASSET_SRATE_SCALE, ASSET_BRATE_SCALE, COLLATERAL_WORTH_THRESHOLD } = poolConfig.masterConstants; + const heCategory = determineHeCategory(principals, assetsConfig); for (const asset of poolConfig.poolAssetsConfig) { const principal = principals.get(asset.assetId)!; @@ -441,16 +590,17 @@ export function calculateLiquidationData( const assetWorth = (bigAbs(balance) * prices.get(asset.assetId)!) / 10n ** assetConfig.decimals; if (balance > 0) { - totalLimit += - (assetWorth * assetConfig.liquidationThreshold) / poolConfig.masterConstants.ASSET_COEFFICIENT_SCALE; - // get the greatest collateral + const liquidationThreshold = + heCategory > 0 && assetConfig.heCategory === heCategory + ? BigInt(assetConfig.heLiquidationThreshold) + : assetConfig.liquidationThreshold; + totalLimit += (assetWorth * liquidationThreshold) / poolConfig.masterConstants.ASSET_COEFFICIENT_SCALE; if (assetWorth > collateralValue) { collateralValue = assetWorth; collateralAsset = asset; } } else if (balance < 0) { totalDebt += assetWorth; - // get the greatest loan if (assetWorth > loanValue) { loanValue = assetWorth; loanAsset = asset; @@ -515,6 +665,10 @@ export function calculateLiquidationData( }; } +/** + * Predicts health factor after a balance change. + * Supports e-mode: uses heLiquidationThreshold for assets matching the active e-mode category. + */ export function predictHealthFactor(args: PredictHealthFactorArgs): number { const healthParams = calculateHealthParams(args); const assetId = args.asset.assetId; @@ -529,9 +683,15 @@ export function predictHealthFactor(args: PredictHealthFactorArgs): number { const decimals = Number(assetConfig.decimals); - const currentBalance = (assetPrice * Number(currentAmount)) / Math.pow(10, decimals); + const currentBalance = (assetPrice * Number(currentAmount)) / 10 ** decimals; const changeType = args.balanceChangeType; + const heCategory = healthParams.heCategory; + const liquidationThreshold = + heCategory > 0 && assetConfig.heCategory === heCategory + ? assetConfig.heLiquidationThreshold + : Number(assetConfig.liquidationThreshold); + if (currentAmount != null && currentAmount != 0n) { if (changeType == BalanceChangeType.Borrow) { totalBorrow += @@ -543,11 +703,11 @@ export function predictHealthFactor(args: PredictHealthFactorArgs): number { totalBorrow -= currentBalance; } else if (changeType == BalanceChangeType.Withdraw) { totalLimit -= - (currentBalance * Number(assetConfig.liquidationThreshold)) / + (currentBalance * Number(liquidationThreshold)) / Number(args.poolConfig.masterConstants.ASSET_COEFFICIENT_SCALE); } else if (changeType == BalanceChangeType.Supply) { totalLimit += - (currentBalance * Number(assetConfig.liquidationThreshold)) / + (currentBalance * Number(liquidationThreshold)) / Number(args.poolConfig.masterConstants.ASSET_COEFFICIENT_SCALE); } } @@ -555,7 +715,7 @@ export function predictHealthFactor(args: PredictHealthFactorArgs): number { return 1; } - return Math.min(Math.max(1 - totalBorrow / totalLimit, 0), 1); // let's limit a result to zero below and one above + return Math.min(Math.max(1 - totalBorrow / totalLimit, 0), 1); } /** diff --git a/src/api/parser.ts b/src/api/parser.ts index b4bd642f..a39d28f4 100644 --- a/src/api/parser.ts +++ b/src/api/parser.ts @@ -130,8 +130,8 @@ export function createAssetConfig(): DictionaryValue { const baseTrackingBorrowSpeed = ref.loadUintBig(64); const borrowCap = ref.loadInt(64); const heCategory = ref.loadUint(8); - const heCollateralFactor = ref.loadUint(16); - const heLiquidationThreshold = ref.loadUint(16); + const heCollateralFactor = ref.loadUintBig(16); + const heLiquidationThreshold = ref.loadUintBig(16); return { jwAddress, @@ -242,8 +242,8 @@ export function parseUserLiteData( const userSlice = Cell.fromBase64(userDataBOC).beginParse(); const codeVersion = userSlice.loadCoins(); - const masterAddress = userSlice.loadAddress(); - const userAddress = userSlice.loadAddress(); + const masterAddress = userSlice.loadAddressAny(); + const userAddress = userSlice.loadAddressAny(); const realPrincipals = userSlice.loadDict(Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(64)); const principalsDict = Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(64)); const userState = userSlice.loadInt(64); @@ -256,18 +256,26 @@ export function parseUserLiteData( let backupCell1: Cell | null = null; let backupCell2: Cell | null = null; const bitsLeft = userSlice.remainingBits; - if (bitsLeft > 32) { + const refsLeft = userSlice.remainingRefs; + if (bitsLeft === 0 && refsLeft === 0) { + // Init format: no extra data after state + } else if (bitsLeft >= 64 + 64 + 32 && refsLeft >= 1) { + // Old format with tracking indexes trackingSupplyIndex = userSlice.loadUintBig(64); trackingBorrowIndex = userSlice.loadUintBig(64); dutchAuctionStart = userSlice.loadUint(32); backupCell = loadMyRef(userSlice); - } else { + } else if (bitsLeft >= 3 && refsLeft >= 1) { + // New format with rewards dict + maybe_refs rewards = userSlice.loadDict(Dictionary.Keys.BigUint(256), createUserRewards()); - backupCell1 = userSlice.loadMaybeRef(); - backupCell2 = userSlice.loadMaybeRef(); + if (userSlice.remainingBits >= 2) { + backupCell1 = userSlice.loadMaybeRef(); + backupCell2 = userSlice.loadMaybeRef(); + } } - userSlice.endParse(); + // Skip remaining data if any (for forward compatibility) + // userSlice.endParse(); const userBalances = Dictionary.empty(); for (const [_, asset] of Object.entries(poolAssetsConfig)) { diff --git a/src/constants/pools/testnet.ts b/src/constants/pools/testnet.ts index 9f87eb44..32d1d98e 100644 --- a/src/constants/pools/testnet.ts +++ b/src/constants/pools/testnet.ts @@ -1,10 +1,19 @@ -import { HexString } from '@pythnetwork/hermes-client'; +import type { HexString } from '@pythnetwork/hermes-client'; +import type { PoolConfig } from '../../types/Master'; +import type { EvaaRewardsConfig } from '../../types/MasterRewards'; import { Address, Dictionary } from '@ton/core'; -import { FEED_ID, FeedMapItem } from '../../api/feeds'; +import { FEED_ID, type FeedMapItem } from '../../api/feeds'; import { ClassicCollector, DefaultPythPriceSourcesConfig, PythCollector } from '../../oracles'; -import { PoolConfig } from '../../types/Master'; -import { EvaaRewardsConfig } from '../../types/MasterRewards'; -import { ASSET_ID, EUSDT_TESTNET, JUSDC_TESTNET, TON_TESTNET } from '../assets'; +import { + ASSET_ID, + EUSDT_TESTNET, + JUSDC_TESTNET, + TON_MAINNET, + TON_TESTNET, + TSTON_MAINNET, + USDE_MAINNET, + USDT_MAINNET, +} from '../assets'; import { EVAA_MASTER_TESTNET_CLASSIC_TOB_AUDITED, EVAA_MASTER_TESTNET_PYTH_TOB_AUDITED, @@ -61,6 +70,19 @@ export const TESTNET_CLASSIC_POOL_CONFIG_TOB_AUDITED: PoolConfig = { poolAssetsConfig: TESTNET_POOL_ASSETS_CONFIG, }; +export const TESTNET_CLASSIC_HE_POOL_CONFIG: PoolConfig = { + masterAddress: Address.parse('EQBykKj3k97Mx_EEGwzmnqFLteFRAt0BIg-ig96ifNkdV3Wn'), + masterVersion: 0, + masterConstants: MASTER_CONSTANTS, + collector: new ClassicCollector({ + poolAssetsConfig: [TON_MAINNET, TSTON_MAINNET, USDT_MAINNET, USDE_MAINNET], + minimalOracles: 1, + evaaOracles: ORACLES_TESTNET, + }), + lendingCode: LENDING_CODE, + poolAssetsConfig: [TON_MAINNET, TSTON_MAINNET, USDT_MAINNET, USDE_MAINNET], +}; + export const TESTNET_MASTER_REWARD_CONFIG: EvaaRewardsConfig = { adminAddress: EVAA_REWARDS_MASTER_TESTNET, evaaMasterAddress: new Address(0, Buffer.alloc(32, 0)), diff --git a/src/types/Master.ts b/src/types/Master.ts index 1b76a88b..91749fab 100644 --- a/src/types/Master.ts +++ b/src/types/Master.ts @@ -66,8 +66,8 @@ export type AssetConfig = { baseTrackingBorrowSpeed: bigint; borrowCap: number | bigint; heCategory: number; - heCollateralFactor: number; - heLiquidationThreshold: number; + heCollateralFactor: bigint; + heLiquidationThreshold: bigint; }; export type AssetData = { diff --git a/src/types/User.ts b/src/types/User.ts index 6f9ca1d9..0c5466e9 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -1,4 +1,4 @@ -import { Address, Cell, Dictionary } from '@ton/core'; +import { Address, Cell, Dictionary, ExternalAddress } from '@ton/core'; import { AssetConfig, AssetData, @@ -43,8 +43,8 @@ export type LiquidationData = LiquidableData | NonLiquidableData; export type UserLiteData = { type: 'active'; codeVersion: number; - masterAddress: Address; - ownerAddress: Address; + masterAddress: Address | ExternalAddress | null; + ownerAddress: Address | ExternalAddress | null; principals: Dictionary; realPrincipals: Dictionary; // principals before applying dusts state: number; diff --git a/tests/supply_withdraw_classic_he.ts b/tests/supply_withdraw_classic_he.ts new file mode 100644 index 00000000..80f9afd3 --- /dev/null +++ b/tests/supply_withdraw_classic_he.ts @@ -0,0 +1,348 @@ +import 'dotenv/config'; + +import { mnemonicToWalletKey } from '@ton/crypto'; +import { Cell, toNano, TonClient, WalletContractV4 } from '@ton/ton'; +import { + BalanceType, + ClassicCollector, + EvaaMasterClassic, + EvaaUser, + FEES, + TESTNET_CLASSIC_HE_POOL_CONFIG, + TON_MAINNET, + USDE_MAINNET, + USDT_MAINNET, + calculateHealthParams, + determineHeCategory, + getAvailableToBorrowWithEMode, + presentValue, +} from '../src'; + +// ==================== Configuration ==================== + +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) +// This gives higher collateral factors and liquidation thresholds + +const TON_CLIENT = new TonClient({ + endpoint: 'https://toncenter.com/api/v2/jsonRPC', + apiKey: process.env.RPC_API_KEY_MAINNET, +}); + +// ==================== Helpers ==================== + +async function getWallet() { + const mnemonic = process.env.MAINNET_WALLET_MNEMONIC; + if (!mnemonic) { + throw new Error('MAINNET_WALLET_MNEMONIC is not defined in environment variables'); + } + const keyPair = await mnemonicToWalletKey(mnemonic.split(' ')); + const wallet = TON_CLIENT.open( + WalletContractV4.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + const sender = { + address: wallet.address, + send: wallet.sender(keyPair.secretKey).send, + }; + return { wallet, sender, keyPair }; +} + +async function getMasterAndPrices() { + const master = TON_CLIENT.open(new EvaaMasterClassic({ poolConfig: POOL_CONFIG, debug: true })); + await master.getSync(); + + if (!master.data?.assetsData || !master.data?.assetsConfig) { + throw new Error('Failed to sync master contract data'); + } + + const collector = POOL_CONFIG.collector as ClassicCollector; + const prices = await collector.getPrices(); + + return { master, prices, collector }; +} + +async function getUserData( + master: ReturnType>, + walletAddress: any, + prices: any, +) { + const userContract = TON_CLIENT.open(master.openUserContract(walletAddress)); + await userContract.getSync(master.data!.assetsData, master.data!.assetsConfig, prices.dict); + return userContract; +} + +function printUserHealth(user: ReturnType>, master: any, prices: any) { + if (!user.data || user.data.type === 'inactive') { + console.log(' User SC is inactive (no positions)'); + return; + } + + console.dir(user.data.principals); + + const healthParams = calculateHealthParams({ + principals: user.data.principals, + prices: prices.dict, + assetsData: master.data!.assetsData, + assetsConfig: master.data!.assetsConfig, + poolConfig: POOL_CONFIG, + }); + + const heCategory = determineHeCategory(user.data.principals, master.data!.assetsConfig); + + console.log(` HE Category: ${heCategory > 0 ? heCategory : 'none (standard mode)'}`); + console.log(` Total Supply: ${healthParams.totalSupply}`); + console.log(` Total Debt: ${healthParams.totalDebt}`); + console.log(` Total Limit: ${healthParams.totalLimit}`); + console.log(` Liquidatable: ${healthParams.isLiquidatable}`); + + if (healthParams.totalLimit > 0n) { + const healthFactor = 1 - Number(healthParams.totalDebt) / Number(healthParams.totalLimit); + console.log(` Health Factor: ${(healthFactor * 100).toFixed(2)}%`); + } + + // Show available to borrow with HE + const { availableToBorrow, heCategory: borrowHeCategory } = getAvailableToBorrowWithEMode( + master.data!.assetsConfig, + master.data!.assetsData, + user.data.principals, + prices.dict, + POOL_CONFIG.masterConstants, + ); + console.log(` Available to Borrow: ${availableToBorrow} (HE cat: ${borrowHeCategory})`); + + // Print per-asset balances + console.log(' Positions:'); + for (const asset of POOL_CONFIG.poolAssetsConfig) { + const principal = user.data.principals.get(asset.assetId); + if (principal && principal !== 0n) { + const assetData = master.data!.assetsData.get(asset.assetId)!; + const balance = presentValue(assetData.sRate, assetData.bRate, principal, POOL_CONFIG.masterConstants); + const side = balance.type === BalanceType.supply ? 'SUPPLY' : 'BORROW'; + console.log(` ${asset.name}: ${balance.amount} (${side})`); + } + } +} + +// ==================== Supply ==================== + +/** + * Supply a jetton asset (USDT, USDe, tsTON) into the HE pool. + * + * To activate HE mode, supply correlated assets and borrow within the same category: + * - Category 1: TON, tsTON (TON derivatives) + * - Category 2: USDT, USDe (stablecoins) + */ +async function supplyJetton() { + const { wallet, sender } = await getWallet(); + const { master } = await getMasterAndPrices(); + + console.log(`Wallet: ${wallet.address}`); + console.log(`Balance: ${await wallet.getBalance()}`); + + // --- Supply USDT (HE category 2) --- + const supplyAsset = USDT_MAINNET; + const supplyAmount = 100_000n; // 0.1 USDT (6 decimals) + + console.log(`\nSupplying ${supplyAmount} ${supplyAsset.name}...`); + + await master.sendSupply(sender, FEES.SUPPLY_WITHDRAW + FEES.JETTON_FWD, { + queryID: 0n, + includeUserCode: true, + amount: supplyAmount, + userAddress: wallet.address, + asset: supplyAsset, + payload: Cell.EMPTY, + customPayloadRecipient: wallet.address, + subaccountId: 0, + customPayloadSaturationFlag: false, + returnRepayRemainingsFlag: false, + }); + + console.log(`Supply ${supplyAsset.name} sent!`); +} + +/** + * Supply TON into the HE pool. + */ +async function supplyTON() { + const { wallet, sender } = await getWallet(); + const { master } = await getMasterAndPrices(); + + console.log(`Wallet: ${wallet.address}`); + + const supplyAmount = toNano('1'); // 1 TON + + console.log(`\nSupplying ${supplyAmount} TON...`); + + await master.sendSupply(sender, supplyAmount + FEES.SUPPLY_WITHDRAW, { + queryID: 0n, + includeUserCode: true, + amount: supplyAmount, + userAddress: wallet.address, + asset: TON_MAINNET, + payload: Cell.EMPTY, + customPayloadRecipient: wallet.address, + subaccountId: 0, + customPayloadSaturationFlag: false, + returnRepayRemainingsFlag: false, + }); + + console.log('Supply TON sent!'); +} + +// ==================== Withdraw (Borrow) ==================== + +/** + * Withdraw/borrow an asset from the HE pool. + * + * 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 + * 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. + */ +async function withdrawJetton() { + const { wallet, sender } = await getWallet(); + const { master, prices, collector } = await getMasterAndPrices(); + + console.log(`Wallet: ${wallet.address}`); + + // First, get user data to fetch principals for price filtering + const user = await getUserData(master, wallet.address, prices); + + console.log('\n--- Before Withdraw ---'); + printUserHealth(user, master, prices); + + if (!user.liteData) { + throw new Error('User has no positions. Supply first.'); + } + + // --- Withdraw USDe (HE category 2, same as USDT) --- + // If user has only USDE supplied, withdrawing USDe activates HE mode (cat 2) + // giving ~80% CF instead of ~40% + const withdrawAsset = USDE_MAINNET; + const withdrawAmount = 100_000n; // 0.1 USDe (6 decimals) + + // Get TWAP prices required for withdraw with borrow + const withdrawPrices = await collector.getPricesForWithdraw( + user.liteData.realPrincipals, + withdrawAsset, + true, // collateralToDebt = true (borrowing, not just withdrawing own supply) + ); + + console.log(`\nWithdrawing (borrowing) ${withdrawAmount} ${withdrawAsset.name}...`); + + await master.sendWithdraw(sender, FEES.SUPPLY_WITHDRAW, { + queryID: 0n, + includeUserCode: true, + amount: withdrawAmount, + asset: withdrawAsset, + userAddress: wallet.address, + amountToTransfer: 0n, + priceData: withdrawPrices.dataCell, + payload: Cell.EMPTY, + subaccountId: 0, + customPayloadSaturationFlag: false, + returnRepayRemainingsFlag: false, + }); + + console.log(`Withdraw ${withdrawAsset.name} sent!`); +} + +/** + * Withdraw own supply (no borrow created). + * If user has no debt, no prices needed. + */ +async function withdrawOwnSupply() { + const { wallet, sender } = await getWallet(); + const { master, prices, collector } = await getMasterAndPrices(); + + const user = await getUserData(master, wallet.address, prices); + + console.log('\n--- Before Withdraw ---'); + printUserHealth(user, master, prices); + + if (!user.liteData) { + throw new Error('User has no positions.'); + } + + const withdrawAsset = USDT_MAINNET; + const withdrawAmount = MAX_WITHDRAW_AMOUNT; // max withdraw + + // Get prices for withdraw + const withdrawPrices = await collector.getPricesForWithdraw( + user.liteData.realPrincipals, + withdrawAsset, + false, // not creating debt, just withdrawing own supply + ); + + console.log(`\nWithdrawing max ${withdrawAsset.name}...`); + + await master.sendWithdraw(sender, FEES.SUPPLY_WITHDRAW, { + queryID: 0n, + includeUserCode: true, + amount: withdrawAmount, + asset: withdrawAsset, + userAddress: wallet.address, + amountToTransfer: 0n, + priceData: withdrawPrices.dataCell, + payload: Cell.EMPTY, + subaccountId: 0, + customPayloadSaturationFlag: false, + returnRepayRemainingsFlag: false, + }); + + console.log(`Withdraw ${withdrawAsset.name} sent!`); +} + +// ==================== Check User State ==================== + +async function checkUserState() { + const { wallet } = await getWallet(); + const { master, prices } = await getMasterAndPrices(); + + console.log(`Wallet: ${wallet.address}`); + + const user = await getUserData(master, wallet.address, prices); + + console.log('\n--- User State ---'); + printUserHealth(user, master, prices); +} + +// ==================== Main ==================== + +const command = process.argv[2] || 'check'; + +switch (command) { + case 'supply-jetton': + supplyJetton(); + break; + case 'supply-ton': + supplyTON(); + break; + case 'withdraw-borrow': + withdrawJetton(); + break; + case 'withdraw-own': + withdrawOwnSupply(); + break; + case 'check': + checkUserState(); + break; + default: + console.log('Usage: npx ts-node tests/supply_withdraw_classic_he.ts '); + console.log('Commands:'); + console.log(' supply-jetton - Supply USDT into HE pool'); + console.log(' supply-ton - Supply TON into HE pool'); + console.log(' withdraw-borrow - Borrow tsTON against TON collateral (HE mode)'); + console.log(' withdraw-own - Withdraw own USDT supply'); + console.log(' check - Check current user positions and HE status'); +}