@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 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** | `0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3` |
25
+ | **Router** | `0xb8a90CD2811d6DDbB4B7969d30B036574842cb6E` |
26
26
  | **ApUSD** | `0x4570047eeB5aDb4081c5d470494EB5134e34A287` |
27
27
  | **XBNB** | `0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43` |
28
28
  | **SApUSD** | `0xE2BE739C4aA4126ee72D612d9548C38B1B0e5A1b` |
29
- | **wclisBNB** | `0x448f7c2fa4e5135a4a5B50879602cf3CD428e108` |
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: "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3";
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108";
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: "0x67011Ce4B5E534FA78dD9922559B7005197DBcc8";
2825
- readonly apUSD: "0x82ac96db027772cE8a770e099370b5D7941B3ae4";
2826
- readonly xBNB: "0xFF43b2f50b2c6c588d14Ea4565Ee5Eb93e438576";
2827
- readonly sApUSD: "0xFA4B826CfD5faaAfA48E4963d8a7d6Cf9889A9A8";
2828
- readonly mockLST: "0xc7BB55759e8e04AfAA620De96E9E133B1FAd3e18";
2829
- readonly mockPriceFeed: "0xa2B41DDE7a7BB0A897DE476Ea441fB27A6978682";
2830
- readonly mockExchangeRate: "0xffA1938e072Ea3b144EA7e61662dd8B0818D82B9";
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: "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3";
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108";
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: "0x67011Ce4B5E534FA78dD9922559B7005197DBcc8";
2825
- readonly apUSD: "0x82ac96db027772cE8a770e099370b5D7941B3ae4";
2826
- readonly xBNB: "0xFF43b2f50b2c6c588d14Ea4565Ee5Eb93e438576";
2827
- readonly sApUSD: "0xFA4B826CfD5faaAfA48E4963d8a7d6Cf9889A9A8";
2828
- readonly mockLST: "0xc7BB55759e8e04AfAA620De96E9E133B1FAd3e18";
2829
- readonly mockPriceFeed: "0xa2B41DDE7a7BB0A897DE476Ea441fB27A6978682";
2830
- readonly mockExchangeRate: "0xffA1938e072Ea3b144EA7e61662dd8B0818D82B9";
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
- lstAmount = lstPrice18 === 0n ? 0n : inputAmount * one / lstPrice18;
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
- outputAmount = usdValue;
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: "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3",
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108",
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: "0x67011Ce4B5E534FA78dD9922559B7005197DBcc8",
2972
- apUSD: "0x82ac96db027772cE8a770e099370b5D7941B3ae4",
2973
- xBNB: "0xFF43b2f50b2c6c588d14Ea4565Ee5Eb93e438576",
2974
- sApUSD: "0xFA4B826CfD5faaAfA48E4963d8a7d6Cf9889A9A8",
2975
- mockLST: "0xc7BB55759e8e04AfAA620De96E9E133B1FAd3e18",
2976
- mockPriceFeed: "0xa2B41DDE7a7BB0A897DE476Ea441fB27A6978682",
2977
- mockExchangeRate: "0xffA1938e072Ea3b144EA7e61662dd8B0818D82B9"
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
- lstAmount = lstPrice18 === 0n ? 0n : inputAmount * one / lstPrice18;
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
- outputAmount = usdValue;
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: "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3",
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108",
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: "0x67011Ce4B5E534FA78dD9922559B7005197DBcc8",
2919
- apUSD: "0x82ac96db027772cE8a770e099370b5D7941B3ae4",
2920
- xBNB: "0xFF43b2f50b2c6c588d14Ea4565Ee5Eb93e438576",
2921
- sApUSD: "0xFA4B826CfD5faaAfA48E4963d8a7d6Cf9889A9A8",
2922
- mockLST: "0xc7BB55759e8e04AfAA620De96E9E133B1FAd3e18",
2923
- mockPriceFeed: "0xa2B41DDE7a7BB0A897DE476Ea441fB27A6978682",
2924
- mockExchangeRate: "0xffA1938e072Ea3b144EA7e61662dd8B0818D82B9"
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.0",
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": "^2.21.0"
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
- "dotenv": "^16.3.1"
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 = "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3" as Address;
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108" as Address,
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.001");
46
+ const BNB_AMOUNT = parseEther("0.002");
47
47
  const USDT_AMOUNT = parseAmount("1");
48
- const WBNB_AMOUNT = parseEther("0.001");
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
- const unstakeHash = await writeClient.redeemAndRequestUnstake({
593
- redeemXBNB: true,
594
- amount: freshMinted,
595
- });
596
- const unstakeReceipt = await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
597
- await sleep(2000);
598
- expect(unstakeReceipt.status).toBe("success");
599
-
600
- const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
601
- expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
602
- console.log(` Unstake requested ✓ (new indices: ${indicesAfter.length - indicesBefore.length})`);
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
- const unstakeAmount = apUSDBalance > parseAmount("0.5") ? parseAmount("0.5") : apUSDBalance;
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
- // Calculate amount to clean (X% of total)
322
- const cleanAmount = (vaultState.xBNBAmount * BigInt(this.TWAP_PERCENTAGE)) / 100n;
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 too small, skipping");
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: "0x1Ca976d2043dfb785c7F3A17535B3A38c50113f3" as const,
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: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108" as const,
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: "0x67011Ce4B5E534FA78dD9922559B7005197DBcc8" as const,
135
- apUSD: "0x82ac96db027772cE8a770e099370b5D7941B3ae4" as const,
136
- xBNB: "0xFF43b2f50b2c6c588d14Ea4565Ee5Eb93e438576" as const,
137
- sApUSD: "0xFA4B826CfD5faaAfA48E4963d8a7d6Cf9889A9A8" as const,
138
- mockLST: "0xc7BB55759e8e04AfAA620De96E9E133B1FAd3e18" as const,
139
- mockPriceFeed: "0xa2B41DDE7a7BB0A897DE476Ea441fB27A6978682" as const,
140
- mockExchangeRate: "0xffA1938e072Ea3b144EA7e61662dd8B0818D82B9" as const,
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 (18-decimal normalized in this SDK path) -> LST
237
- lstAmount = lstPrice18 === 0n ? 0n : (inputAmount * one) / lstPrice18;
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
- outputAmount = usdValue;
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