diff --git a/.gitignore b/.gitignore index 3076205..d28e8d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build dist temp *.log -.env \ No newline at end of file +.env +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ea0ec..0668aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). +## 0.5.0 - 2024-06-29 +This release contains breaking changes. + +### Added +- Reserve variables parsing on user & master sc +- Added endpoint argument for getPrices, default api.stardust-mainnet.iotaledger.net +- Added applyDust (default false) option in parseUserLiteData and parseUserData + +### Changed +- Master contracts' version +- Testnet master contract address +- Parsers on master sc +- Parsers on user sc +- Liquidation calculations now counts with reserve factor from master config + +### Fixed +- UserBalance calculation was fixed + ## 0.4.0 - 2024-06-01 This release contains breaking changes. diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..aafb1ee --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + rootDir: './tests', + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/tests/**/*.ts', '**/?(*.)+(spec|test).ts'], +}; \ No newline at end of file diff --git a/src/api/math.ts b/src/api/math.ts index 77f37d6..e3aa291 100644 --- a/src/api/math.ts +++ b/src/api/math.ts @@ -103,12 +103,10 @@ export function calculateAssetInterest(assetConfig: AssetConfig, assetData: Asse ); } - const reserveFactor = 10n; - const reserveScale = 100n; supplyInterest = mulDiv( mulDiv(borrowInterest, utilization, MASTER_CONSTANTS.FACTOR_SCALE), - reserveScale - reserveFactor, - reserveScale, + MASTER_CONSTANTS.ASSET_RESERVE_FACTOR_SCALE - assetConfig.reserveFactor, + MASTER_CONSTANTS.ASSET_RESERVE_FACTOR_SCALE, ); return { @@ -205,7 +203,7 @@ export function calculateLiquidationData( const values: bigint[] = []; const gCollateralAssetConfig = assetsConfig.get(gCollateralAsset)!; const gLoanAssetConfig = assetsConfig.get(gLoanAsset)!; - const liquidationBonus = gLoanAssetConfig.liquidationBonus; + const liquidationBonus = gCollateralAssetConfig.liquidationBonus; const loanDecimal = 10n ** gLoanAssetConfig.decimals; values.push( (bigIntMax(gCollateralValue / 2n, bigIntMin(gCollateralValue, 10_000_000_000n)) * diff --git a/src/api/parser.ts b/src/api/parser.ts index 8adbb8d..2130d59 100644 --- a/src/api/parser.ts +++ b/src/api/parser.ts @@ -11,7 +11,22 @@ import { presentValue, } from './math'; import { loadMaybeMyRef, loadMyRef } from './helpers'; -import { BalanceType, UserBalance, UserData, UserLiteData } from '../types/User'; +import { BalanceType, UserBalance, UserData, UserLiteData, UserRewards } from '../types/User'; + +// Will be in v6 +/* export function createUserRewards(): DictionaryValue { + return { + serialize: (src: any, buidler: any) => { + buidler.storeUint(src.trackingIndex, 64); + buidler.storeUint(src.trackingAccured, 64); + }, + parse: (src: Slice) => { + const trackingIndex = BigInt(src.loadUint(64)); + const trackingAccured = BigInt(src.loadUint(64)); + return { trackingIndex, trackingAccured }; + }, + }; +}*/ export function createAssetData(): DictionaryValue { return { @@ -22,6 +37,10 @@ export function createAssetData(): DictionaryValue { buidler.storeUint(src.totalBorrow, 64); buidler.storeUint(src.lastAccural, 32); buidler.storeUint(src.balance, 64); + /* Will be in v6 + buidler.storeUint(src.trackingSupplyIndex, 64); + buidler.storeUint(src.trackingBorrowIndex, 64); + buidler.storeUint(src.lastTrackingAccural, 32); */ }, parse: (src: Slice) => { const sRate = BigInt(src.loadInt(64)); @@ -30,8 +49,13 @@ export function createAssetData(): DictionaryValue { const totalBorrow = BigInt(src.loadInt(64)); const lastAccural = BigInt(src.loadInt(32)); const balance = BigInt(src.loadInt(64)); - return { sRate, bRate, totalSupply, totalBorrow, lastAccural, balance }; - }, + /* Will be in v6 + const trackingSupplyIndex = BigInt(src.loadUint(64)); + const trackingBorrowIndex = BigInt(src.loadUint(64)); + const lastTrackingAccural = BigInt(src.loadUint(32)); + + return { sRate, bRate, totalSupply, totalBorrow, lastAccural, balance, trackingSupplyIndex, trackingBorrowIndex, lastTrackingAccural}; }, */ + return { sRate, bRate, totalSupply, totalBorrow, lastAccural, balance}; }, }; } @@ -51,7 +75,14 @@ export function createAssetConfig(): DictionaryValue { refBuild.storeUint(src.supplyRateSlopeHigh, 64); refBuild.storeUint(src.targetUtilization, 64); refBuild.storeUint(src.originationFee, 64); + refBuild.storeUint(src.dust, 64); refBuild.storeUint(src.maxTotalSupply, 64); + refBuild.storeUint(src.reserveFactor, 16); + refBuild.storeUint(src.liquidationReserveFactor, 16); + /* Will be in v6 + refBuild.storeUint(src.minPrincipalForRewards, 64); + refBuild.storeUint(src.baseTrackingSupplySpeed, 64); + refBuild.storeUint(src.baseTrackingBorrowSpeed, 64); */ builder.storeRef(refBuild.endCell()); }, parse: (src: Slice) => { @@ -70,6 +101,12 @@ export function createAssetConfig(): DictionaryValue { const originationFee = ref.loadUintBig(64); const dust = ref.loadUintBig(64); const maxTotalSupply = ref.loadUintBig(64); + const reserveFactor = ref.loadUintBig(16); + const liquidationReserveFactor = ref.loadUintBig(16); + /* Will be in v6 + const minPrincipalForRewards = ref.loadUintBig(64); + const baseTrackingSupplySpeed = ref.loadUintBig(64); + const baseTrackingBorrowSpeed = ref.loadUintBig(64); */ return { oracle, @@ -86,6 +123,12 @@ export function createAssetConfig(): DictionaryValue { originationFee, dust, maxTotalSupply, + reserveFactor, + liquidationReserveFactor, + /* Will be in v6 + minPrincipalForRewards, + baseTrackingSupplySpeed, + baseTrackingBorrowSpeed */ }; }, }; @@ -162,6 +205,7 @@ export function parseUserLiteData( assetsData: Dictionary, assetsConfig: Dictionary, testnet: boolean = false, + applyDust: boolean = false ): UserLiteData { const ASSETS_ID = testnet ? TESTNET_ASSETS_ID : MAINNET_ASSETS_ID; const userSlice = Cell.fromBase64(userDataBOC).beginParse(); @@ -175,6 +219,26 @@ export function parseUserLiteData( const trackingBorrowIndex = userSlice.loadUintBig(64); const dutchAuctionStart = userSlice.loadUint(32); const backupCell = loadMyRef(userSlice); + /* Will be in v6 + let trackingSupplyIndex = 0n; + let trackingBorrowIndex = 0n; + let dutchAuctionStart = 0; + let backupCell = Cell.EMPTY; + let rewards = Dictionary.empty(Dictionary.Keys.BigUint(256), createUserRewards()); + let backupCell1: Cell | null = null; + let backupCell2: Cell | null = null; + const bitsLeft = userSlice.remainingBits; + if (bitsLeft > 32) { + trackingSupplyIndex = userSlice.loadUintBig(64); + trackingBorrowIndex = userSlice.loadUintBig(64); + dutchAuctionStart = userSlice.loadUint(32); + backupCell = loadMyRef(userSlice); + } else { + rewards = userSlice.loadDict(Dictionary.Keys.BigUint(256), createUserRewards()); + backupCell1 = userSlice.loadMaybeRef(); + backupCell2 = userSlice.loadMaybeRef(); + } + */ userSlice.endParse(); const userBalances = Dictionary.empty(); @@ -182,7 +246,13 @@ export function parseUserLiteData( for (const [_, assetID] of Object.entries(ASSETS_ID)) { const assetData = assetsData.get(assetID) as ExtendedAssetData; const assetConfig = assetsConfig.get(assetID) as AssetConfig; - const balance = presentValue(assetData.sRate, assetData.bRate, principalsDict.get(assetID) || 0n); + let principals = principalsDict.get(assetID) || 0n; + + if (applyDust && (principals > -assetConfig.dust && principals < assetConfig.dust)) { // abs(principals) < dust + principals = 0n; + principalsDict.set(assetID, 0n); + } + const balance = presentValue(assetData.sRate, assetData.bRate, principals); userBalances.set(assetID, balance); } @@ -198,6 +268,10 @@ export function parseUserLiteData( trackingBorrowIndex: trackingBorrowIndex, dutchAuctionStart: dutchAuctionStart, backupCell: backupCell, + /* Will be in v6 + rewards: rewards, + backupCell1: backupCell1, + backupCell2: backupCell2, */ }; } @@ -207,6 +281,7 @@ export function parseUserData( assetsConfig: Dictionary, prices: Dictionary, testnet: boolean = false, + applyDust: boolean = false ): UserData { const ASSETS_ID = testnet ? TESTNET_ASSETS_ID : MAINNET_ASSETS_ID; const withdrawalLimits = Dictionary.empty(); @@ -217,7 +292,14 @@ export function parseUserData( for (const [_, assetID] of Object.entries(ASSETS_ID)) { const assetData = assetsData.get(assetID) as ExtendedAssetData; const assetConfig = assetsConfig.get(assetID) as AssetConfig; - const balance = presentValue(assetData.sRate, assetData.bRate, userLiteData.principals.get(assetID) || 0n); + let principals = userLiteData.principals.get(assetID) || 0n; + + if (applyDust && (principals > -assetConfig.dust && principals < assetConfig.dust )) { // abs(principals) < dust + principals = 0n; + userLiteData.principals.set(assetID, 0n); + } + + const balance = presentValue(assetData.sRate, assetData.bRate, principals); userLiteData.balances.set(assetID, balance); } @@ -255,12 +337,11 @@ export function parseUserData( 0n, ), ); - } else { - borrowLimits.set( - assetID, - bigIntMin((availableToBorrow * 10n ** assetConfig.decimals) / prices.get(assetID)!, assetData.balance), - ); } + borrowLimits.set( + assetID, + bigIntMin((availableToBorrow * 10n ** assetConfig.decimals) / prices.get(assetID)!, assetData.balance), + ); } const limitUsed = borrowBalance + availableToBorrow; diff --git a/src/api/prices.ts b/src/api/prices.ts index 8f8e145..02df31d 100644 --- a/src/api/prices.ts +++ b/src/api/prices.ts @@ -39,14 +39,14 @@ type OutputData = { }; }; -export async function getPrices(): Promise { +export async function getPrices(endpoint: String = "api.stardust-mainnet.iotaledger.net"): Promise { try { - let result = await fetch('https://api.stardust-mainnet.iotaledger.net/api/indexer/v1/outputs/nft/' + NFT_ID, { + let result = await fetch(`https://${endpoint}/api/indexer/v1/outputs/nft/${NFT_ID}`, { headers: { accept: 'application/json' }, }); let outputId = (await result.json()) as NftData; - result = await fetch('https://api.stardust-mainnet.iotaledger.net/api/core/v2/outputs/' + outputId.items[0], { + result = await fetch(`https://${endpoint}/api/core/v2/outputs/${outputId.items[0]}`, { headers: { accept: 'application/json' }, }); diff --git a/src/constants.ts b/src/constants.ts index da5a4a9..63bff05 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,9 +2,9 @@ import { Address, Cell, toNano } from '@ton/core'; import { sha256Hash } from './utils/sha256BigInt'; export const EVAA_MASTER_MAINNET = Address.parse('EQC8rUZqR_pWV1BylWUlPNBzyiTYVoBEmQkMIQDZXICfnuRr'); -export const MAINNET_VERSION = 4; +export const MAINNET_VERSION = 5; export const EVAA_MASTER_TESTNET = Address.parse('kQCj7qf3i2Cbf1GVtZCinla7lIvE7l3MBMsJMTfAja3BdoRP'); -export const TESTNET_VERSION = 4; +export const TESTNET_VERSION = 5; export const NFT_ID = '0xfb9874544d76ca49c5db9cc3e5121e4c018bc8a2fb2bfe8f2a38c5b9963492f5'; @@ -43,6 +43,8 @@ export const MASTER_CONSTANTS = { FACTOR_SCALE: BigInt(1e12), ASSET_COEFFICIENT_SCALE: 10000n, ASSET_PRICE_SCALE: BigInt(1e8), + ASSET_RESERVE_FACTOR_SCALE: 10000n, + ASSET_LIQUIDATION_RESERVE_FACTOR_SCALE: 10000n, }; export const LENDING_CODE = Cell.fromBoc( diff --git a/src/contracts/MasterContract.ts b/src/contracts/MasterContract.ts index 2cf847d..d57534d 100644 --- a/src/contracts/MasterContract.ts +++ b/src/contracts/MasterContract.ts @@ -59,6 +59,9 @@ export type SupplyBaseParameters = { amount: bigint; userAddress: Address; assetID: bigint; + /* Will be in v6 + amountToTransfer: bigint; + payload: Cell; */ }; /** * Parameters for the TON supply message @@ -92,6 +95,9 @@ export type WithdrawParameters = { userAddress: Address; includeUserCode: boolean; priceData: Cell; + /* Will be in v6 + amountToTransfer: bigint; + payload: Cell; */ }; /** @@ -184,6 +190,9 @@ export class Evaa implements Contract { .storeUint(OPCODES.SUPPLY, 32) .storeInt(parameters.includeUserCode ? -1 : 0, 2) .storeAddress(parameters.userAddress) + /* Will be in v6 + .storeUint(parameters.amountToTransfer, 64) + .storeRef(parameters.payload) */ .endCell(), ) .endCell(); @@ -194,6 +203,9 @@ export class Evaa implements Contract { .storeInt(parameters.includeUserCode ? -1 : 0, 2) .storeUint(parameters.amount, 64) .storeAddress(parameters.userAddress) + /* Will be in v6 + .storeUint(parameters.amountToTransfer, 64) + .storeRef(parameters.payload) */ .endCell(); } } @@ -210,6 +222,9 @@ export class Evaa implements Contract { .storeUint(parameters.amount, 64) .storeAddress(parameters.userAddress) .storeInt(parameters.includeUserCode ? -1 : 0, 2) + /* Will be in v6 + .storeUint(parameters.amountToTransfer, 64) + .storeRef(parameters.payload) */ .storeRef(parameters.priceData) .endCell(); } diff --git a/src/types/Master.ts b/src/types/Master.ts index f543b64..4ba9178 100644 --- a/src/types/Master.ts +++ b/src/types/Master.ts @@ -27,6 +27,12 @@ export type AssetConfig = { originationFee: bigint; dust: bigint; maxTotalSupply: bigint; + reserveFactor: bigint; + liquidationReserveFactor: bigint; + /* Will be in v6 + minPrincipalForRewards: bigint; + baseTrackingSupplySpeed: bigint; + baseTrackingBorrowSpeed: bigint; */ }; export type MasterConfig = { @@ -44,6 +50,10 @@ export type AssetData = { totalBorrow: bigint; lastAccural: bigint; balance: bigint; + /* Will be in v6 + trackingSupplyIndex: bigint; + trackingBorrowIndex: bigint; + lastTrackingAccural: bigint; */ }; export type AssetInterest = { diff --git a/src/types/User.ts b/src/types/User.ts index f86e719..edf7a77 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -43,6 +43,10 @@ export type UserLiteData = { trackingBorrowIndex: bigint; dutchAuctionStart: number; backupCell: Cell; + /* Will be in v6 + rewards: Dictionary; + backupCell1: Cell | null; + backupCell2: Cell | null; */ }; export type UserDataActive = UserLiteData & { @@ -64,3 +68,8 @@ export type UserDataInactive = { }; export type UserData = UserDataActive | UserDataInactive; + +export type UserRewards = { + trackingIndex: bigint; + trackingAccured: bigint; +}; diff --git a/tests/asset_config_test.ts b/tests/asset_config_test.ts new file mode 100644 index 0000000..3dc3677 --- /dev/null +++ b/tests/asset_config_test.ts @@ -0,0 +1,34 @@ +import {createAssetConfig, EVAA_MASTER_MAINNET} from '../src'; +import {beginCell, Dictionary, TonClient} from '@ton/ton'; +import dotenv from 'dotenv'; + +let client: TonClient; + + +beforeAll(async () => { + dotenv.config(); + client = new TonClient({ + endpoint: 'https://toncenter.com/api/v2/jsonRPC', + apiKey: process.env.RPC_API_KEY, + }); +}); + +test('createAssetConfig test', async () => { + expect.assertions(2); + + const assetConfigBuilder = createAssetConfig(); + const res = await client.runMethod(EVAA_MASTER_MAINNET, 'getAssetsConfig'); + const cellParsing = res.stack.readCell().beginParse(); + + let dictBuilder = beginCell(); + + const dict = cellParsing.loadDictDirect(Dictionary.Keys.BigUint(256), createAssetConfig()); + dict.storeDirect(dictBuilder, Dictionary.Keys.BigUint(256), createAssetConfig()); + + //console.log(dict); + const dictAgain = Dictionary.loadDirect(Dictionary.Keys.BigUint(256), createAssetConfig(), dictBuilder.asSlice()); + //console.log(dictAgain); + + await expect(cellParsing.remainingBits).toStrictEqual(0); + await expect(JSON.stringify(dict)).toStrictEqual(JSON.stringify(dictAgain)); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d520feb..41eb1dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,7 +55,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "build/dist", /* Specify an output folder for all emitted files. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -107,5 +107,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "jest.config.js"] }