@aspan/sdk 0.2.2 → 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/dist/index.d.mts CHANGED
@@ -212,6 +212,8 @@ interface RouterSwapParams {
212
212
  }
213
213
  /** Router mint parameters */
214
214
  interface RouterMintParams {
215
+ /** true = mint xBNB, false = mint apUSD */
216
+ mintXBNB: boolean;
215
217
  /** Minimum output to receive (slippage protection) */
216
218
  minMintOut: bigint;
217
219
  /** Recipient of minted tokens (address(0) = msg.sender) */
@@ -1672,6 +1674,9 @@ declare const RouterABI: readonly [{
1672
1674
  readonly name: "mintParams";
1673
1675
  readonly type: "tuple";
1674
1676
  readonly components: readonly [{
1677
+ readonly name: "mintXBNB";
1678
+ readonly type: "bool";
1679
+ }, {
1675
1680
  readonly name: "minMintOut";
1676
1681
  readonly type: "uint256";
1677
1682
  }, {
@@ -1716,6 +1721,9 @@ declare const RouterABI: readonly [{
1716
1721
  readonly name: "mintParams";
1717
1722
  readonly type: "tuple";
1718
1723
  readonly components: readonly [{
1724
+ readonly name: "mintXBNB";
1725
+ readonly type: "bool";
1726
+ }, {
1719
1727
  readonly name: "minMintOut";
1720
1728
  readonly type: "uint256";
1721
1729
  }, {
package/dist/index.d.ts CHANGED
@@ -212,6 +212,8 @@ interface RouterSwapParams {
212
212
  }
213
213
  /** Router mint parameters */
214
214
  interface RouterMintParams {
215
+ /** true = mint xBNB, false = mint apUSD */
216
+ mintXBNB: boolean;
215
217
  /** Minimum output to receive (slippage protection) */
216
218
  minMintOut: bigint;
217
219
  /** Recipient of minted tokens (address(0) = msg.sender) */
@@ -1672,6 +1674,9 @@ declare const RouterABI: readonly [{
1672
1674
  readonly name: "mintParams";
1673
1675
  readonly type: "tuple";
1674
1676
  readonly components: readonly [{
1677
+ readonly name: "mintXBNB";
1678
+ readonly type: "bool";
1679
+ }, {
1675
1680
  readonly name: "minMintOut";
1676
1681
  readonly type: "uint256";
1677
1682
  }, {
@@ -1716,6 +1721,9 @@ declare const RouterABI: readonly [{
1716
1721
  readonly name: "mintParams";
1717
1722
  readonly type: "tuple";
1718
1723
  readonly components: readonly [{
1724
+ readonly name: "mintXBNB";
1725
+ readonly type: "bool";
1726
+ }, {
1719
1727
  readonly name: "minMintOut";
1720
1728
  readonly type: "uint256";
1721
1729
  }, {
package/dist/index.js CHANGED
@@ -1483,6 +1483,7 @@ var RouterABI = [
1483
1483
  name: "mintParams",
1484
1484
  type: "tuple",
1485
1485
  components: [
1486
+ { name: "mintXBNB", type: "bool" },
1486
1487
  { name: "minMintOut", type: "uint256" },
1487
1488
  { name: "recipient", type: "address" },
1488
1489
  { name: "deadline", type: "uint256" }
@@ -1515,6 +1516,7 @@ var RouterABI = [
1515
1516
  name: "mintParams",
1516
1517
  type: "tuple",
1517
1518
  components: [
1519
+ { name: "mintXBNB", type: "bool" },
1518
1520
  { name: "minMintOut", type: "uint256" },
1519
1521
  { name: "recipient", type: "address" },
1520
1522
  { name: "deadline", type: "uint256" }
@@ -2184,6 +2186,8 @@ var AspanRouterClient = class extends AspanRouterReadClient {
2184
2186
  poolFee: params.swapParams.poolFee
2185
2187
  },
2186
2188
  {
2189
+ mintXBNB: false,
2190
+ // swapAndMintApUSD always mints apUSD
2187
2191
  minMintOut: params.mintParams.minMintOut,
2188
2192
  recipient: params.mintParams.recipient,
2189
2193
  deadline: params.mintParams.deadline
@@ -2212,6 +2216,8 @@ var AspanRouterClient = class extends AspanRouterReadClient {
2212
2216
  poolFee: params.swapParams.poolFee
2213
2217
  },
2214
2218
  {
2219
+ mintXBNB: true,
2220
+ // swapAndMintXBNB always mints xBNB
2215
2221
  minMintOut: params.mintParams.minMintOut,
2216
2222
  recipient: params.mintParams.recipient,
2217
2223
  deadline: params.mintParams.deadline
package/dist/index.mjs CHANGED
@@ -1443,6 +1443,7 @@ var RouterABI = [
1443
1443
  name: "mintParams",
1444
1444
  type: "tuple",
1445
1445
  components: [
1446
+ { name: "mintXBNB", type: "bool" },
1446
1447
  { name: "minMintOut", type: "uint256" },
1447
1448
  { name: "recipient", type: "address" },
1448
1449
  { name: "deadline", type: "uint256" }
@@ -1475,6 +1476,7 @@ var RouterABI = [
1475
1476
  name: "mintParams",
1476
1477
  type: "tuple",
1477
1478
  components: [
1479
+ { name: "mintXBNB", type: "bool" },
1478
1480
  { name: "minMintOut", type: "uint256" },
1479
1481
  { name: "recipient", type: "address" },
1480
1482
  { name: "deadline", type: "uint256" }
@@ -2144,6 +2146,8 @@ var AspanRouterClient = class extends AspanRouterReadClient {
2144
2146
  poolFee: params.swapParams.poolFee
2145
2147
  },
2146
2148
  {
2149
+ mintXBNB: false,
2150
+ // swapAndMintApUSD always mints apUSD
2147
2151
  minMintOut: params.mintParams.minMintOut,
2148
2152
  recipient: params.mintParams.recipient,
2149
2153
  deadline: params.mintParams.deadline
@@ -2172,6 +2176,8 @@ var AspanRouterClient = class extends AspanRouterReadClient {
2172
2176
  poolFee: params.swapParams.poolFee
2173
2177
  },
2174
2178
  {
2179
+ mintXBNB: true,
2180
+ // swapAndMintXBNB always mints xBNB
2175
2181
  minMintOut: params.mintParams.minMintOut,
2176
2182
  recipient: params.mintParams.recipient,
2177
2183
  deadline: params.mintParams.deadline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspan/sdk",
3
- "version": "0.2.2",
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,
@@ -33,7 +33,7 @@ const TOKENS = {
33
33
  };
34
34
 
35
35
  // Minimal amounts
36
- const BNB_AMOUNT = parseEther("0.002"); // ~$1.20
36
+ const BNB_AMOUNT = parseEther("0.0005"); // ~$0.30
37
37
  const USDT_AMOUNT = parseAmount("0.5"); // $0.50
38
38
 
39
39
  const ERC20_ABI = [
@@ -44,6 +44,9 @@ const ERC20_ABI = [
44
44
  // Check env at module level
45
45
  const HAS_PRIVATE_KEY = !!process.env.PRIVATE_KEY;
46
46
 
47
+ // Helper to wait for RPC state sync
48
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
49
+
47
50
  // ============ Tests ============
48
51
 
49
52
  describe("AspanRouter SDK", () => {
@@ -69,6 +72,7 @@ describe("AspanRouter SDK", () => {
69
72
  args: [ROUTER, amount],
70
73
  });
71
74
  await publicClient.waitForTransactionReceipt({ hash });
75
+ await sleep(1000);
72
76
  };
73
77
 
74
78
  beforeAll(() => {
@@ -136,7 +140,7 @@ describe("AspanRouter SDK", () => {
136
140
 
137
141
  // ============ E2E Tests (Sequential) ============
138
142
 
139
- describe("E2E Flows", () => {
143
+ describe.sequential("E2E Flows", () => {
140
144
  // Test 1: BNB → apUSD → slisBNB
141
145
  it.skipIf(!HAS_PRIVATE_KEY)("BNB → apUSD → slisBNB", async () => {
142
146
  console.log(`\n[E2E 1] BNB → apUSD → slisBNB`);
@@ -147,7 +151,9 @@ describe("AspanRouter SDK", () => {
147
151
 
148
152
  const mintHash = await writeClient.stakeAndMintApUSD(0n, BNB_AMOUNT);
149
153
  const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
154
+ await sleep(2000);
150
155
  expect(mintReceipt.status).toBe("success");
156
+ await sleep(2000); // Wait for RPC state sync
151
157
 
152
158
  const apUSDAfter = await getBalance(TOKENS.apUSD);
153
159
  const apUSDMinted = apUSDAfter - apUSDBefore;
@@ -166,6 +172,7 @@ describe("AspanRouter SDK", () => {
166
172
  minOut: 0n,
167
173
  });
168
174
  const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
175
+ await sleep(2000);
169
176
  expect(redeemReceipt.status).toBe("success");
170
177
 
171
178
  const slisBNBAfter = await getBalance(TOKENS.slisBNB);
@@ -184,6 +191,7 @@ describe("AspanRouter SDK", () => {
184
191
  minOut: 0n,
185
192
  });
186
193
  await publicClient.waitForTransactionReceipt({ hash: directMintHash });
194
+ await sleep(2000);
187
195
 
188
196
  const apUSDAfter2 = await getBalance(TOKENS.apUSD);
189
197
  const directMinted = apUSDAfter2 - apUSDBefore2;
@@ -197,6 +205,7 @@ describe("AspanRouter SDK", () => {
197
205
  const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
198
206
  const unstakeHash = await writeClient.redeemApUSDAndRequestUnstake(directMinted);
199
207
  await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
208
+ await sleep(2000);
200
209
 
201
210
  const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
202
211
  expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
@@ -213,6 +222,7 @@ describe("AspanRouter SDK", () => {
213
222
 
214
223
  const mintHash = await writeClient.stakeAndMintXBNB(0n, BNB_AMOUNT);
215
224
  const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
225
+ await sleep(2000);
216
226
  expect(mintReceipt.status).toBe("success");
217
227
 
218
228
  const xBNBAfter = await getBalance(TOKENS.xBNB);
@@ -232,6 +242,7 @@ describe("AspanRouter SDK", () => {
232
242
  minOut: 0n,
233
243
  });
234
244
  const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
245
+ await sleep(2000);
235
246
  expect(redeemReceipt.status).toBe("success");
236
247
 
237
248
  const slisBNBAfter = await getBalance(TOKENS.slisBNB);
@@ -250,6 +261,7 @@ describe("AspanRouter SDK", () => {
250
261
  minOut: 0n,
251
262
  });
252
263
  await publicClient.waitForTransactionReceipt({ hash: directMintHash });
264
+ await sleep(2000);
253
265
 
254
266
  const xBNBAfter2 = await getBalance(TOKENS.xBNB);
255
267
  console.log(` Minted: ${formatAmount(xBNBAfter2 - xBNBBefore2, 8)} xBNB ✓`);
@@ -288,6 +300,7 @@ describe("AspanRouter SDK", () => {
288
300
  },
289
301
  });
290
302
  const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
303
+ await sleep(2000);
291
304
  expect(mintReceipt.status).toBe("success");
292
305
 
293
306
  const apUSDAfter = await getBalance(TOKENS.apUSD);
@@ -310,6 +323,7 @@ describe("AspanRouter SDK", () => {
310
323
  deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
311
324
  });
312
325
  const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
326
+ await sleep(2000);
313
327
  expect(redeemReceipt.status).toBe("success");
314
328
 
315
329
  const usdtAfter = await getBalance(TOKENS.USDT);
@@ -349,6 +363,7 @@ describe("AspanRouter SDK", () => {
349
363
  },
350
364
  });
351
365
  const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
366
+ await sleep(2000);
352
367
  expect(mintReceipt.status).toBe("success");
353
368
 
354
369
  const xBNBAfter = await getBalance(TOKENS.xBNB);
@@ -368,11 +383,385 @@ describe("AspanRouter SDK", () => {
368
383
  minOut: 0n,
369
384
  });
370
385
  const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
386
+ await sleep(2000);
371
387
  expect(redeemReceipt.status).toBe("success");
372
388
 
373
389
  const slisBNBAfter = await getBalance(TOKENS.slisBNB);
374
390
  console.log(` Received: ${formatAmount(slisBNBAfter - slisBNBBefore)} slisBNB ✓`);
375
391
  expect(slisBNBAfter).toBeGreaterThan(slisBNBBefore);
376
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);
377
766
  });
378
767
  });
package/src/abi/router.ts CHANGED
@@ -27,6 +27,7 @@ export const RouterABI = [
27
27
  name: "mintParams",
28
28
  type: "tuple",
29
29
  components: [
30
+ { name: "mintXBNB", type: "bool" },
30
31
  { name: "minMintOut", type: "uint256" },
31
32
  { name: "recipient", type: "address" },
32
33
  { name: "deadline", type: "uint256" },
@@ -60,6 +61,7 @@ export const RouterABI = [
60
61
  name: "mintParams",
61
62
  type: "tuple",
62
63
  components: [
64
+ { name: "mintXBNB", type: "bool" },
63
65
  { name: "minMintOut", type: "uint256" },
64
66
  { name: "recipient", type: "address" },
65
67
  { name: "deadline", type: "uint256" },
package/src/router.ts CHANGED
@@ -313,6 +313,7 @@ export class AspanRouterClient extends AspanRouterReadClient {
313
313
  poolFee: params.swapParams.poolFee,
314
314
  },
315
315
  {
316
+ mintXBNB: false, // swapAndMintApUSD always mints apUSD
316
317
  minMintOut: params.mintParams.minMintOut,
317
318
  recipient: params.mintParams.recipient,
318
319
  deadline: params.mintParams.deadline,
@@ -346,6 +347,7 @@ export class AspanRouterClient extends AspanRouterReadClient {
346
347
  poolFee: params.swapParams.poolFee,
347
348
  },
348
349
  {
350
+ mintXBNB: true, // swapAndMintXBNB always mints xBNB
349
351
  minMintOut: params.mintParams.minMintOut,
350
352
  recipient: params.mintParams.recipient,
351
353
  deadline: params.mintParams.deadline,
package/src/types.ts CHANGED
@@ -262,6 +262,8 @@ export interface RouterSwapParams {
262
262
 
263
263
  /** Router mint parameters */
264
264
  export interface RouterMintParams {
265
+ /** true = mint xBNB, false = mint apUSD */
266
+ mintXBNB: boolean;
265
267
  /** Minimum output to receive (slippage protection) */
266
268
  minMintOut: bigint;
267
269
  /** Recipient of minted tokens (address(0) = msg.sender) */