@aspan/sdk 0.3.1 → 0.4.1

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.
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Risk Keeper Service
3
+ * Executes risk management actions: SM2 trigger and TWAP vault cleaning
4
+ */
5
+
6
+ import {
7
+ createPublicClient,
8
+ createWalletClient,
9
+ http,
10
+ type Address,
11
+ type PublicClient,
12
+ type WalletClient,
13
+ type Chain,
14
+ type Account,
15
+ type Hash,
16
+ } from "viem";
17
+ import { privateKeyToAccount } from "viem/accounts";
18
+ import { bsc } from "viem/chains";
19
+ import type { AspanReadClient } from "../../client";
20
+ import { DiamondABI } from "../../abi/diamond";
21
+ import { SApUSDABI } from "../../abi/sApUSD";
22
+ import type { SlackService } from "./slack";
23
+ import { SlackColors } from "./slack";
24
+ import type { BotConfig } from "../config";
25
+ import { withRetry } from "./rpc-client";
26
+
27
+ // ============ Types ============
28
+
29
+ export interface RiskKeeperConfig {
30
+ /** Diamond contract address */
31
+ diamondAddress: Address;
32
+ /** sApUSD vault address */
33
+ sApUSDAddress: Address;
34
+ /** DEX router address (PancakeSwap) */
35
+ routerAddress: Address;
36
+ /** WBNB address */
37
+ wbnbAddress: Address;
38
+ /** Keeper private key */
39
+ privateKey: `0x${string}`;
40
+ /** RPC URL */
41
+ rpcUrl: string;
42
+ /** Chain */
43
+ chain?: Chain;
44
+ /** SM2 cooldown in ms (default 30 min) */
45
+ sm2CooldownMs?: number;
46
+ /** SM2 max pool percentage per trigger (default 40%) */
47
+ sm2MaxPoolPercent?: number;
48
+ /** TWAP interval in ms (default 10 min) */
49
+ twapIntervalMs?: number;
50
+ /** TWAP percentage per batch (default 5%) */
51
+ twapPercentage?: number;
52
+ /** Max slippage BPS (default 50 = 0.5%) */
53
+ maxSlippageBps?: number;
54
+ /** CR threshold to start TWAP cleaning (default 13500 = 135%) */
55
+ twapStartCR?: bigint;
56
+ /** Max BNB price volatility for TWAP (default 5%) */
57
+ maxVolatilityPercent?: number;
58
+ }
59
+
60
+ interface KeeperState {
61
+ lastSM2Trigger: number;
62
+ lastTWAPExecution: number;
63
+ isProcessing: boolean;
64
+ }
65
+
66
+ // ============ Risk Keeper Service ============
67
+
68
+ export class RiskKeeperService {
69
+ private readonly client: AspanReadClient;
70
+ private readonly publicClient: PublicClient;
71
+ private readonly walletClient: WalletClient;
72
+ private readonly slack: SlackService;
73
+ private readonly config: BotConfig;
74
+ private readonly keeperConfig: RiskKeeperConfig;
75
+ private readonly account: Account;
76
+
77
+ private state: KeeperState = {
78
+ lastSM2Trigger: 0,
79
+ lastTWAPExecution: 0,
80
+ isProcessing: false,
81
+ };
82
+
83
+ private sm2IntervalId?: ReturnType<typeof setInterval>;
84
+ private twapIntervalId?: ReturnType<typeof setInterval>;
85
+
86
+ // Defaults
87
+ private readonly SM2_COOLDOWN_MS: number;
88
+ private readonly SM2_MAX_POOL_PERCENT: number;
89
+ private readonly TWAP_INTERVAL_MS: number;
90
+ private readonly TWAP_PERCENTAGE: number;
91
+ private readonly MAX_SLIPPAGE_BPS: number;
92
+ private readonly TWAP_START_CR: bigint;
93
+ private readonly MAX_VOLATILITY_PERCENT: number;
94
+
95
+ // Price tracking for volatility
96
+ private lastBNBPrice: bigint = 0n;
97
+ /** Timestamp of last BNB price check (used for volatility tracking) */
98
+ public lastPriceTime: number = 0;
99
+
100
+ constructor(
101
+ client: AspanReadClient,
102
+ slack: SlackService,
103
+ botConfig: BotConfig,
104
+ keeperConfig: RiskKeeperConfig
105
+ ) {
106
+ this.client = client;
107
+ this.slack = slack;
108
+ this.config = botConfig;
109
+ this.keeperConfig = keeperConfig;
110
+
111
+ // Set defaults
112
+ this.SM2_COOLDOWN_MS = keeperConfig.sm2CooldownMs ?? 30 * 60 * 1000; // 30 min
113
+ this.SM2_MAX_POOL_PERCENT = keeperConfig.sm2MaxPoolPercent ?? 40; // 40% max per trigger
114
+ this.TWAP_INTERVAL_MS = keeperConfig.twapIntervalMs ?? 10 * 60 * 1000; // 10 min
115
+ this.TWAP_PERCENTAGE = keeperConfig.twapPercentage ?? 5; // 5%
116
+ this.MAX_SLIPPAGE_BPS = keeperConfig.maxSlippageBps ?? 50; // 0.5%
117
+ this.TWAP_START_CR = keeperConfig.twapStartCR ?? 13500n; // 135%
118
+ this.MAX_VOLATILITY_PERCENT = keeperConfig.maxVolatilityPercent ?? 5; // 5%
119
+
120
+ // Setup account and clients
121
+ this.account = privateKeyToAccount(keeperConfig.privateKey);
122
+ const chain = keeperConfig.chain ?? bsc;
123
+
124
+ this.publicClient = createPublicClient({
125
+ chain,
126
+ transport: http(keeperConfig.rpcUrl),
127
+ });
128
+
129
+ this.walletClient = createWalletClient({
130
+ chain,
131
+ transport: http(keeperConfig.rpcUrl),
132
+ account: this.account,
133
+ });
134
+
135
+ console.log(`[RiskKeeper] Initialized with address: ${this.account.address}`);
136
+ }
137
+
138
+ // ============ Lifecycle ============
139
+
140
+ start(): void {
141
+ console.log("[RiskKeeper] Starting risk keeper service...");
142
+ console.log(`[RiskKeeper] SM2 cooldown: ${this.SM2_COOLDOWN_MS / 1000}s`);
143
+ console.log(`[RiskKeeper] TWAP interval: ${this.TWAP_INTERVAL_MS / 1000}s`);
144
+ console.log(`[RiskKeeper] TWAP percentage: ${this.TWAP_PERCENTAGE}%`);
145
+
146
+ // Initial checks
147
+ this.checkAndTriggerSM2();
148
+ this.checkAndExecuteTWAP();
149
+
150
+ // Schedule regular checks
151
+ this.sm2IntervalId = setInterval(
152
+ () => this.checkAndTriggerSM2(),
153
+ 30 * 1000 // Check every 30 seconds
154
+ );
155
+
156
+ this.twapIntervalId = setInterval(
157
+ () => this.checkAndExecuteTWAP(),
158
+ this.TWAP_INTERVAL_MS
159
+ );
160
+
161
+ console.log("[RiskKeeper] Service started");
162
+ }
163
+
164
+ stop(): void {
165
+ if (this.sm2IntervalId) {
166
+ clearInterval(this.sm2IntervalId);
167
+ this.sm2IntervalId = undefined;
168
+ }
169
+ if (this.twapIntervalId) {
170
+ clearInterval(this.twapIntervalId);
171
+ this.twapIntervalId = undefined;
172
+ }
173
+ console.log("[RiskKeeper] Service stopped");
174
+ }
175
+
176
+ // ============ Stability Mode 2 ============
177
+
178
+ private async checkAndTriggerSM2(): Promise<void> {
179
+ if (this.state.isProcessing) return;
180
+
181
+ try {
182
+ // Check cooldown
183
+ const now = Date.now();
184
+ if (now - this.state.lastSM2Trigger < this.SM2_COOLDOWN_MS) {
185
+ return;
186
+ }
187
+
188
+ // Check if SM2 can be triggered
189
+ const sm2Info = await withRetry(
190
+ () => this.client.canTriggerStabilityMode2(),
191
+ this.config,
192
+ "canTriggerStabilityMode2"
193
+ );
194
+
195
+ if (!sm2Info.canTrigger) {
196
+ return;
197
+ }
198
+
199
+ // Check if stability pool has balance
200
+ if (sm2Info.potentialConversion === 0n) {
201
+ console.log("[RiskKeeper] SM2 triggered but no conversion possible (empty pool)");
202
+ return;
203
+ }
204
+
205
+ // Get pool balance and enforce 40% max
206
+ const poolStats = await withRetry(
207
+ () => this.client.getStabilityPoolStats(),
208
+ this.config,
209
+ "getStabilityPoolStats"
210
+ );
211
+
212
+ const maxConversion = (poolStats.totalStaked * BigInt(this.SM2_MAX_POOL_PERCENT)) / 100n;
213
+
214
+ // If potential conversion exceeds 40%, we need a lower target CR
215
+ // For simplicity, we'll just warn and proceed - the contract will cap at pool balance anyway
216
+ if (sm2Info.potentialConversion > maxConversion) {
217
+ console.log(`[RiskKeeper] Potential conversion ${sm2Info.potentialConversion} exceeds ${this.SM2_MAX_POOL_PERCENT}% limit (${maxConversion})`);
218
+ console.log(`[RiskKeeper] Will execute with reduced target to stay within limit`);
219
+ }
220
+
221
+ console.log(`[RiskKeeper] SM2 conditions met! CR: ${sm2Info.currentCR}, Potential: ${sm2Info.potentialConversion}`);
222
+
223
+ this.state.isProcessing = true;
224
+
225
+ // Calculate target CR that respects the 40% limit
226
+ // If potentialConversion > maxConversion, use a lower target
227
+ let targetCR = 14000n; // Default 140%
228
+ if (sm2Info.potentialConversion > maxConversion && maxConversion > 0n) {
229
+ // Calculate what CR we'd get with maxConversion
230
+ // This is approximate - actual CR depends on TVL which we fetch
231
+ const tvl = await withRetry(
232
+ () => this.client.getTVLInUSD(),
233
+ this.config,
234
+ "getTVLInUSD"
235
+ );
236
+ const apUSDSupply = await withRetry(
237
+ () => this.client.getApUSDSupply(),
238
+ this.config,
239
+ "getApUSDSupply"
240
+ );
241
+
242
+ // New supply after burning maxConversion
243
+ const newSupply = apUSDSupply - maxConversion;
244
+ if (newSupply > 0n) {
245
+ // targetCR = TVL * 10000 / newSupply
246
+ targetCR = (tvl * 10000n) / newSupply;
247
+ // Cap at 140% max
248
+ if (targetCR > 14000n) targetCR = 14000n;
249
+ }
250
+ console.log(`[RiskKeeper] Adjusted target CR to ${targetCR} to respect ${this.SM2_MAX_POOL_PERCENT}% limit`);
251
+ }
252
+
253
+ // Execute SM2
254
+ const txHash = await this.executeSM2(targetCR);
255
+
256
+ // Wait for confirmation
257
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash });
258
+
259
+ if (receipt.status === "success") {
260
+ this.state.lastSM2Trigger = now;
261
+ await this.notifySM2Success(sm2Info.currentCR, sm2Info.potentialConversion, txHash);
262
+ } else {
263
+ await this.notifySM2Failure(txHash, "Transaction reverted");
264
+ }
265
+ } catch (error) {
266
+ console.error("[RiskKeeper] SM2 check/trigger failed:", error);
267
+ await this.notifySM2Failure(undefined, String(error));
268
+ } finally {
269
+ this.state.isProcessing = false;
270
+ }
271
+ }
272
+
273
+ private async executeSM2(targetCR: bigint): Promise<Hash> {
274
+ console.log(`[RiskKeeper] Executing SM2 with target CR: ${targetCR}`);
275
+
276
+ return this.walletClient.writeContract({
277
+ chain: bsc,
278
+ account: this.account, address: this.keeperConfig.diamondAddress,
279
+ abi: DiamondABI,
280
+ functionName: "triggerStabilityMode2",
281
+ args: [targetCR],
282
+ });
283
+ }
284
+
285
+ // ============ TWAP Vault Cleaning ============
286
+
287
+ private async checkAndExecuteTWAP(): Promise<void> {
288
+ if (this.state.isProcessing) return;
289
+
290
+ try {
291
+ // Check if vault is dirty (has xBNB)
292
+ const vaultState = await this.getVaultState();
293
+
294
+ if (!vaultState.hasXBNB || vaultState.xBNBAmount === 0n) {
295
+ return;
296
+ }
297
+
298
+ // Check CR is above threshold (safe to sell xBNB)
299
+ const sm = await withRetry(
300
+ () => this.client.getStabilityMode(),
301
+ this.config,
302
+ "getStabilityMode"
303
+ );
304
+
305
+ if (sm.currentCR < this.TWAP_START_CR) {
306
+ console.log(`[RiskKeeper] CR ${sm.currentCR} below TWAP threshold ${this.TWAP_START_CR}, skipping`);
307
+ return;
308
+ }
309
+
310
+ // Check BNB price volatility
311
+ const isVolatile = await this.checkVolatility();
312
+ if (isVolatile) {
313
+ console.log(`[RiskKeeper] BNB price too volatile (>${this.MAX_VOLATILITY_PERCENT}%), skipping TWAP`);
314
+ return;
315
+ }
316
+
317
+ console.log(`[RiskKeeper] Vault dirty with ${vaultState.xBNBAmount} xBNB, executing TWAP clean`);
318
+
319
+ this.state.isProcessing = true;
320
+
321
+ // Calculate amount to clean (X% of total)
322
+ const cleanAmount = (vaultState.xBNBAmount * BigInt(this.TWAP_PERCENTAGE)) / 100n;
323
+
324
+ if (cleanAmount === 0n) {
325
+ console.log("[RiskKeeper] Clean amount too small, skipping");
326
+ return;
327
+ }
328
+
329
+ // Execute TWAP clean
330
+ const txHash = await this.executeVaultClean(cleanAmount);
331
+
332
+ // Wait for confirmation
333
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash });
334
+
335
+ if (receipt.status === "success") {
336
+ this.state.lastTWAPExecution = Date.now();
337
+ await this.notifyTWAPSuccess(cleanAmount, txHash);
338
+ } else {
339
+ await this.notifyTWAPFailure(txHash, "Transaction reverted");
340
+ }
341
+ } catch (error) {
342
+ console.error("[RiskKeeper] TWAP check/execute failed:", error);
343
+ await this.notifyTWAPFailure(undefined, String(error));
344
+ } finally {
345
+ this.state.isProcessing = false;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Check if BNB price is too volatile for TWAP execution
351
+ * Returns true if volatility exceeds threshold
352
+ */
353
+ private async checkVolatility(): Promise<boolean> {
354
+ try {
355
+ const currentPrice = await withRetry(
356
+ () => this.client.getBNBPriceUSD(),
357
+ this.config,
358
+ "getBNBPriceUSD"
359
+ );
360
+
361
+ const now = Date.now();
362
+
363
+ // First run - just record price
364
+ if (this.lastBNBPrice === 0n) {
365
+ this.lastBNBPrice = currentPrice;
366
+ this.lastPriceTime = now;
367
+ return false;
368
+ }
369
+
370
+ // Calculate price change percentage
371
+ const priceDiff = currentPrice > this.lastBNBPrice
372
+ ? currentPrice - this.lastBNBPrice
373
+ : this.lastBNBPrice - currentPrice;
374
+
375
+ const changePercent = Number((priceDiff * 100n) / this.lastBNBPrice);
376
+
377
+ // Update tracked price
378
+ this.lastBNBPrice = currentPrice;
379
+ this.lastPriceTime = now;
380
+
381
+ if (changePercent > this.MAX_VOLATILITY_PERCENT) {
382
+ console.log(`[RiskKeeper] Volatility: ${changePercent.toFixed(2)}% > ${this.MAX_VOLATILITY_PERCENT}%`);
383
+ return true;
384
+ }
385
+
386
+ return false;
387
+ } catch (error) {
388
+ console.error("[RiskKeeper] Volatility check failed:", error);
389
+ // If we can't check volatility, be conservative and skip
390
+ return true;
391
+ }
392
+ }
393
+
394
+ private async getVaultState(): Promise<{ hasXBNB: boolean; xBNBAmount: bigint }> {
395
+ const result = await this.publicClient.readContract({
396
+ address: this.keeperConfig.sApUSDAddress,
397
+ abi: SApUSDABI,
398
+ functionName: "hasStabilityConversion",
399
+ });
400
+
401
+ return {
402
+ hasXBNB: result[0],
403
+ xBNBAmount: result[1],
404
+ };
405
+ }
406
+
407
+ private async executeVaultClean(xBNBAmount: bigint): Promise<Hash> {
408
+ console.log(`[RiskKeeper] Cleaning ${xBNBAmount} xBNB from vault`);
409
+
410
+ // Build swap path: xBNB -> WBNB -> apUSD
411
+ // Note: In production, might need xBNB -> WBNB -> USDT -> apUSD
412
+ const path = [
413
+ await this.getXBNBAddress(),
414
+ this.keeperConfig.wbnbAddress,
415
+ await this.getApUSDAddress(),
416
+ ];
417
+
418
+ // Preview output
419
+ const expectedOut = await this.previewClean(xBNBAmount, path);
420
+
421
+ // Calculate min out with slippage
422
+ const minOut = (expectedOut * BigInt(10000 - this.MAX_SLIPPAGE_BPS)) / 10000n;
423
+
424
+ // Deadline: 5 minutes from now
425
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
426
+
427
+ return this.walletClient.writeContract({
428
+ chain: bsc,
429
+ account: this.account, address: this.keeperConfig.sApUSDAddress,
430
+ abi: SApUSDABI,
431
+ functionName: "cleanXBNB",
432
+ args: [xBNBAmount, minOut, this.keeperConfig.routerAddress, path, deadline],
433
+ });
434
+ }
435
+
436
+ private async previewClean(xBNBAmount: bigint, path: Address[]): Promise<bigint> {
437
+ return this.publicClient.readContract({
438
+ address: this.keeperConfig.sApUSDAddress,
439
+ abi: SApUSDABI,
440
+ functionName: "previewCleanXBNB",
441
+ args: [xBNBAmount, this.keeperConfig.routerAddress, path],
442
+ });
443
+ }
444
+
445
+ private async getXBNBAddress(): Promise<Address> {
446
+ const tokens = await this.client.getTokens();
447
+ return tokens.xBNB;
448
+ }
449
+
450
+ private async getApUSDAddress(): Promise<Address> {
451
+ const tokens = await this.client.getTokens();
452
+ return tokens.apUSD;
453
+ }
454
+
455
+ // ============ Notifications ============
456
+
457
+ private async notifySM2Success(cr: bigint, amount: bigint, txHash: Hash): Promise<void> {
458
+ console.log(`[RiskKeeper] SM2 executed successfully: ${txHash}`);
459
+
460
+ await this.slack.send({
461
+ attachments: [{
462
+ color: SlackColors.warning,
463
+ title: ":rotating_light: Stability Mode 2 Triggered",
464
+ text: "Forced conversion executed to restore CR",
465
+ fields: [
466
+ { title: "CR Before", value: `${Number(cr) / 100}%`, short: true },
467
+ { title: "Amount Converted", value: `${Number(amount) / 1e18} apUSD`, short: true },
468
+ { title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true },
469
+ ],
470
+ ts: Math.floor(Date.now() / 1000),
471
+ }],
472
+ });
473
+ }
474
+
475
+ private async notifySM2Failure(txHash: Hash | undefined, error: string): Promise<void> {
476
+ console.error(`[RiskKeeper] SM2 execution failed: ${error}`);
477
+
478
+ await this.slack.send({
479
+ attachments: [{
480
+ color: SlackColors.danger,
481
+ title: ":x: SM2 Execution Failed",
482
+ text: error,
483
+ fields: txHash ? [{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true }] : [],
484
+ ts: Math.floor(Date.now() / 1000),
485
+ }],
486
+ });
487
+ }
488
+
489
+ private async notifyTWAPSuccess(amount: bigint, txHash: Hash): Promise<void> {
490
+ console.log(`[RiskKeeper] TWAP clean executed successfully: ${txHash}`);
491
+
492
+ await this.slack.send({
493
+ attachments: [{
494
+ color: SlackColors.success,
495
+ title: ":broom: Vault TWAP Clean Executed",
496
+ text: "xBNB cleaned from stability pool vault",
497
+ fields: [
498
+ { title: "xBNB Sold", value: `${Number(amount) / 1e18}`, short: true },
499
+ { title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true },
500
+ ],
501
+ ts: Math.floor(Date.now() / 1000),
502
+ }],
503
+ });
504
+ }
505
+
506
+ private async notifyTWAPFailure(txHash: Hash | undefined, error: string): Promise<void> {
507
+ console.error(`[RiskKeeper] TWAP execution failed: ${error}`);
508
+
509
+ await this.slack.send({
510
+ attachments: [{
511
+ color: SlackColors.danger,
512
+ title: ":x: TWAP Clean Failed",
513
+ text: error,
514
+ fields: txHash ? [{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true }] : [],
515
+ ts: Math.floor(Date.now() / 1000),
516
+ }],
517
+ });
518
+ }
519
+ }
package/src/client.ts CHANGED
@@ -798,6 +798,32 @@ export class AspanReadClient {
798
798
  });
