@aspan/sdk 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ pnpm add @aspan/sdk
16
16
  | Contract | Address |
17
17
  |----------|---------|
18
18
  | **Diamond (Main Entry)** | `0x10d25Ae0690533e0BA9E64EC7ae77dbD4fE8A46f` |
19
- | **Router** | `0xf63f34f7e9608ae7d3a6f5b06ce423d9f9043648` |
19
+ | **Router** | `0x159B2990966B0E4f07cD58c9Def513EA1fF81c0C` |
20
20
  | **wclisBNB** | `0x439faaC2229559121C4Ad4fd8B3FE13Dff038046` |
21
21
  | **ApUSD** | `0x1977097E2E5697A6DD91b6732F368a14F50f6B3d` |
22
22
  | **XBNB** | `0xB78eB4d5928bAb158Eb23c3154544084cD2661d5` |
@@ -98,7 +98,7 @@ import { createRouterClient, AspanRouterClient } from "@aspan/sdk";
98
98
  import { privateKeyToAccount } from "viem/accounts";
99
99
  import { zeroAddress } from "viem";
100
100
 
101
- const ROUTER = "0xf63f34f7e9608ae7d3a6f5b06ce423d9f9043648";
101
+ const ROUTER = "0x159B2990966B0E4f07cD58c9Def513EA1fF81c0C";
102
102
 
103
103
  // Node.js
