@aspan/sdk 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.mts +33 -10
- package/dist/index.d.ts +33 -10
- package/dist/index.js +114 -11
- package/dist/index.mjs +114 -11
- package/package.json +7 -7
- package/src/__tests__/risk.test.ts +2 -2
- package/src/__tests__/router.test.ts +24 -14
- package/src/abi/sApUSD.ts +14 -0
- package/src/bot/services/risk-keeper.ts +17 -3
- package/src/client.ts +60 -0
- package/src/index.ts +12 -9
- package/src/router.ts +53 -3
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -22,11 +22,11 @@ import { BSC_ADDRESSES } from "@aspan/sdk";
|
|
|
22
22
|
| Contract | Address |
|
|
23
23
|
|----------|---------|
|
|
24
24
|
| **Diamond (Main Entry)** | `0x6a11B30d3a70727d5477D6d8090e144443fA1c78` |
|
|
25
|
-
| **Router** | `
|
|
25
|
+
| **Router** | `0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E` |
|
|
26
26
|
| **ApUSD** | `0x4570047eeB5aDb4081c5d470494EB5134e34A287` |
|
|
27
27
|
| **XBNB** | `0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43` |
|
|
28
28
|
| **SApUSD** | `0xE2BE739C4aA4126ee72D612d9548C38B1B0e5A1b` |
|
|
29
|
-
| **wclisBNB** | `
|
|
29
|
+
| **wclisBNB** | `0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07` |
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
package/dist/index.d.mts
CHANGED
|
@@ -203,7 +203,7 @@ interface RouterSwapParams {
|
|
|
203
203
|
targetLST: Address;
|
|
204
204
|
/** Minimum LST to receive from swap (slippage protection) */
|
|
205
205
|
minLSTOut: bigint;
|
|
206
|
-
/** PancakeSwap V3 pool fee tier (500, 2500, 10000) */
|
|
206
|
+
/** PancakeSwap V3 pool fee tier (100, 500, 2500, 10000) */
|
|
207
207
|
poolFee: number;
|
|
208
208
|
}
|
|
209
209
|
/** Router mint parameters */
|
|
@@ -461,6 +461,21 @@ declare class AspanReadClient {
|
|
|
461
461
|
xBNBBalance: bigint;
|
|
462
462
|
}>;
|
|
463
463
|
getUserStabilityPoolPosition(user: Address): Promise<UserStabilityPoolPosition>;
|
|
464
|
+
/**
|
|
465
|
+
* Get realtime exchange rate using live xBNB price (not cached xBNBToApUSDRate).
|
|
466
|
+
* Use this for accurate PnL display — the on-chain exchangeRate() only updates
|
|
467
|
+
* on deposit/withdraw and doesn't reflect xBNB price changes between operations.
|
|
468
|
+
*
|
|
469
|
+
* @returns Exchange rate in 18 decimals (e.g. 1.15e18 means 1 sApUSD = 1.15 apUSD equivalent)
|
|
470
|
+
*/
|
|
471
|
+
getRealtimeExchangeRate(): Promise<{
|
|
472
|
+
exchangeRate: bigint;
|
|
473
|
+
accountedApUSD: bigint;
|
|
474
|
+
accountedXBNB: bigint;
|
|
475
|
+
xBNBPriceUSD: bigint;
|
|
476
|
+
totalValue: bigint;
|
|
477
|
+
totalSupply: bigint;
|
|
478
|
+
}>;
|
|
464
479
|
getExchangeRate(): Promise<bigint>;
|
|
465
480
|
getTotalStaked(): Promise<bigint>;
|
|
466
481
|
previewDeposit(assets: bigint): Promise<bigint>;
|
|
@@ -770,6 +785,11 @@ declare class AspanRouterReadClient {
|
|
|
770
785
|
getWBNB(): Promise<Address>;
|
|
771
786
|
getUSDT(): Promise<Address>;
|
|
772
787
|
getUSDC(): Promise<Address>;
|
|
788
|
+
/**
|
|
789
|
+
* Get stablecoin price in USD (18 decimals) from Chainlink.
|
|
790
|
+
* Returns 1e18 if feed unavailable (graceful fallback).
|
|
791
|
+
*/
|
|
792
|
+
getStablecoinPrice(token: Address): Promise<bigint>;
|
|
773
793
|
getSlisBNB(): Promise<Address>;
|
|
774
794
|
getAsBNB(): Promise<Address>;
|
|
775
795
|
getWclisBNB(): Promise<Address>;
|
|
@@ -2809,25 +2829,28 @@ declare const BPS_PRECISION = 10000n;
|
|
|
2809
2829
|
declare const PRICE_PRECISION: bigint;
|
|
2810
2830
|
declare const BSC_ADDRESSES: {
|
|
2811
2831
|
readonly diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78";
|
|
2812
|
-
readonly router: "
|
|
2832
|
+
readonly router: "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E";
|
|
2813
2833
|
readonly apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287";
|
|
2814
2834
|
readonly xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43";
|
|
2815
2835
|
readonly sApUSD: "0x896770Dba7c0481539E25aaB56bE285ECF6D65eB";
|
|
2816
2836
|
readonly slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B";
|
|
2817
2837
|
readonly asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6";
|
|
2818
|
-
readonly wclisBNB: "
|
|
2838
|
+
readonly wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07";
|
|
2819
2839
|
readonly USDT: "0x55d398326f99059fF775485246999027B3197955";
|
|
2820
2840
|
readonly USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
2841
|
+
/** Chainlink price feeds (BSC Mainnet) */
|
|
2842
|
+
readonly USDT_USD_FEED: "0xB97Ad0E74fa7d920791E90258A6E2085088b4320";
|
|
2843
|
+
readonly USDC_USD_FEED: "0x51597f405303C4377E36123cBc172b13269EA163";
|
|
2821
2844
|
readonly WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c";
|
|
2822
2845
|
};
|
|
2823
2846
|
declare const PHAROS_ADDRESSES: {
|
|
2824
|
-
readonly diamond: "
|
|
2825
|
-
readonly apUSD: "
|
|
2826
|
-
readonly xBNB: "
|
|
2827
|
-
readonly sApUSD: "
|
|
2828
|
-
readonly mockLST: "
|
|
2829
|
-
readonly mockPriceFeed: "
|
|
2830
|
-
readonly mockExchangeRate: "
|
|
2847
|
+
readonly diamond: "0x3d5ceb29BA3b4B837eddCAf53442371828933e5D";
|
|
2848
|
+
readonly apUSD: "0x054f8DB2F700883949d657Da9418c6F6B734BA18";
|
|
2849
|
+
readonly xBNB: "0x1da6F06F7195464732FB9ECa98DE8BC1c520C3a1";
|
|
2850
|
+
readonly sApUSD: "0xf94f0b76942f83Bb4BC010a942c297D18842313b";
|
|
2851
|
+
readonly mockLST: "0xDD890267E72320522E8C61aD357B650ebe4C47c5";
|
|
2852
|
+
readonly mockPriceFeed: "0x4180062949036fCe833027f0ABf7052e7485ddfC";
|
|
2853
|
+
readonly mockExchangeRate: "0xb9aDC2552cF3Fb223236CfEa1c78D277fA8fb8B6";
|
|
2831
2854
|
};
|
|
2832
2855
|
/**
|
|
2833
2856
|
* Format a bigint amount to human-readable string with decimals
|
package/dist/index.d.ts
CHANGED
|
@@ -203,7 +203,7 @@ interface RouterSwapParams {
|
|
|
203
203
|
targetLST: Address;
|
|
204
204
|
/** Minimum LST to receive from swap (slippage protection) */
|
|
205
205
|
minLSTOut: bigint;
|
|
206
|
-
/** PancakeSwap V3 pool fee tier (500, 2500, 10000) */
|
|
206
|
+
/** PancakeSwap V3 pool fee tier (100, 500, 2500, 10000) */
|
|
207
207
|
poolFee: number;
|
|
208
208
|
}
|
|
209
209
|
/** Router mint parameters */
|
|
@@ -461,6 +461,21 @@ declare class AspanReadClient {
|
|
|
461
461
|
xBNBBalance: bigint;
|
|
462
462
|
}>;
|
|
463
463
|
getUserStabilityPoolPosition(user: Address): Promise<UserStabilityPoolPosition>;
|
|
464
|
+
/**
|
|
465
|
+
* Get realtime exchange rate using live xBNB price (not cached xBNBToApUSDRate).
|
|
466
|
+
* Use this for accurate PnL display — the on-chain exchangeRate() only updates
|
|
467
|
+
* on deposit/withdraw and doesn't reflect xBNB price changes between operations.
|
|
468
|
+
*
|
|
469
|
+
* @returns Exchange rate in 18 decimals (e.g. 1.15e18 means 1 sApUSD = 1.15 apUSD equivalent)
|
|
470
|
+
*/
|
|
471
|
+
getRealtimeExchangeRate(): Promise<{
|
|
472
|
+
exchangeRate: bigint;
|
|
473
|
+
accountedApUSD: bigint;
|
|
474
|
+
accountedXBNB: bigint;
|
|
475
|
+
xBNBPriceUSD: bigint;
|
|
476
|
+
totalValue: bigint;
|
|
477
|
+
totalSupply: bigint;
|
|
478
|
+
}>;
|
|
464
479
|
getExchangeRate(): Promise<bigint>;
|
|
465
480
|
getTotalStaked(): Promise<bigint>;
|
|
466
481
|
previewDeposit(assets: bigint): Promise<bigint>;
|
|
@@ -770,6 +785,11 @@ declare class AspanRouterReadClient {
|
|
|
770
785
|
getWBNB(): Promise<Address>;
|
|
771
786
|
getUSDT(): Promise<Address>;
|
|
772
787
|
getUSDC(): Promise<Address>;
|
|
788
|
+
/**
|
|
789
|
+
* Get stablecoin price in USD (18 decimals) from Chainlink.
|
|
790
|
+
* Returns 1e18 if feed unavailable (graceful fallback).
|
|
791
|
+
*/
|
|
792
|
+
getStablecoinPrice(token: Address): Promise<bigint>;
|
|
773
793
|
getSlisBNB(): Promise<Address>;
|
|
774
794
|
getAsBNB(): Promise<Address>;
|
|
775
795
|
getWclisBNB(): Promise<Address>;
|
|
@@ -2809,25 +2829,28 @@ declare const BPS_PRECISION = 10000n;
|
|
|
2809
2829
|
declare const PRICE_PRECISION: bigint;
|
|
2810
2830
|
declare const BSC_ADDRESSES: {
|
|
2811
2831
|
readonly diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78";
|
|
2812
|
-
readonly router: "
|
|
2832
|
+
readonly router: "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E";
|
|
2813
2833
|
readonly apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287";
|
|
2814
2834
|
readonly xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43";
|
|
2815
2835
|
readonly sApUSD: "0x896770Dba7c0481539E25aaB56bE285ECF6D65eB";
|
|
2816
2836
|
readonly slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B";
|
|
2817
2837
|
readonly asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6";
|
|
2818
|
-
readonly wclisBNB: "
|
|
2838
|
+
readonly wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07";
|
|
2819
2839
|
readonly USDT: "0x55d398326f99059fF775485246999027B3197955";
|
|
2820
2840
|
readonly USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
2841
|
+
/** Chainlink price feeds (BSC Mainnet) */
|
|
2842
|
+
readonly USDT_USD_FEED: "0xB97Ad0E74fa7d920791E90258A6E2085088b4320";
|
|
2843
|
+
readonly USDC_USD_FEED: "0x51597f405303C4377E36123cBc172b13269EA163";
|
|
2821
2844
|
readonly WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c";
|
|
2822
2845
|
};
|
|
2823
2846
|
declare const PHAROS_ADDRESSES: {
|
|
2824
|
-
readonly diamond: "
|
|
2825
|
-
readonly apUSD: "
|
|
2826
|
-
readonly xBNB: "
|
|
2827
|
-
readonly sApUSD: "
|
|
2828
|
-
readonly mockLST: "
|
|
2829
|
-
readonly mockPriceFeed: "
|
|
2830
|
-
readonly mockExchangeRate: "
|
|
2847
|
+
readonly diamond: "0x3d5ceb29BA3b4B837eddCAf53442371828933e5D";
|
|
2848
|
+
readonly apUSD: "0x054f8DB2F700883949d657Da9418c6F6B734BA18";
|
|
2849
|
+
readonly xBNB: "0x1da6F06F7195464732FB9ECa98DE8BC1c520C3a1";
|
|
2850
|
+
readonly sApUSD: "0xf94f0b76942f83Bb4BC010a942c297D18842313b";
|
|
2851
|
+
readonly mockLST: "0xDD890267E72320522E8C61aD357B650ebe4C47c5";
|
|
2852
|
+
readonly mockPriceFeed: "0x4180062949036fCe833027f0ABf7052e7485ddfC";
|
|
2853
|
+
readonly mockExchangeRate: "0xb9aDC2552cF3Fb223236CfEa1c78D277fA8fb8B6";
|
|
2831
2854
|
};
|
|
2832
2855
|
/**
|
|
2833
2856
|
* Format a bigint amount to human-readable string with decimals
|
package/dist/index.js
CHANGED
|
@@ -773,6 +773,20 @@ var SApUSDABI = [
|
|
|
773
773
|
],
|
|
774
774
|
stateMutability: "view"
|
|
775
775
|
},
|
|
776
|
+
{
|
|
777
|
+
type: "function",
|
|
778
|
+
name: "accountedApUSD",
|
|
779
|
+
inputs: [],
|
|
780
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
781
|
+
stateMutability: "view"
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
type: "function",
|
|
785
|
+
name: "accountedXBNB",
|
|
786
|
+
inputs: [],
|
|
787
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
788
|
+
stateMutability: "view"
|
|
789
|
+
},
|
|
776
790
|
{
|
|
777
791
|
type: "function",
|
|
778
792
|
name: "xBNBToApUSDRate",
|
|
@@ -1269,6 +1283,48 @@ var AspanReadClient = class _AspanReadClient {
|
|
|
1269
1283
|
]);
|
|
1270
1284
|
return { shares, balance };
|
|
1271
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Get realtime exchange rate using live xBNB price (not cached xBNBToApUSDRate).
|
|
1288
|
+
* Use this for accurate PnL display — the on-chain exchangeRate() only updates
|
|
1289
|
+
* on deposit/withdraw and doesn't reflect xBNB price changes between operations.
|
|
1290
|
+
*
|
|
1291
|
+
* @returns Exchange rate in 18 decimals (e.g. 1.15e18 means 1 sApUSD = 1.15 apUSD equivalent)
|
|
1292
|
+
*/
|
|
1293
|
+
async getRealtimeExchangeRate() {
|
|
1294
|
+
const sApUSDAddress = await this.getSApUSD();
|
|
1295
|
+
const PRECISION2 = 10n ** 18n;
|
|
1296
|
+
const VIRTUAL_ASSETS = 10n ** 6n;
|
|
1297
|
+
const VIRTUAL_SHARES = 10n ** 6n;
|
|
1298
|
+
const [accountedApUSD, accountedXBNB, totalSupply, xBNBPriceUSD] = await Promise.all([
|
|
1299
|
+
this.publicClient.readContract({
|
|
1300
|
+
address: sApUSDAddress,
|
|
1301
|
+
abi: SApUSDABI,
|
|
1302
|
+
functionName: "accountedApUSD"
|
|
1303
|
+
}),
|
|
1304
|
+
this.publicClient.readContract({
|
|
1305
|
+
address: sApUSDAddress,
|
|
1306
|
+
abi: SApUSDABI,
|
|
1307
|
+
functionName: "accountedXBNB"
|
|
1308
|
+
}),
|
|
1309
|
+
this.publicClient.readContract({
|
|
1310
|
+
address: sApUSDAddress,
|
|
1311
|
+
abi: SApUSDABI,
|
|
1312
|
+
functionName: "totalSupply"
|
|
1313
|
+
}),
|
|
1314
|
+
this.getXBNBPriceUSD()
|
|
1315
|
+
]);
|
|
1316
|
+
const totalValue = accountedXBNB === 0n || xBNBPriceUSD === 0n ? accountedApUSD : accountedApUSD + accountedXBNB * xBNBPriceUSD / PRECISION2;
|
|
1317
|
+
const supply = totalSupply + VIRTUAL_SHARES;
|
|
1318
|
+
const exchangeRate = supply === 0n ? PRECISION2 : (totalValue + VIRTUAL_ASSETS) * PRECISION2 / supply;
|
|
1319
|
+
return {
|
|
1320
|
+
exchangeRate,
|
|
1321
|
+
accountedApUSD,
|
|
1322
|
+
accountedXBNB,
|
|
1323
|
+
xBNBPriceUSD,
|
|
1324
|
+
totalValue,
|
|
1325
|
+
totalSupply
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1272
1328
|
async getExchangeRate() {
|
|
1273
1329
|
try {
|
|
1274
1330
|
return await this.publicClient.readContract({
|
|
@@ -2384,6 +2440,21 @@ var RouterABI = [
|
|
|
2384
2440
|
];
|
|
2385
2441
|
|
|
2386
2442
|
// src/router.ts
|
|
2443
|
+
var ChainlinkABI = [
|
|
2444
|
+
{
|
|
2445
|
+
type: "function",
|
|
2446
|
+
name: "latestRoundData",
|
|
2447
|
+
inputs: [],
|
|
2448
|
+
outputs: [
|
|
2449
|
+
{ name: "roundId", type: "uint80" },
|
|
2450
|
+
{ name: "answer", type: "int256" },
|
|
2451
|
+
{ name: "startedAt", type: "uint256" },
|
|
2452
|
+
{ name: "updatedAt", type: "uint256" },
|
|
2453
|
+
{ name: "answeredInRound", type: "uint80" }
|
|
2454
|
+
],
|
|
2455
|
+
stateMutability: "view"
|
|
2456
|
+
}
|
|
2457
|
+
];
|
|
2387
2458
|
var AspanRouterReadClient = class {
|
|
2388
2459
|
publicClient;
|
|
2389
2460
|
routerAddress;
|
|
@@ -2539,7 +2610,9 @@ var AspanRouterReadClient = class {
|
|
|
2539
2610
|
const usdValue = inputAmount * bnbPrice18 / one;
|
|
2540
2611
|
lstAmount = lstPrice18 === 0n ? 0n : usdValue * one / lstPrice18;
|
|
2541
2612
|
} else if (inNorm === usdt.toLowerCase() || inNorm === usdc.toLowerCase()) {
|
|
2542
|
-
|
|
2613
|
+
const stablePrice18 = await this.getStablecoinPrice(inputToken);
|
|
2614
|
+
const usdValue = inputAmount * stablePrice18 / one;
|
|
2615
|
+
lstAmount = lstPrice18 === 0n ? 0n : usdValue * one / lstPrice18;
|
|
2543
2616
|
} else {
|
|
2544
2617
|
throw new Error("Unsupported input token for SDK preview");
|
|
2545
2618
|
}
|
|
@@ -2585,7 +2658,8 @@ var AspanRouterReadClient = class {
|
|
|
2585
2658
|
if (outNorm === import_viem2.zeroAddress.toLowerCase() || outNorm === wbnb.toLowerCase()) {
|
|
2586
2659
|
outputAmount = bnbPrice18 === 0n ? 0n : usdValue * one / bnbPrice18;
|
|
2587
2660
|
} else if (outNorm === usdt.toLowerCase() || outNorm === usdc.toLowerCase()) {
|
|
2588
|
-
|
|
2661
|
+
const stablePrice18 = await this.getStablecoinPrice(outputToken);
|
|
2662
|
+
outputAmount = stablePrice18 === 0n ? 0n : usdValue * one / stablePrice18;
|
|
2589
2663
|
} else {
|
|
2590
2664
|
throw new Error("Unsupported output token for SDK preview");
|
|
2591
2665
|
}
|
|
@@ -2650,6 +2724,32 @@ var AspanRouterReadClient = class {
|
|
|
2650
2724
|
})
|
|
2651
2725
|
);
|
|
2652
2726
|
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Get stablecoin price in USD (18 decimals) from Chainlink.
|
|
2729
|
+
* Returns 1e18 if feed unavailable (graceful fallback).
|
|
2730
|
+
*/
|
|
2731
|
+
async getStablecoinPrice(token) {
|
|
2732
|
+
const usdt = await this.getUSDT();
|
|
2733
|
+
const usdc = await this.getUSDC();
|
|
2734
|
+
const norm = token.toLowerCase();
|
|
2735
|
+
let feed = null;
|
|
2736
|
+
if (norm === usdt.toLowerCase()) {
|
|
2737
|
+
feed = BSC_ADDRESSES.USDT_USD_FEED;
|
|
2738
|
+
} else if (norm === usdc.toLowerCase()) {
|
|
2739
|
+
feed = BSC_ADDRESSES.USDC_USD_FEED;
|
|
2740
|
+
}
|
|
2741
|
+
if (!feed) return 10n ** 18n;
|
|
2742
|
+
try {
|
|
2743
|
+
const [, answer] = await this.publicClient.readContract({
|
|
2744
|
+
address: feed,
|
|
2745
|
+
abi: ChainlinkABI,
|
|
2746
|
+
functionName: "latestRoundData"
|
|
2747
|
+
});
|
|
2748
|
+
return BigInt(answer) * 10n ** 10n;
|
|
2749
|
+
} catch {
|
|
2750
|
+
return 10n ** 18n;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2653
2753
|
async getSlisBNB() {
|
|
2654
2754
|
return this._getCachedAddress(
|
|
2655
2755
|
"slisBNB",
|
|
@@ -2954,27 +3054,30 @@ var BPS_PRECISION = 10000n;
|
|
|
2954
3054
|
var PRICE_PRECISION = 10n ** 8n;
|
|
2955
3055
|
var BSC_ADDRESSES = {
|
|
2956
3056
|
diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78",
|
|
2957
|
-
router: "
|
|
3057
|
+
router: "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E",
|
|
2958
3058
|
apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287",
|
|
2959
3059
|
xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43",
|
|
2960
3060
|
sApUSD: "0x896770Dba7c0481539E25aaB56bE285ECF6D65eB",
|
|
2961
3061
|
// LSTs
|
|
2962
3062
|
slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B",
|
|
2963
3063
|
asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6",
|
|
2964
|
-
wclisBNB: "
|
|
3064
|
+
wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07",
|
|
2965
3065
|
// Stablecoins
|
|
2966
3066
|
USDT: "0x55d398326f99059fF775485246999027B3197955",
|
|
2967
3067
|
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
3068
|
+
/** Chainlink price feeds (BSC Mainnet) */
|
|
3069
|
+
USDT_USD_FEED: "0xB97Ad0E74fa7d920791E90258A6E2085088b4320",
|
|
3070
|
+
USDC_USD_FEED: "0x51597f405303C4377E36123cBc172b13269EA163",
|
|
2968
3071
|
WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
|
|
2969
3072
|
};
|
|
2970
3073
|
var PHAROS_ADDRESSES = {
|
|
2971
|
-
diamond: "
|
|
2972
|
-
apUSD: "
|
|
2973
|
-
xBNB: "
|
|
2974
|
-
sApUSD: "
|
|
2975
|
-
mockLST: "
|
|
2976
|
-
mockPriceFeed: "
|
|
2977
|
-
mockExchangeRate: "
|
|
3074
|
+
diamond: "0x3d5ceb29BA3b4B837eddCAf53442371828933e5D",
|
|
3075
|
+
apUSD: "0x054f8DB2F700883949d657Da9418c6F6B734BA18",
|
|
3076
|
+
xBNB: "0x1da6F06F7195464732FB9ECa98DE8BC1c520C3a1",
|
|
3077
|
+
sApUSD: "0xf94f0b76942f83Bb4BC010a942c297D18842313b",
|
|
3078
|
+
mockLST: "0xDD890267E72320522E8C61aD357B650ebe4C47c5",
|
|
3079
|
+
mockPriceFeed: "0x4180062949036fCe833027f0ABf7052e7485ddfC",
|
|
3080
|
+
mockExchangeRate: "0xb9aDC2552cF3Fb223236CfEa1c78D277fA8fb8B6"
|
|
2978
3081
|
};
|
|
2979
3082
|
function formatAmount(amount, decimals = 4) {
|
|
2980
3083
|
const divisor = 10n ** BigInt(18 - decimals);
|
package/dist/index.mjs
CHANGED
|
@@ -715,6 +715,20 @@ var SApUSDABI = [
|
|
|
715
715
|
],
|
|
716
716
|
stateMutability: "view"
|
|
717
717
|
},
|
|
718
|
+
{
|
|
719
|
+
type: "function",
|
|
720
|
+
name: "accountedApUSD",
|
|
721
|
+
inputs: [],
|
|
722
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
723
|
+
stateMutability: "view"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
type: "function",
|
|
727
|
+
name: "accountedXBNB",
|
|
728
|
+
inputs: [],
|
|
729
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
730
|
+
stateMutability: "view"
|
|
731
|
+
},
|
|
718
732
|
{
|
|
719
733
|
type: "function",
|
|
720
734
|
name: "xBNBToApUSDRate",
|
|
@@ -1211,6 +1225,48 @@ var AspanReadClient = class _AspanReadClient {
|
|
|
1211
1225
|
]);
|
|
1212
1226
|
return { shares, balance };
|
|
1213
1227
|
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Get realtime exchange rate using live xBNB price (not cached xBNBToApUSDRate).
|
|
1230
|
+
* Use this for accurate PnL display — the on-chain exchangeRate() only updates
|
|
1231
|
+
* on deposit/withdraw and doesn't reflect xBNB price changes between operations.
|
|
1232
|
+
*
|
|
1233
|
+
* @returns Exchange rate in 18 decimals (e.g. 1.15e18 means 1 sApUSD = 1.15 apUSD equivalent)
|
|
1234
|
+
*/
|
|
1235
|
+
async getRealtimeExchangeRate() {
|
|
1236
|
+
const sApUSDAddress = await this.getSApUSD();
|
|
1237
|
+
const PRECISION2 = 10n ** 18n;
|
|
1238
|
+
const VIRTUAL_ASSETS = 10n ** 6n;
|
|
1239
|
+
const VIRTUAL_SHARES = 10n ** 6n;
|
|
1240
|
+
const [accountedApUSD, accountedXBNB, totalSupply, xBNBPriceUSD] = await Promise.all([
|
|
1241
|
+
this.publicClient.readContract({
|
|
1242
|
+
address: sApUSDAddress,
|
|
1243
|
+
abi: SApUSDABI,
|
|
1244
|
+
functionName: "accountedApUSD"
|
|
1245
|
+
}),
|
|
1246
|
+
this.publicClient.readContract({
|
|
1247
|
+
address: sApUSDAddress,
|
|
1248
|
+
abi: SApUSDABI,
|
|
1249
|
+
functionName: "accountedXBNB"
|
|
1250
|
+
}),
|
|
1251
|
+
this.publicClient.readContract({
|
|
1252
|
+
address: sApUSDAddress,
|
|
1253
|
+
abi: SApUSDABI,
|
|
1254
|
+
functionName: "totalSupply"
|
|
1255
|
+
}),
|
|
1256
|
+
this.getXBNBPriceUSD()
|
|
1257
|
+
]);
|
|
1258
|
+
const totalValue = accountedXBNB === 0n || xBNBPriceUSD === 0n ? accountedApUSD : accountedApUSD + accountedXBNB * xBNBPriceUSD / PRECISION2;
|
|
1259
|
+
const supply = totalSupply + VIRTUAL_SHARES;
|
|
1260
|
+
const exchangeRate = supply === 0n ? PRECISION2 : (totalValue + VIRTUAL_ASSETS) * PRECISION2 / supply;
|
|
1261
|
+
return {
|
|
1262
|
+
exchangeRate,
|
|
1263
|
+
accountedApUSD,
|
|
1264
|
+
accountedXBNB,
|
|
1265
|
+
xBNBPriceUSD,
|
|
1266
|
+
totalValue,
|
|
1267
|
+
totalSupply
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1214
1270
|
async getExchangeRate() {
|
|
1215
1271
|
try {
|
|
1216
1272
|
return await this.publicClient.readContract({
|
|
@@ -2331,6 +2387,21 @@ var RouterABI = [
|
|
|
2331
2387
|
];
|
|
2332
2388
|
|
|
2333
2389
|
// src/router.ts
|
|
2390
|
+
var ChainlinkABI = [
|
|
2391
|
+
{
|
|
2392
|
+
type: "function",
|
|
2393
|
+
name: "latestRoundData",
|
|
2394
|
+
inputs: [],
|
|
2395
|
+
outputs: [
|
|
2396
|
+
{ name: "roundId", type: "uint80" },
|
|
2397
|
+
{ name: "answer", type: "int256" },
|
|
2398
|
+
{ name: "startedAt", type: "uint256" },
|
|
2399
|
+
{ name: "updatedAt", type: "uint256" },
|
|
2400
|
+
{ name: "answeredInRound", type: "uint80" }
|
|
2401
|
+
],
|
|
2402
|
+
stateMutability: "view"
|
|
2403
|
+
}
|
|
2404
|
+
];
|
|
2334
2405
|
var AspanRouterReadClient = class {
|
|
2335
2406
|
publicClient;
|
|
2336
2407
|
routerAddress;
|
|
@@ -2486,7 +2557,9 @@ var AspanRouterReadClient = class {
|
|
|
2486
2557
|
const usdValue = inputAmount * bnbPrice18 / one;
|
|
2487
2558
|
lstAmount = lstPrice18 === 0n ? 0n : usdValue * one / lstPrice18;
|
|
2488
2559
|
} else if (inNorm === usdt.toLowerCase() || inNorm === usdc.toLowerCase()) {
|
|
2489
|
-
|
|
2560
|
+
const stablePrice18 = await this.getStablecoinPrice(inputToken);
|
|
2561
|
+
const usdValue = inputAmount * stablePrice18 / one;
|
|
2562
|
+
lstAmount = lstPrice18 === 0n ? 0n : usdValue * one / lstPrice18;
|
|
2490
2563
|
} else {
|
|
2491
2564
|
throw new Error("Unsupported input token for SDK preview");
|
|
2492
2565
|
}
|
|
@@ -2532,7 +2605,8 @@ var AspanRouterReadClient = class {
|
|
|
2532
2605
|
if (outNorm === zeroAddress.toLowerCase() || outNorm === wbnb.toLowerCase()) {
|
|
2533
2606
|
outputAmount = bnbPrice18 === 0n ? 0n : usdValue * one / bnbPrice18;
|
|
2534
2607
|
} else if (outNorm === usdt.toLowerCase() || outNorm === usdc.toLowerCase()) {
|
|
2535
|
-
|
|
2608
|
+
const stablePrice18 = await this.getStablecoinPrice(outputToken);
|
|
2609
|
+
outputAmount = stablePrice18 === 0n ? 0n : usdValue * one / stablePrice18;
|
|
2536
2610
|
} else {
|
|
2537
2611
|
throw new Error("Unsupported output token for SDK preview");
|
|
2538
2612
|
}
|
|
@@ -2597,6 +2671,32 @@ var AspanRouterReadClient = class {
|
|
|
2597
2671
|
})
|
|
2598
2672
|
);
|
|
2599
2673
|
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Get stablecoin price in USD (18 decimals) from Chainlink.
|
|
2676
|
+
* Returns 1e18 if feed unavailable (graceful fallback).
|
|
2677
|
+
*/
|
|
2678
|
+
async getStablecoinPrice(token) {
|
|
2679
|
+
const usdt = await this.getUSDT();
|
|
2680
|
+
const usdc = await this.getUSDC();
|
|
2681
|
+
const norm = token.toLowerCase();
|
|
2682
|
+
let feed = null;
|
|
2683
|
+
if (norm === usdt.toLowerCase()) {
|
|
2684
|
+
feed = BSC_ADDRESSES.USDT_USD_FEED;
|
|
2685
|
+
} else if (norm === usdc.toLowerCase()) {
|
|
2686
|
+
feed = BSC_ADDRESSES.USDC_USD_FEED;
|
|
2687
|
+
}
|
|
2688
|
+
if (!feed) return 10n ** 18n;
|
|
2689
|
+
try {
|
|
2690
|
+
const [, answer] = await this.publicClient.readContract({
|
|
2691
|
+
address: feed,
|
|
2692
|
+
abi: ChainlinkABI,
|
|
2693
|
+
functionName: "latestRoundData"
|
|
2694
|
+
});
|
|
2695
|
+
return BigInt(answer) * 10n ** 10n;
|
|
2696
|
+
} catch {
|
|
2697
|
+
return 10n ** 18n;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2600
2700
|
async getSlisBNB() {
|
|
2601
2701
|
return this._getCachedAddress(
|
|
2602
2702
|
"slisBNB",
|
|
@@ -2901,27 +3001,30 @@ var BPS_PRECISION = 10000n;
|
|
|
2901
3001
|
var PRICE_PRECISION = 10n ** 8n;
|
|
2902
3002
|
var BSC_ADDRESSES = {
|
|
2903
3003
|
diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78",
|
|
2904
|
-
router: "
|
|
3004
|
+
router: "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E",
|
|
2905
3005
|
apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287",
|
|
2906
3006
|
xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43",
|
|
2907
3007
|
sApUSD: "0x896770Dba7c0481539E25aaB56bE285ECF6D65eB",
|
|
2908
3008
|
// LSTs
|
|
2909
3009
|
slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B",
|
|
2910
3010
|
asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6",
|
|
2911
|
-
wclisBNB: "
|
|
3011
|
+
wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07",
|
|
2912
3012
|
// Stablecoins
|
|
2913
3013
|
USDT: "0x55d398326f99059fF775485246999027B3197955",
|
|
2914
3014
|
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
3015
|
+
/** Chainlink price feeds (BSC Mainnet) */
|
|
3016
|
+
USDT_USD_FEED: "0xB97Ad0E74fa7d920791E90258A6E2085088b4320",
|
|
3017
|
+
USDC_USD_FEED: "0x51597f405303C4377E36123cBc172b13269EA163",
|
|
2915
3018
|
WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
|
|
2916
3019
|
};
|
|
2917
3020
|
var PHAROS_ADDRESSES = {
|
|
2918
|
-
diamond: "
|
|
2919
|
-
apUSD: "
|
|
2920
|
-
xBNB: "
|
|
2921
|
-
sApUSD: "
|
|
2922
|
-
mockLST: "
|
|
2923
|
-
mockPriceFeed: "
|
|
2924
|
-
mockExchangeRate: "
|
|
3021
|
+
diamond: "0x3d5ceb29BA3b4B837eddCAf53442371828933e5D",
|
|
3022
|
+
apUSD: "0x054f8DB2F700883949d657Da9418c6F6B734BA18",
|
|
3023
|
+
xBNB: "0x1da6F06F7195464732FB9ECa98DE8BC1c520C3a1",
|
|
3024
|
+
sApUSD: "0xf94f0b76942f83Bb4BC010a942c297D18842313b",
|
|
3025
|
+
mockLST: "0xDD890267E72320522E8C61aD357B650ebe4C47c5",
|
|
3026
|
+
mockPriceFeed: "0x4180062949036fCe833027f0ABf7052e7485ddfC",
|
|
3027
|
+
mockExchangeRate: "0xb9aDC2552cF3Fb223236CfEa1c78D277fA8fb8B6"
|
|
2925
3028
|
};
|
|
2926
3029
|
function formatAmount(amount, decimals = 4) {
|
|
2927
3030
|
const divisor = 10n ** BigInt(18 - decimals);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspan/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "TypeScript SDK for Aspan Protocol - LST-backed stablecoin on BNB Chain",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -45,18 +45,18 @@
|
|
|
45
45
|
"author": "",
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"viem": "
|
|
48
|
+
"viem": ">=2.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^20.10.0",
|
|
52
|
-
"tsup": "^8.0.1",
|
|
53
|
-
"typescript": "^5.3.0",
|
|
54
|
-
"vitest": "^1.0.0",
|
|
55
|
-
"eslint": "^8.55.0",
|
|
56
52
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
57
53
|
"@typescript-eslint/parser": "^6.13.0",
|
|
54
|
+
"dotenv": "^16.3.1",
|
|
55
|
+
"eslint": "^8.55.0",
|
|
56
|
+
"tsup": "^8.0.1",
|
|
58
57
|
"tsx": "^4.7.0",
|
|
59
|
-
"
|
|
58
|
+
"typescript": "^5.3.0",
|
|
59
|
+
"vitest": "^1.6.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"viem": ">=2.0.0"
|
|
@@ -61,7 +61,7 @@ const BSC_RPC_URL = process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org
|
|
|
61
61
|
const ANVIL_DEFAULT_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
|
62
62
|
|
|
63
63
|
const DIAMOND = "0x6a11B30d3a70727d5477D6d8090e144443fA1c78" as Address;
|
|
64
|
-
const ROUTER = "
|
|
64
|
+
const ROUTER = "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E" as Address;
|
|
65
65
|
|
|
66
66
|
const TOKENS = {
|
|
67
67
|
WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as Address,
|
|
@@ -69,7 +69,7 @@ const TOKENS = {
|
|
|
69
69
|
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" as Address,
|
|
70
70
|
slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as Address,
|
|
71
71
|
asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6" as Address,
|
|
72
|
-
wclisBNB: "
|
|
72
|
+
wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07" as Address,
|
|
73
73
|
apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287" as Address,
|
|
74
74
|
xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43" as Address,
|
|
75
75
|
sApUSD: "0xE2BE739C4aA4126ee72D612d9548C38B1B0e5A1b" as Address,
|
|
@@ -43,9 +43,9 @@ const TOKENS = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
// Minimal amounts — just enough to verify path connectivity
|
|
46
|
-
const BNB_AMOUNT = parseEther("0.
|
|
46
|
+
const BNB_AMOUNT = parseEther("0.002");
|
|
47
47
|
const USDT_AMOUNT = parseAmount("1");
|
|
48
|
-
const WBNB_AMOUNT = parseEther("0.
|
|
48
|
+
const WBNB_AMOUNT = parseEther("0.002");
|
|
49
49
|
const USDC_AMOUNT = parseAmount("1");
|
|
50
50
|
|
|
51
51
|
const ERC20_ABI = [
|
|
@@ -589,17 +589,26 @@ describe("AspanRouter SDK", () => {
|
|
|
589
589
|
|
|
590
590
|
const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
|
|
591
591
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
592
|
+
try {
|
|
593
|
+
const unstakeHash = await writeClient.redeemAndRequestUnstake({
|
|
594
|
+
redeemXBNB: true,
|
|
595
|
+
amount: freshMinted,
|
|
596
|
+
});
|
|
597
|
+
const unstakeReceipt = await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
|
|
598
|
+
await sleep(2000);
|
|
599
|
+
expect(unstakeReceipt.status).toBe("success");
|
|
600
|
+
|
|
601
|
+
const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
|
|
602
|
+
expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
|
|
603
|
+
console.log(` Unstake requested ✓ (new indices: ${indicesAfter.length - indicesBefore.length})`);
|
|
604
|
+
} catch (err: any) {
|
|
605
|
+
const msg = err.shortMessage || err.message || "";
|
|
606
|
+
if (msg.includes("too small")) {
|
|
607
|
+
console.log(` ⚠️ Amount too small for Lista unstake (min ~0.001 BNB equivalent), skipped`);
|
|
608
|
+
} else {
|
|
609
|
+
throw err;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
603
612
|
}, 180000);
|
|
604
613
|
|
|
605
614
|
// Test 9: stakeAndMint xBNB
|
|
@@ -854,7 +863,8 @@ describe("AspanRouter SDK", () => {
|
|
|
854
863
|
return;
|
|
855
864
|
}
|
|
856
865
|
|
|
857
|
-
|
|
866
|
+
// Lista requires minimum ~0.001 BNB equivalent; at ~$670/BNB, that's ~$0.67 apUSD min
|
|
867
|
+
const unstakeAmount = apUSDBalance > parseAmount("1.0") ? parseAmount("1.0") : apUSDBalance;
|
|
858
868
|
console.log(` Requesting unstake for ${formatAmount(unstakeAmount)} apUSD (of ${formatAmount(apUSDBalance)} total)...`);
|
|
859
869
|
await approve(TOKENS.apUSD, unstakeAmount);
|
|
860
870
|
|
package/src/abi/sApUSD.ts
CHANGED
|
@@ -64,6 +64,20 @@ export const SApUSDABI = [
|
|
|
64
64
|
],
|
|
65
65
|
stateMutability: "view"
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
name: "accountedApUSD",
|
|
70
|
+
inputs: [],
|
|
71
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
72
|
+
stateMutability: "view"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "function",
|
|
76
|
+
name: "accountedXBNB",
|
|
77
|
+
inputs: [],
|
|
78
|
+
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
|
|
79
|
+
stateMutability: "view"
|
|
80
|
+
},
|
|
67
81
|
{
|
|
68
82
|
type: "function",
|
|
69
83
|
name: "xBNBToApUSDRate",
|
|
@@ -318,11 +318,25 @@ export class RiskKeeperService {
|
|
|
318
318
|
|
|
319
319
|
this.state.isProcessing = true;
|
|
320
320
|
|
|
321
|
-
//
|
|
322
|
-
|
|
321
|
+
// Contract allows full clean when vault xBNB <= 10 xBNB (dust exemption)
|
|
322
|
+
// Otherwise capped at 10% per call (MAX_CLEAN_PERCENT_BPS = 1000)
|
|
323
|
+
const DUST_THRESHOLD = 10n * 10n ** 18n; // 10 xBNB — must match contract DUST_CLEAN_THRESHOLD
|
|
324
|
+
const FULL_CLEAN_THRESHOLD = 1n * 10n ** 18n; // 1 xBNB — below this, clean everything
|
|
325
|
+
let cleanAmount: bigint;
|
|
326
|
+
|
|
327
|
+
if (vaultState.xBNBAmount <= FULL_CLEAN_THRESHOLD) {
|
|
328
|
+
// Less than 1 xBNB remaining — clean all to avoid infinite tiny trades
|
|
329
|
+
cleanAmount = vaultState.xBNBAmount;
|
|
330
|
+
console.log(`[RiskKeeper] Below 1 xBNB (${Number(vaultState.xBNBAmount) / 1e18}), cleaning all in one shot`);
|
|
331
|
+
} else if (vaultState.xBNBAmount <= DUST_THRESHOLD) {
|
|
332
|
+
cleanAmount = vaultState.xBNBAmount;
|
|
333
|
+
console.log(`[RiskKeeper] Dust amount (${Number(vaultState.xBNBAmount) / 1e18} xBNB <= 10), cleaning all in one shot`);
|
|
334
|
+
} else {
|
|
335
|
+
cleanAmount = (vaultState.xBNBAmount * BigInt(this.TWAP_PERCENTAGE)) / 100n;
|
|
336
|
+
}
|
|
323
337
|
|
|
324
338
|
if (cleanAmount === 0n) {
|
|
325
|
-
console.log("[RiskKeeper] Clean amount
|
|
339
|
+
console.log("[RiskKeeper] Clean amount rounds to 0, skipping");
|
|
326
340
|
return;
|
|
327
341
|
}
|
|
328
342
|
|
package/src/client.ts
CHANGED
|
@@ -551,6 +551,66 @@ export class AspanReadClient {
|
|
|
551
551
|
return { shares, balance };
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Get realtime exchange rate using live xBNB price (not cached xBNBToApUSDRate).
|
|
556
|
+
* Use this for accurate PnL display — the on-chain exchangeRate() only updates
|
|
557
|
+
* on deposit/withdraw and doesn't reflect xBNB price changes between operations.
|
|
558
|
+
*
|
|
559
|
+
* @returns Exchange rate in 18 decimals (e.g. 1.15e18 means 1 sApUSD = 1.15 apUSD equivalent)
|
|
560
|
+
*/
|
|
561
|
+
async getRealtimeExchangeRate(): Promise<{
|
|
562
|
+
exchangeRate: bigint;
|
|
563
|
+
accountedApUSD: bigint;
|
|
564
|
+
accountedXBNB: bigint;
|
|
565
|
+
xBNBPriceUSD: bigint;
|
|
566
|
+
totalValue: bigint;
|
|
567
|
+
totalSupply: bigint;
|
|
568
|
+
}> {
|
|
569
|
+
const sApUSDAddress = await this.getSApUSD();
|
|
570
|
+
const PRECISION = 10n ** 18n;
|
|
571
|
+
const VIRTUAL_ASSETS = 10n ** 6n;
|
|
572
|
+
const VIRTUAL_SHARES = 10n ** 6n;
|
|
573
|
+
|
|
574
|
+
const [accountedApUSD, accountedXBNB, totalSupply, xBNBPriceUSD] = await Promise.all([
|
|
575
|
+
this.publicClient.readContract({
|
|
576
|
+
address: sApUSDAddress,
|
|
577
|
+
abi: SApUSDABI,
|
|
578
|
+
functionName: "accountedApUSD",
|
|
579
|
+
}) as Promise<bigint>,
|
|
580
|
+
this.publicClient.readContract({
|
|
581
|
+
address: sApUSDAddress,
|
|
582
|
+
abi: SApUSDABI,
|
|
583
|
+
functionName: "accountedXBNB",
|
|
584
|
+
}) as Promise<bigint>,
|
|
585
|
+
this.publicClient.readContract({
|
|
586
|
+
address: sApUSDAddress,
|
|
587
|
+
abi: SApUSDABI,
|
|
588
|
+
functionName: "totalSupply",
|
|
589
|
+
}) as Promise<bigint>,
|
|
590
|
+
this.getXBNBPriceUSD(),
|
|
591
|
+
]);
|
|
592
|
+
|
|
593
|
+
// _totalValue = accountedApUSD + (accountedXBNB * xBNBPrice) / 1e18
|
|
594
|
+
const totalValue = accountedXBNB === 0n || xBNBPriceUSD === 0n
|
|
595
|
+
? accountedApUSD
|
|
596
|
+
: accountedApUSD + (accountedXBNB * xBNBPriceUSD) / PRECISION;
|
|
597
|
+
|
|
598
|
+
// exchangeRate = (totalValue + VIRTUAL_ASSETS) * 1e18 / (totalSupply + VIRTUAL_SHARES)
|
|
599
|
+
const supply = totalSupply + VIRTUAL_SHARES;
|
|
600
|
+
const exchangeRate = supply === 0n
|
|
601
|
+
? PRECISION
|
|
602
|
+
: ((totalValue + VIRTUAL_ASSETS) * PRECISION) / supply;
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
exchangeRate,
|
|
606
|
+
accountedApUSD,
|
|
607
|
+
accountedXBNB,
|
|
608
|
+
xBNBPriceUSD,
|
|
609
|
+
totalValue,
|
|
610
|
+
totalSupply,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
554
614
|
async getExchangeRate(): Promise<bigint> {
|
|
555
615
|
try {
|
|
556
616
|
return await this.publicClient.readContract({
|
package/src/index.ts
CHANGED
|
@@ -116,28 +116,31 @@ export const PRICE_PRECISION = 10n ** 8n; // Chainlink price precision
|
|
|
116
116
|
// ============ Contract Addresses (BSC Mainnet) ============
|
|
117
117
|
export const BSC_ADDRESSES = {
|
|
118
118
|
diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78" as const,
|
|
119
|
-
router: "
|
|
119
|
+
router: "0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E" as const,
|
|
120
120
|
apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287" as const,
|
|
121
121
|
xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43" as const,
|
|
122
122
|
sApUSD: "0x896770Dba7c0481539E25aaB56bE285ECF6D65eB" as const,
|
|
123
123
|
// LSTs
|
|
124
124
|
slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as const,
|
|
125
125
|
asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6" as const,
|
|
126
|
-
wclisBNB: "
|
|
126
|
+
wclisBNB: "0xb2A0631bF0aC326fEefc201E7337E13C63Bbed07" as const,
|
|
127
127
|
// Stablecoins
|
|
128
128
|
USDT: "0x55d398326f99059fF775485246999027B3197955" as const,
|
|
129
129
|
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" as const,
|
|
130
|
+
/** Chainlink price feeds (BSC Mainnet) */
|
|
131
|
+
USDT_USD_FEED: "0xB97Ad0E74fa7d920791E90258A6E2085088b4320" as const,
|
|
132
|
+
USDC_USD_FEED: "0x51597f405303C4377E36123cBc172b13269EA163" as const,
|
|
130
133
|
WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as const,
|
|
131
134
|
} as const;
|
|
132
135
|
|
|
133
136
|
export const PHAROS_ADDRESSES = {
|
|
134
|
-
diamond: "
|
|
135
|
-
apUSD: "
|
|
136
|
-
xBNB: "
|
|
137
|
-
sApUSD: "
|
|
138
|
-
mockLST: "
|
|
139
|
-
mockPriceFeed: "
|
|
140
|
-
mockExchangeRate: "
|
|
137
|
+
diamond: "0x3d5ceb29BA3b4B837eddCAf53442371828933e5D" as const,
|
|
138
|
+
apUSD: "0x054f8DB2F700883949d657Da9418c6F6B734BA18" as const,
|
|
139
|
+
xBNB: "0x1da6F06F7195464732FB9ECa98DE8BC1c520C3a1" as const,
|
|
140
|
+
sApUSD: "0xf94f0b76942f83Bb4BC010a942c297D18842313b" as const,
|
|
141
|
+
mockLST: "0xDD890267E72320522E8C61aD357B650ebe4C47c5" as const,
|
|
142
|
+
mockPriceFeed: "0x4180062949036fCe833027f0ABf7052e7485ddfC" as const,
|
|
143
|
+
mockExchangeRate: "0xb9aDC2552cF3Fb223236CfEa1c78D277fA8fb8B6" as const,
|
|
141
144
|
} as const;
|
|
142
145
|
|
|
143
146
|
// ============ Utility Functions ============
|
package/src/router.ts
CHANGED
|
@@ -19,8 +19,26 @@ import {
|
|
|
19
19
|
} from "viem";
|
|
20
20
|
import { bsc, bscTestnet } from "viem/chains";
|
|
21
21
|
import { pharosTestnet, CHAIN_IDS, getChainById } from "./client";
|
|
22
|
+
import { BSC_ADDRESSES } from "./index";
|
|
22
23
|
import { RouterABI } from "./abi/router";
|
|
23
24
|
import { DiamondABI } from "./abi/diamond";
|
|
25
|
+
|
|
26
|
+
/** Chainlink AggregatorV3 — only latestRoundData needed */
|
|
27
|
+
const ChainlinkABI = [
|
|
28
|
+
{
|
|
29
|
+
type: "function",
|
|
30
|
+
name: "latestRoundData",
|
|
31
|
+
inputs: [],
|
|
32
|
+
outputs: [
|
|
33
|
+
{ name: "roundId", type: "uint80" },
|
|
34
|
+
{ name: "answer", type: "int256" },
|
|
35
|
+
{ name: "startedAt", type: "uint256" },
|
|
36
|
+
{ name: "updatedAt", type: "uint256" },
|
|
37
|
+
{ name: "answeredInRound", type: "uint80" },
|
|
38
|
+
],
|
|
39
|
+
stateMutability: "view",
|
|
40
|
+
},
|
|
41
|
+
] as const;
|
|
24
42
|
import type {
|
|
25
43
|
SwapAndMintParams,
|
|
26
44
|
StakeAndMintParams,
|
|
@@ -233,8 +251,10 @@ export class AspanRouterReadClient {
|
|
|
233
251
|
const usdValue = (inputAmount * bnbPrice18) / one;
|
|
234
252
|
lstAmount = lstPrice18 === 0n ? 0n : (usdValue * one) / lstPrice18;
|
|
235
253
|
} else if (inNorm === usdt.toLowerCase() || inNorm === usdc.toLowerCase()) {
|
|
236
|
-
// stablecoin
|
|
237
|
-
|
|
254
|
+
// stablecoin -> USD (via Chainlink) -> LST
|
|
255
|
+
const stablePrice18 = await this.getStablecoinPrice(inputToken);
|
|
256
|
+
const usdValue = (inputAmount * stablePrice18) / one;
|
|
257
|
+
lstAmount = lstPrice18 === 0n ? 0n : (usdValue * one) / lstPrice18;
|
|
238
258
|
} else {
|
|
239
259
|
throw new Error("Unsupported input token for SDK preview");
|
|
240
260
|
}
|
|
@@ -292,7 +312,9 @@ export class AspanRouterReadClient {
|
|
|
292
312
|
if (outNorm === zeroAddress.toLowerCase() || outNorm === wbnb.toLowerCase()) {
|
|
293
313
|
outputAmount = bnbPrice18 === 0n ? 0n : (usdValue * one) / bnbPrice18;
|
|
294
314
|
} else if (outNorm === usdt.toLowerCase() || outNorm === usdc.toLowerCase()) {
|
|
295
|
-
|
|
315
|
+
// USD value -> stablecoin amount (via Chainlink price)
|
|
316
|
+
const stablePrice18 = await this.getStablecoinPrice(outputToken);
|
|
317
|
+
outputAmount = stablePrice18 === 0n ? 0n : (usdValue * one) / stablePrice18;
|
|
296
318
|
} else {
|
|
297
319
|
throw new Error("Unsupported output token for SDK preview");
|
|
298
320
|
}
|
|
@@ -365,6 +387,34 @@ export class AspanRouterReadClient {
|
|
|
365
387
|
);
|
|
366
388
|
}
|
|
367
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Get stablecoin price in USD (18 decimals) from Chainlink.
|
|
392
|
+
* Returns 1e18 if feed unavailable (graceful fallback).
|
|
393
|
+
*/
|
|
394
|
+
async getStablecoinPrice(token: Address): Promise<bigint> {
|
|
395
|
+
const usdt = await this.getUSDT();
|
|
396
|
+
const usdc = await this.getUSDC();
|
|
397
|
+
const norm = token.toLowerCase();
|
|
398
|
+
let feed: Address | null = null;
|
|
399
|
+
if (norm === usdt.toLowerCase()) {
|
|
400
|
+
feed = BSC_ADDRESSES.USDT_USD_FEED as Address;
|
|
401
|
+
} else if (norm === usdc.toLowerCase()) {
|
|
402
|
+
feed = BSC_ADDRESSES.USDC_USD_FEED as Address;
|
|
403
|
+
}
|
|
404
|
+
if (!feed) return 10n ** 18n; // not a stablecoin, assume 1:1
|
|
405
|
+
try {
|
|
406
|
+
const [, answer] = await this.publicClient.readContract({
|
|
407
|
+
address: feed,
|
|
408
|
+
abi: ChainlinkABI,
|
|
409
|
+
functionName: "latestRoundData",
|
|
410
|
+
});
|
|
411
|
+
// Chainlink returns 8 decimals, normalize to 18
|
|
412
|
+
return BigInt(answer) * 10n ** 10n;
|
|
413
|
+
} catch {
|
|
414
|
+
return 10n ** 18n; // fallback: 1 USD
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
368
418
|
async getSlisBNB(): Promise<Address> {
|
|
369
419
|
return this._getCachedAddress("slisBNB", async () =>
|
|
370
420
|
this.publicClient.readContract({
|
package/src/types.ts
CHANGED
|
@@ -251,7 +251,7 @@ export interface RouterSwapParams {
|
|
|
251
251
|
targetLST: Address;
|
|
252
252
|
/** Minimum LST to receive from swap (slippage protection) */
|
|
253
253
|
minLSTOut: bigint;
|
|
254
|
-
/** PancakeSwap V3 pool fee tier (500, 2500, 10000) */
|
|
254
|
+
/** PancakeSwap V3 pool fee tier (100, 500, 2500, 10000) */
|
|
255
255
|
poolFee: number;
|
|
256
256
|
}
|
|
257
257
|
|