799
799
  }
800
800
 
801
+ // ============ Bootstrap View Functions ============
802
+
803
+ /**
804
+ * Check if xBNB liquidity has been bootstrapped
805
+ * @returns true if bootstrap has been called
806
+ */
807
+ async isBootstrapped(): Promise<boolean> {
808
+ return this.publicClient.readContract({
809
+ address: this.diamondAddress,
810
+ abi: DiamondABI,
811
+ functionName: "isBootstrapped",
812
+ });
813
+ }
814
+
815
+ /**
816
+ * Get the amount of xBNB locked in bootstrap (burned to dead address)
817
+ * @returns xBNB amount in wei
818
+ */
819
+ async getBootstrapXBNBAmount(): Promise<bigint> {
820
+ return this.publicClient.readContract({
821
+ address: this.diamondAddress,
822
+ abi: DiamondABI,
823
+ functionName: "getBootstrapXBNBAmount",
824
+ });
825
+ }
826
+
801
827
  // ============ Stability Mode View Functions ============
802
828
 
803
829
  async getStabilityMode(): Promise<StabilityModeInfo> {
@@ -1031,6 +1057,89 @@ export class AspanClient extends AspanReadClient {
1031
1057
  });
1032
1058
  }
1033
1059
 
1060
+ // ============ Risk Management Functions ============
1061
+
1062
+ /**
1063
+ * Trigger Stability Mode 2 forced conversion
1064
+ * @description Anyone can call when CR < 130%. Burns apUSD from stability pool
1065
+ * and mints xBNB to compensate stakers.
1066
+ * @param targetCR Target CR to restore to (in BPS, e.g., 14000 = 140%)
1067
+ * @returns Transaction hash
1068
+ */
1069
+ async triggerStabilityMode2(targetCR: bigint = 14000n): Promise<Hash> {
1070
+ return this.walletClient.writeContract({
1071
+ chain: this.chain,
1072
+ account: this.walletClient.account!,
1073
+ address: this.diamondAddress,
1074
+ abi: DiamondABI,
1075
+ functionName: "triggerStabilityMode2",
1076
+ args: [targetCR],
1077
+ });
1078
+ }
1079
+
1080
+ /**
1081
+ * Clean underwater xBNB (burn without getting LST back)
1082
+ * @description Only works when xBNB is underwater (price = 0)
1083
+ * @param xBNBAmount Amount of xBNB to clean (burn)
1084
+ * @returns Transaction hash
1085
+ */
1086
+ async cleanXbnb(xBNBAmount: bigint): Promise<Hash> {
1087
+ return this.walletClient.writeContract({
1088
+ chain: this.chain,
1089
+ account: this.walletClient.account!,
1090
+ address: this.diamondAddress,
1091
+ abi: DiamondABI,
1092
+ functionName: "cleanXbnb",
1093
+ args: [xBNBAmount],
1094
+ });
1095
+ }
1096
+
1097
+ // ============ Fee Management Functions ============
1098
+
1099
+ /**
1100
+ * Set fee tiers (requires feeManager or owner role)
1101
+ * @param tiers Array of fee tier configurations
1102
+ * @returns Transaction hash
1103
+ */
1104
+ async setFeeTiers(tiers: Array<{
1105
+ minCR: bigint;
1106
+ apUSDMintFee: number;
1107
+ apUSDRedeemFee: number;
1108
+ xBNBMintFee: number;
1109
+ xBNBRedeemFee: number;
1110
+ apUSDMintDisabled: boolean;
1111
+ }>): Promise<Hash> {
1112
+ return this.walletClient.writeContract({
1113
+ chain: this.chain,
1114
+ account: this.walletClient.account!,
1115
+ address: this.diamondAddress,
1116
+ abi: DiamondABI,
1117
+ functionName: "setFeeTiers",
1118
+ args: [tiers],
1119
+ });
1120
+ }
1121
+
1122
+ // ============ Admin Functions ============
1123
+
1124
+ /**
1125
+ * Bootstrap xBNB liquidity (owner only)
1126
+ * @description Initializes xBNB supply by minting to dead address, preventing extreme initial prices.
1127
+ * Similar to Uniswap V2's MINIMUM_LIQUIDITY. Can only be called once.
1128
+ * @param lstToken LST token address to deposit
1129
+ * @param lstAmount Amount of LST to deposit for bootstrap
1130
+ * @returns Transaction hash
1131
+ */
1132
+ async bootstrap(lstToken: Address, lstAmount: bigint): Promise<Hash> {
1133
+ return this.walletClient.writeContract({
1134
+ chain: this.chain,
1135
+ account: this.walletClient.account!,
1136
+ address: this.diamondAddress,
1137
+ abi: DiamondABI,
1138
+ functionName: "bootstrap",
1139
+ args: [lstToken, lstAmount],
1140
+ });
1141
+ }
1142
+
1034
1143
  // ============ Transaction Helpers ============