104
104
  const account = privateKeyToAccount("0x...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspan/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
@@ -22,6 +22,8 @@
22
22
  "lint": "eslint src --ext .ts",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "test": "vitest run",
25
+ "test:fork": "ANVIL_RPC=http://127.0.0.1:8545 vitest run fork.test.ts",
26
+ "test:e2e": "vitest run router.test.ts",
25
27
  "test:watch": "vitest",
26
28
  "clean": "rm -rf dist",
27
29
  "prepublishOnly": "npm run build",
@@ -0,0 +1,206 @@
1
+ /**
2
+ * AspanRouter SDK Fork Tests
3
+ *
4
+ * Uses Anvil fork for time manipulation and state control.
5
+ * Run with: npm run test:fork
6
+ *
7
+ * These tests require a local Anvil fork:
8
+ * anvil --fork-url https://bsc-dataseed.binance.org/ --fork-block-number <recent>
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
12
+ import { privateKeyToAccount } from "viem/accounts";
13
+ import { createPublicClient, createTestClient, createWalletClient, http, parseEther, type Address } from "viem";
14
+ import { bsc } from "viem/chains";
15
+ import {
16
+ AspanRouterClient,
17
+ AspanRouterReadClient,
18
+ formatAmount,
19
+ } from "../index";
20
+
21
+ // ============ Configuration ============
22
+
23
+ const ROUTER = "0x159B2990966B0E4f07cD58c9Def513EA1fF81c0C" as Address;
24
+ const ANVIL_RPC = process.env.ANVIL_RPC || "http://127.0.0.1:8545";
25
+
26
+ const TOKENS = {
27
+ WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as Address,
28
+ USDT: "0x55d398326f99059fF775485246999027B3197955" as Address,
29
+ slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as Address,
30
+ apUSD: "0x1977097E2E5697A6DD91b6732F368a14F50f6B3d" as Address,
31
+ xBNB: "0xB78eB4d5928bAb158Eb23c3154544084cD2661d5" as Address,
32
+ };
33
+
34
+ const ERC20_ABI = [
35
+ { name: "balanceOf", type: "function", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
36
+ { name: "approve", type: "function", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }], stateMutability: "nonpayable" },
37
+ ] as const;
38
+
39
+ // Use Anvil's default test account
40
+ const TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
41
+
42
+ // Check if Anvil is available
43
+ const isAnvilAvailable = async (): Promise<boolean> => {
44
+ try {
45
+ const client = createPublicClient({ transport: http(ANVIL_RPC) });
46
+ await client.getChainId();
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ };
52
+
53
+ // ============ Tests ============
54
+
55
+ describe.skipIf(!(await isAnvilAvailable()))("Fork Tests (Anvil)", () => {
56
+ let readClient: AspanRouterReadClient;
57
+ let writeClient: AspanRouterClient;
58
+ let publicClient: ReturnType<typeof createPublicClient>;
59
+ let testClient: ReturnType<typeof createTestClient>;
60
+ let account: ReturnType<typeof privateKeyToAccount>;
61
+
62
+ const getBalance = async (token: Address): Promise<bigint> => {
63
+ return publicClient.readContract({
64
+ address: token,
65
+ abi: ERC20_ABI,
66
+ functionName: "balanceOf",
67
+ args: [account.address],
68
+ }) as Promise<bigint>;
69
+ };
70
+
71
+ const approve = async (token: Address, amount: bigint) => {
72
+ const hash = await writeClient.walletClient.writeContract({
73
+ address: token,
74
+ abi: ERC20_ABI,
75
+ functionName: "approve",
76
+ args: [ROUTER, amount],
77
+ });
78
+ await publicClient.waitForTransactionReceipt({ hash });
79
+ };
80
+
81
+ beforeAll(async () => {
82
+ account = privateKeyToAccount(TEST_PRIVATE_KEY);
83
+
84
+ publicClient = createPublicClient({
85
+ chain: bsc,
86
+ transport: http(ANVIL_RPC),
87
+ });
88
+
89
+ testClient = createTestClient({
90
+ chain: bsc,
91
+ transport: http(ANVIL_RPC),
92
+ mode: "anvil",
93
+ });
94
+
95
+ readClient = new AspanRouterReadClient({
96
+ routerAddress: ROUTER,
97
+ chain: bsc,
98
+ rpcUrl: ANVIL_RPC,
99
+ });
100
+
101
+ writeClient = new AspanRouterClient({
102
+ routerAddress: ROUTER,
103
+ account,
104
+ chain: bsc,
105
+ rpcUrl: ANVIL_RPC,
106
+ });
107
+
108
+ // Fund test account with BNB
109
+ await testClient.setBalance({
110
+ address: account.address,
111
+ value: parseEther("10"),
112
+ });
113
+
114
+ console.log(`📍 Fork Test Wallet: ${account.address}`);
115
+ });
116
+
117
+ describe("claimUnstake (with time manipulation)", () => {
118
+ it("should claim unstake after waiting period", async () => {
119
+ console.log(`\n[Fork 1] claimUnstake after time manipulation`);
120
+
121
+ // Step 1: Create some apUSD via stake
122
+ console.log(` Staking BNB → apUSD...`);
123
+ const mintHash = await writeClient.stakeAndMintApUSD(0n, parseEther("0.01"));
124
+ await publicClient.waitForTransactionReceipt({ hash: mintHash });
125
+
126
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
127
+ console.log(` Minted: ${formatAmount(apUSDBalance)} apUSD`);
128
+ expect(apUSDBalance).toBeGreaterThan(0n);
129
+
130
+ // Step 2: Request unstake
131
+ console.log(` Requesting Lista unstake...`);
132
+ await approve(TOKENS.apUSD, apUSDBalance);
133
+
134
+ const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
135
+ const unstakeHash = await writeClient.redeemApUSDAndRequestUnstake(apUSDBalance);
136
+ await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
137
+
138
+ const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
139
+ expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
140
+
141
+ const newIndex = indicesAfter[indicesAfter.length - 1];
142
+ console.log(` Unstake requested, index: ${newIndex}`);
143
+
144
+ // Step 3: Check withdrawal status (should not be claimable yet)
145
+ const statusBefore = await readClient.getWithdrawalStatus(newIndex);
146
+ console.log(` Status before: claimable=${statusBefore.isClaimable}`);
147
+ expect(statusBefore.isClaimable).toBe(false);
148
+
149
+ // Step 4: Fast forward time by 8 days (Lista unbonding period is ~7 days)
150
+ console.log(` Fast forwarding 8 days...`);
151
+ const EIGHT_DAYS = 8 * 24 * 60 * 60;
152
+ await testClient.increaseTime({ seconds: EIGHT_DAYS });
153
+ await testClient.mine({ blocks: 1 });
154
+
155
+ // Step 5: Check withdrawal status (should be claimable now)
156
+ const statusAfter = await readClient.getWithdrawalStatus(newIndex);
157
+ console.log(` Status after: claimable=${statusAfter.isClaimable}`);
158
+
159
+ if (!statusAfter.isClaimable) {
160
+ console.log(` ⚠️ Still not claimable, Lista might have longer unbonding. Skipping claim.`);
161
+ return;
162
+ }
163
+
164
+ // Step 6: Claim unstake
165
+ console.log(` Claiming unstake...`);
166
+ const bnbBefore = await publicClient.getBalance({ address: account.address });
167
+
168
+ const claimHash = await writeClient.claimUnstake(newIndex);
169
+ await publicClient.waitForTransactionReceipt({ hash: claimHash });
170
+
171
+ const bnbAfter = await publicClient.getBalance({ address: account.address });
172
+ const bnbReceived = bnbAfter - bnbBefore;
173
+ console.log(` Received: ${formatAmount(bnbReceived)} BNB ✓`);
174
+
175
+ // Note: bnbReceived might be negative due to gas, but the claim should succeed
176
+ expect(statusAfter.isClaimable).toBe(true);
177
+ }, 300000);
178
+ });
179
+
180
+ describe("Edge Cases", () => {
181
+ it("should handle minimum amounts", async () => {
182
+ console.log(`\n[Fork 2] Minimum amount handling`);
183
+
184
+ // Try very small stake
185
+ const minAmount = parseEther("0.0001");
186
+ console.log(` Staking ${formatAmount(minAmount)} BNB...`);
187
+
188
+ try {
189
+ const mintHash = await writeClient.stakeAndMintApUSD(0n, minAmount);
190
+ await publicClient.waitForTransactionReceipt({ hash: mintHash });
191
+ console.log(` Success ✓`);
192
+ } catch (e: any) {
193
+ console.log(` Failed (expected for very small amounts): ${e.message?.slice(0, 50)}`);
194
+ }
195
+ }, 60000);
196
+
197
+ it("should revert on zero amounts", async () => {
198
+ console.log(`\n[Fork 3] Zero amount revert`);
199
+
200
+ await expect(
201
+ writeClient.stakeAndMintApUSD(0n, 0n)
202
+ ).rejects.toThrow();
203
+ console.log(` Reverted as expected ✓`);
204
+ }, 30000);
205
+ });
206
+ });
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { describe, it, expect, beforeAll } from "vitest";
12
12
  import { privateKeyToAccount } from "viem/accounts";
13
- import { createPublicClient, http, parseEther, zeroAddress, type Address } from "viem";
13
+ import { createPublicClient, http, parseEther, formatEther, zeroAddress, type Address } from "viem";
14
14
  import { bsc } from "viem/chains";
15
15
  import {
16
16
  AspanRouterClient,
@@ -22,7 +22,7 @@ import {
22
22
 
23
23
  // ============ Configuration ============
24
24
 
25
- const ROUTER = "0xf63f34f7e9608ae7d3a6f5b06ce423d9f9043648" as Address;
25
+ const ROUTER = "0x159B2990966B0E4f07cD58c9Def513EA1fF81c0C" as Address;
26
26
 
27
27
  const TOKENS = {
28
28
  WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as Address,
@@ -390,5 +390,378 @@ describe("AspanRouter SDK", () => {
390
390
  console.log(` Received: ${formatAmount(slisBNBAfter - slisBNBBefore)} slisBNB ✓`);
391
391
  expect(slisBNBAfter).toBeGreaterThan(slisBNBBefore);
392
392
  }, 180000);
393
+
394
+ // Test 5: swapAndMintApUSDDefault (USDT → apUSD with default LST)
395
+ it.skipIf(!HAS_PRIVATE_KEY)("swapAndMintApUSDDefault: USDT → apUSD", async () => {
396
+ console.log(`\n[E2E 5] swapAndMintApUSDDefault: USDT → apUSD`);
397
+
398
+ const usdtBalance = await getBalance(TOKENS.USDT);
399
+ if (usdtBalance < USDT_AMOUNT) {
400
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBalance)}), skipping`);
401
+ return;
402
+ }
403
+
404
+ console.log(` Swapping ${formatAmount(USDT_AMOUNT)} USDT → apUSD (default LST)...`);
405
+ await approve(TOKENS.USDT, USDT_AMOUNT);
406
+
407
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
408
+
409
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
410
+ const mintHash = await writeClient.swapAndMintApUSDDefault({
411
+ inputToken: TOKENS.USDT,
412
+ inputAmount: USDT_AMOUNT,
413
+ minMintOut: 0n,
414
+ deadline,
415
+ });
416
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
417
+ await sleep(2000);
418
+ expect(mintReceipt.status).toBe("success");
419
+
420
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
421
+ const apUSDMinted = apUSDAfter - apUSDBefore;
422
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD ✓`);
423
+ expect(apUSDMinted).toBeGreaterThan(0n);
424
+ }, 180000);
425
+
426
+ // Test 6: swapAndMintXBNBDefault (USDT → xBNB with default LST)
427
+ it.skipIf(!HAS_PRIVATE_KEY)("swapAndMintXBNBDefault: USDT → xBNB", async () => {
428
+ console.log(`\n[E2E 6] swapAndMintXBNBDefault: USDT → xBNB`);
429
+
430
+ const usdtBalance = await getBalance(TOKENS.USDT);
431
+ if (usdtBalance < USDT_AMOUNT) {
432
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBalance)}), skipping`);
433
+ return;
434
+ }
435
+
436
+ console.log(` Swapping ${formatAmount(USDT_AMOUNT)} USDT → xBNB (default LST)...`);
437
+ await approve(TOKENS.USDT, USDT_AMOUNT);
438
+
439
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
440
+
441
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
442
+ const mintHash = await writeClient.swapAndMintXBNBDefault({
443
+ inputToken: TOKENS.USDT,
444
+ inputAmount: USDT_AMOUNT,
445
+ minMintOut: 0n,
446
+ deadline,
447
+ });
448
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
449
+ await sleep(2000);
450
+ expect(mintReceipt.status).toBe("success");
451
+
452
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
453
+ const xBNBMinted = xBNBAfter - xBNBBefore;
454
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB ✓`);
455
+ expect(xBNBMinted).toBeGreaterThan(0n);
456
+ }, 180000);
457
+
458
+ // Test 7: redeemXBNBAndSwap (xBNB → USDT via V3)
459
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemXBNBAndSwap: xBNB → USDT", async () => {
460
+ console.log(`\n[E2E 7] redeemXBNBAndSwap: xBNB → USDT`);
461
+
462
+ const xBNBBalance = await getBalance(TOKENS.xBNB);
463
+ if (xBNBBalance === 0n) {
464
+ console.log(` ⚠️ No xBNB balance, skipping`);
465
+ return;
466
+ }
467
+
468
+ const redeemAmount = xBNBBalance > parseEther("0.0001") ? parseEther("0.0001") : xBNBBalance;
469
+ console.log(` Redeeming ${formatAmount(redeemAmount, 8)} xBNB → USDT...`);
470
+ await approve(TOKENS.xBNB, redeemAmount);
471
+
472
+ const usdtBefore = await getBalance(TOKENS.USDT);
473
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
474
+
475
+ const redeemHash = await writeClient.redeemXBNBAndSwap({
476
+ lst: TOKENS.slisBNB,
477
+ amount: redeemAmount,
478
+ path,
479
+ minOut: 0n,
480
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
481
+ });
482
+ const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
483
+ await sleep(2000);
484
+ expect(redeemReceipt.status).toBe("success");
485
+
486
+ const usdtAfter = await getBalance(TOKENS.USDT);
487
+ console.log(` Received: ${formatAmount(usdtAfter - usdtBefore)} USDT ✓`);
488
+ expect(usdtAfter).toBeGreaterThan(usdtBefore);
489
+ }, 180000);
490
+
491
+ // Test 8: redeemXBNBAndRequestUnstake (xBNB → Lista unstake)
492
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemXBNBAndRequestUnstake: xBNB → Lista unstake", async () => {
493
+ console.log(`\n[E2E 8] redeemXBNBAndRequestUnstake: xBNB → Lista unstake`);
494
+
495
+ const xBNBBalance = await getBalance(TOKENS.xBNB);
496
+ if (xBNBBalance === 0n) {
497
+ console.log(` ⚠️ No xBNB balance, skipping`);
498
+ return;
499
+ }
500
+
501
+ console.log(` Requesting unstake for ${formatAmount(xBNBBalance, 8)} xBNB...`);
502
+ await approve(TOKENS.xBNB, xBNBBalance);
503
+
504
+ const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
505
+
506
+ const unstakeHash = await writeClient.redeemXBNBAndRequestUnstake(xBNBBalance);
507
+ const unstakeReceipt = await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
508
+ await sleep(2000);
509
+ expect(unstakeReceipt.status).toBe("success");
510
+
511
+ const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
512
+ expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
513
+ console.log(` Unstake requested ✓ (new indices: ${indicesAfter.length - indicesBefore.length})`);
514
+ }, 180000);
515
+
516
+ // Note: claimUnstake requires 7+ days waiting period, cannot be tested in e2e
517
+ // Would need a fork test with time manipulation
518
+
519
+ // Test 9: stakeAndMintXBNB (BNB → slisBNB → xBNB, simplified)
520
+ it.skipIf(!HAS_PRIVATE_KEY)("stakeAndMintXBNB: BNB → xBNB", async () => {
521
+ console.log(`\n[E2E 9] stakeAndMintXBNB: BNB → xBNB`);
522
+
523
+ const amount = parseEther("0.001");
524
+ console.log(` Staking ${formatEther(amount)} BNB → xBNB...`);
525
+
526
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
527
+
528
+ const hash = await writeClient.stakeAndMintXBNB(0n, amount);
529
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
530
+ await sleep(2000);
531
+ expect(receipt.status).toBe("success");
532
+
533
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
534
+ const minted = xBNBAfter - xBNBBefore;
535
+ console.log(` Minted: ${formatAmount(minted, 8)} xBNB ✓`);
536
+ expect(minted).toBeGreaterThan(0n);
537
+ }, 180000);
538
+
539
+ // Test 10: mintXBNB (direct LST → xBNB)
540
+ it.skipIf(!HAS_PRIVATE_KEY)("mintXBNB: slisBNB → xBNB", async () => {
541
+ console.log(`\n[E2E 10] mintXBNB: slisBNB → xBNB`);
542
+
543
+ const slisBNBBalance = await getBalance(TOKENS.slisBNB);
544
+ if (slisBNBBalance === 0n) {
545
+ console.log(` ⚠️ No slisBNB balance, skipping`);
546
+ return;
547
+ }
548
+
549
+ const amount = slisBNBBalance > parseEther("0.001") ? parseEther("0.001") : slisBNBBalance;
550
+ console.log(` Minting xBNB from ${formatEther(amount)} slisBNB...`);
551
+ await approve(TOKENS.slisBNB, amount);
552
+
553
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
554
+
555
+ const hash = await writeClient.mintXBNB({
556
+ lst: TOKENS.slisBNB,
557
+ lstAmount: amount,
558
+ minOut: 0n,
559
+ });
560
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
561
+ await sleep(2000);
562
+ expect(receipt.status).toBe("success");
563
+
564
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
565
+ const minted = xBNBAfter - xBNBBefore;
566
+ console.log(` Minted: ${formatAmount(minted, 8)} xBNB ✓`);
567
+ expect(minted).toBeGreaterThan(0n);
568
+ }, 180000);
569
+
570
+ // Test 11: redeemXBNB (direct xBNB → slisBNB)
571
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemXBNB: xBNB → slisBNB", async () => {
572
+ console.log(`\n[E2E 11] redeemXBNB: xBNB → slisBNB`);
573
+
574
+ const xBNBBalance = await getBalance(TOKENS.xBNB);
575
+ if (xBNBBalance === 0n) {
576
+ console.log(` ⚠️ No xBNB balance, skipping`);
577
+ return;
578
+ }
579
+
580
+ const amount = xBNBBalance > parseEther("0.0001") ? parseEther("0.0001") : xBNBBalance;
581
+ console.log(` Redeeming ${formatAmount(amount, 8)} xBNB → slisBNB...`);
582
+ await approve(TOKENS.xBNB, amount);
583
+
584
+ const slisBNBBefore = await getBalance(TOKENS.slisBNB);
585
+
586
+ const hash = await writeClient.redeemXBNB({
587
+ lst: TOKENS.slisBNB,
588
+ xBNBAmount: amount,
589
+ minOut: 0n,
590
+ });
591
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
592
+ await sleep(2000);
593
+ expect(receipt.status).toBe("success");
594
+
595
+ const slisBNBAfter = await getBalance(TOKENS.slisBNB);
596
+ const received = slisBNBAfter - slisBNBBefore;
597
+ console.log(` Received: ${formatEther(received)} slisBNB ✓`);
598
+ expect(received).toBeGreaterThan(0n);
599
+ }, 180000);
600
+
601
+ // Test 12: stakeAndMintApUSD (BNB → slisBNB → apUSD, simplified)
602
+ // Note: May fail if apUSD minting is disabled on mainnet
603
+ it.skipIf(!HAS_PRIVATE_KEY)("stakeAndMintApUSD: BNB → apUSD (may skip if disabled)", async () => {
604
+ console.log(`\n[E2E 12] stakeAndMintApUSD: BNB → apUSD`);
605
+
606
+ const amount = parseEther("0.001");
607
+ console.log(` Staking ${formatEther(amount)} BNB → apUSD...`);
608
+
609
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
610
+
611
+ try {
612
+ const hash = await writeClient.stakeAndMintApUSD(0n, amount);
613
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
614
+ await sleep(2000);
615
+
616
+ if (receipt.status === "success") {
617
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
618
+ const minted = apUSDAfter - apUSDBefore;
619
+ console.log(` Minted: ${formatEther(minted)} apUSD ✓`);
620
+ expect(minted).toBeGreaterThan(0n);
621
+ } else {
622
+ console.log(` ⚠️ Transaction reverted (apUSD minting may be disabled)`);
623
+ }
624
+ } catch (e: any) {
625
+ if (e.message?.includes("MintingDisabled") || e.message?.includes("revert")) {
626
+ console.log(` ⚠️ apUSD minting is disabled, skipping`);
627
+ } else {
628
+ throw e;
629
+ }
630
+ }
631
+ }, 180000);
632
+
633
+ // Test 13: mintApUSD (direct LST → apUSD)
634
+ // Note: May fail if apUSD minting is disabled on mainnet
635
+ it.skipIf(!HAS_PRIVATE_KEY)("mintApUSD: slisBNB → apUSD (may skip if disabled)", async () => {
636
+ console.log(`\n[E2E 13] mintApUSD: slisBNB → apUSD`);
637
+
638
+ const slisBNBBalance = await getBalance(TOKENS.slisBNB);
639
+ if (slisBNBBalance === 0n) {
640
+ console.log(` ⚠️ No slisBNB balance, skipping`);
641
+ return;
642
+ }
643
+
644
+ const amount = slisBNBBalance > parseEther("0.001") ? parseEther("0.001") : slisBNBBalance;
645
+ console.log(` Minting apUSD from ${formatEther(amount)} slisBNB...`);
646
+ await approve(TOKENS.slisBNB, amount);
647
+
648
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
649
+
650
+ try {
651
+ const hash = await writeClient.mintApUSD({
652
+ lst: TOKENS.slisBNB,
653
+ lstAmount: amount,
654
+ minOut: 0n,
655
+ });
656
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
657
+ await sleep(2000);
658
+
659
+ if (receipt.status === "success") {
660
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
661
+ const minted = apUSDAfter - apUSDBefore;
662
+ console.log(` Minted: ${formatEther(minted)} apUSD ✓`);
663
+ expect(minted).toBeGreaterThan(0n);
664
+ } else {
665
+ console.log(` ⚠️ Transaction reverted (apUSD minting may be disabled)`);
666
+ }
667
+ } catch (e: any) {
668
+ if (e.message?.includes("MintingDisabled") || e.message?.includes("revert")) {
669
+ console.log(` ⚠️ apUSD minting is disabled, skipping`);
670
+ } else {
671
+ throw e;
672
+ }
673
+ }
674
+ }, 180000);
675
+
676
+ // Test 14: redeemApUSD (direct apUSD → slisBNB)
677
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemApUSD: apUSD → slisBNB", async () => {
678
+ console.log(`\n[E2E 14] redeemApUSD: apUSD → slisBNB`);
679
+
680
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
681
+ if (apUSDBalance === 0n) {
682
+ console.log(` ⚠️ No apUSD balance, skipping`);
683
+ return;
684
+ }
685
+
686
+ const amount = apUSDBalance > parseEther("0.1") ? parseEther("0.1") : apUSDBalance;
687
+ console.log(` Redeeming ${formatEther(amount)} apUSD → slisBNB...`);
688
+ await approve(TOKENS.apUSD, amount);
689
+
690
+ const slisBNBBefore = await getBalance(TOKENS.slisBNB);
691
+
692
+ const hash = await writeClient.redeemApUSD({
693
+ lst: TOKENS.slisBNB,
694
+ apUSDAmount: amount,
695
+ minOut: 0n,
696
+ });
697
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
698
+ await sleep(2000);
699
+ expect(receipt.status).toBe("success");
700
+
701
+ const slisBNBAfter = await getBalance(TOKENS.slisBNB);
702
+ const received = slisBNBAfter - slisBNBBefore;
703
+ console.log(` Received: ${formatEther(received)} slisBNB ✓`);
704
+ expect(received).toBeGreaterThan(0n);
705
+ }, 180000);
706
+
707
+ // Test 15: redeemApUSDAndSwap (apUSD → slisBNB → USDT)
708
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemApUSDAndSwap: apUSD → USDT", async () => {
709
+ console.log(`\n[E2E 15] redeemApUSDAndSwap: apUSD → USDT`);
710
+
711
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
712
+ if (apUSDBalance === 0n) {
713
+ console.log(` ⚠️ No apUSD balance, skipping`);
714
+ return;
715
+ }
716
+
717
+ const amount = apUSDBalance > parseEther("0.1") ? parseEther("0.1") : apUSDBalance;
718
+ console.log(` Redeeming ${formatEther(amount)} apUSD → USDT...`);
719
+ await approve(TOKENS.apUSD, amount);
720
+
721
+ const usdtBefore = await getBalance(TOKENS.USDT);
722
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
723
+
724
+ const hash = await writeClient.redeemApUSDAndSwap({
725
+ lst: TOKENS.slisBNB,
726
+ amount,
727
+ path,
728
+ minOut: 0n,
729
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
730
+ });
731
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
732
+ await sleep(2000);
733
+ expect(receipt.status).toBe("success");
734
+
735
+ const usdtAfter = await getBalance(TOKENS.USDT);
736
+ const received = usdtAfter - usdtBefore;
737
+ console.log(` Received: ${formatAmount(received)} USDT ✓`);
738
+ expect(received).toBeGreaterThan(0n);
739
+ }, 180000);
740
+
741
+ // Test 16: redeemApUSDAndRequestUnstake (apUSD → Lista unstake)
742
+ it.skipIf(!HAS_PRIVATE_KEY)("redeemApUSDAndRequestUnstake: apUSD → Lista unstake", async () => {
743
+ console.log(`\n[E2E 16] redeemApUSDAndRequestUnstake: apUSD → Lista unstake`);
744
+
745
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
746
+ if (apUSDBalance === 0n) {
747
+ console.log(` ⚠️ No apUSD balance, skipping`);
748
+ return;
749
+ }
750
+
751
+ // Use remaining apUSD balance
752
+ console.log(` Requesting unstake for ${formatEther(apUSDBalance)} apUSD...`);
753
+ await approve(TOKENS.apUSD, apUSDBalance);
754
+
755
+ const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
756
+
757
+ const hash = await writeClient.redeemApUSDAndRequestUnstake(apUSDBalance);
758
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
759
+ await sleep(2000);
760
+ expect(receipt.status).toBe("success");
761
+
762
+ const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
763
+ expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
764
+ console.log(` Unstake requested ✓ (new indices: ${indicesAfter.length - indicesBefore.length})`);
765
+ }, 180000);
393
766
  });
394
767
  });