1035
1144
 
1036
1145
  /**
package/src/index.ts CHANGED
@@ -66,18 +66,18 @@ export type {
66
66
  // Transaction result
67
67
  TransactionResult,
68
68
  TransactionReceipt,
69
- // Router types
69
+ // Router types (v2.0.0 consolidated API)
70
70
  RouterSwapParams,
71
71
  RouterMintParams,
72
72
  SwapAndMintParams,
73
73
  StakeAndMintParams,
74
74
  SwapAndMintDefaultParams,
75
- RouterMintApUSDParams,
76
- RouterMintXBNBParams,
77
- RouterRedeemApUSDParams,
78
- RouterRedeemXBNBParams,
75
+ RouterMintParams2,
76
+ RouterRedeemParams,
79
77
  RouterRedeemAndSwapParams,
78
+ RouterRedeemAndUnstakeParams,
80
79
  WithdrawalRequestInfo,
80
+ ExpectedOutput,
81
81
  // Router events
82
82
  SwapAndMintEvent,
83
83
  StakeAndMintEvent,
@@ -100,6 +100,23 @@ export const PRECISION = 10n ** 18n;
100
100
  export const BPS_PRECISION = 10000n;
101
101
  export const PRICE_PRECISION = 10n ** 8n; // Chainlink price precision
102
102
 
103
+ // ============ Contract Addresses (BSC Mainnet) ============
104
+ export const BSC_ADDRESSES = {
105
+ diamond: "0x6a11B30d3a70727d5477D6d8090e144443fA1c78" as const,
106
+ router: "0x813d3D1A3154950E2f1d8718305426a335A974A9" as const,
107
+ apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287" as const,
108
+ xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43" as const,
109
+ sApUSD: "0x73407A291c007a47CC926EcD5CaC256A1E2d00cF" as const,
110
+ // LSTs
111
+ slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as const,
112
+ asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6" as const,
113
+ wclisBNB: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108" as const,
114
+ // Stablecoins
115
+ USDT: "0x55d398326f99059fF775485246999027B3197955" as const,
116
+ USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" as const,
117
+ WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as const,
118
+ } as const;
119
+
103
120
  // ============ Utility Functions ============
104
121
 
105
122
  /**