@aspan/sdk 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspan/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",
@@ -0,0 +1,378 @@
1
+ /**
2
+ * AspanRouter SDK Tests - E2E Flows
3
+ *
4
+ * Run with: npm test
5
+ * Requires: PRIVATE_KEY and BSC_RPC_URL environment variables
6
+ *
7
+ * Token requirements: BNB + USDT only
8
+ * Tests run sequentially to avoid race conditions
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll } from "vitest";
12
+ import { privateKeyToAccount } from "viem/accounts";
13
+ import { createPublicClient, http, parseEther, zeroAddress, type Address } from "viem";
14
+ import { bsc } from "viem/chains";
15
+ import {
16
+ AspanRouterClient,
17
+ AspanRouterReadClient,
18
+ encodeV3Path,
19
+ parseAmount,
20
+ formatAmount,
21
+ } from "../index";
22
+
23
+ // ============ Configuration ============
24
+
25
+ const ROUTER = "0xf63f34f7e9608ae7d3a6f5b06ce423d9f9043648" as Address;
26
+
27
+ const TOKENS = {
28
+ WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as Address,
29
+ USDT: "0x55d398326f99059fF775485246999027B3197955" as Address,
30
+ slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as Address,
31
+ apUSD: "0x1977097E2E5697A6DD91b6732F368a14F50f6B3d" as Address,
32
+ xBNB: "0xB78eB4d5928bAb158Eb23c3154544084cD2661d5" as Address,
33
+ };
34
+
35
+ // Minimal amounts
36
+ const BNB_AMOUNT = parseEther("0.002"); // ~$1.20
37
+ const USDT_AMOUNT = parseAmount("0.5"); // $0.50
38
+
39
+ const ERC20_ABI = [
40
+ { name: "balanceOf", type: "function", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
41
+ { name: "approve", type: "function", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }], stateMutability: "nonpayable" },
42
+ ] as const;
43
+
44
+ // Check env at module level
45
+ const HAS_PRIVATE_KEY = !!process.env.PRIVATE_KEY;
46
+
47
+ // ============ Tests ============
48
+
49
+ describe("AspanRouter SDK", () => {
50
+ let readClient: AspanRouterReadClient;
51
+ let writeClient: AspanRouterClient;
52
+ let publicClient: ReturnType<typeof createPublicClient>;
53
+ let account: ReturnType<typeof privateKeyToAccount>;
54
+
55
+ const getBalance = async (token: Address): Promise<bigint> => {
56
+ return publicClient.readContract({
57
+ address: token,
58
+ abi: ERC20_ABI,
59
+ functionName: "balanceOf",
60
+ args: [account.address],
61
+ }) as Promise<bigint>;
62
+ };
63
+
64
+ const approve = async (token: Address, amount: bigint) => {
65
+ const hash = await writeClient.walletClient.writeContract({
66
+ address: token,
67
+ abi: ERC20_ABI,
68
+ functionName: "approve",
69
+ args: [ROUTER, amount],
70
+ });
71
+ await publicClient.waitForTransactionReceipt({ hash });
72
+ };
73
+
74
+ beforeAll(() => {
75
+ const privateKey = process.env.PRIVATE_KEY;
76
+ const rpcUrl = process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org/";
77
+
78
+ readClient = new AspanRouterReadClient({
79
+ routerAddress: ROUTER,
80
+ chain: bsc,
81
+ rpcUrl,
82
+ });
83
+
84
+ publicClient = createPublicClient({
85
+ chain: bsc,
86
+ transport: http(rpcUrl),
87
+ });
88
+
89
+ if (privateKey) {
90
+ const key = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
91
+ account = privateKeyToAccount(key as `0x${string}`);
92
+
93
+ writeClient = new AspanRouterClient({
94
+ routerAddress: ROUTER,
95
+ account,
96
+ chain: bsc,
97
+ rpcUrl,
98
+ });
99
+
100
+ console.log(`📍 Wallet: ${account.address}`);
101
+ }
102
+ });
103
+
104
+ // ============ Read Tests ============
105
+
106
+ describe("Read Functions", () => {
107
+ it("should get token addresses", async () => {
108
+ const [apUSD, xBNB, slisBNB] = await Promise.all([
109
+ readClient.getApUSD(),
110
+ readClient.getXBNB(),
111
+ readClient.getSlisBNB(),
112
+ ]);
113
+ expect(apUSD.toLowerCase()).toBe(TOKENS.apUSD.toLowerCase());
114
+ expect(xBNB.toLowerCase()).toBe(TOKENS.xBNB.toLowerCase());
115
+ expect(slisBNB.toLowerCase()).toBe(TOKENS.slisBNB.toLowerCase());
116
+ });
117
+
118
+ it("should preview mint/redeem", async () => {
119
+ const lstAmount = parseAmount("1");
120
+ const apUSDOut = await readClient.previewMintApUSD(TOKENS.slisBNB, lstAmount);
121
+ expect(apUSDOut).toBeGreaterThan(parseAmount("400"));
122
+
123
+ const lstBack = await readClient.previewRedeemApUSD(TOKENS.slisBNB, apUSDOut);
124
+ const ratio = Number(lstBack) / Number(lstAmount);
125
+ expect(ratio).toBeGreaterThan(0.95);
126
+ console.log(`Preview: 1 slisBNB → ${formatAmount(apUSDOut)} apUSD (${(ratio * 100).toFixed(1)}% round-trip)`);
127
+ });
128
+ });
129
+
130
+ describe("encodeV3Path", () => {
131
+ it("should encode paths correctly", () => {
132
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
133
+ expect(path.length).toBe(134);
134
+ });
135
+ });
136
+
137
+ // ============ E2E Tests (Sequential) ============
138
+
139
+ describe("E2E Flows", () => {
140
+ // Test 1: BNB → apUSD → slisBNB
141
+ it.skipIf(!HAS_PRIVATE_KEY)("BNB → apUSD → slisBNB", async () => {
142
+ console.log(`\n[E2E 1] BNB → apUSD → slisBNB`);
143
+
144
+ // Step 1: Stake BNB → apUSD
145
+ console.log(` Staking ${formatAmount(BNB_AMOUNT)} BNB...`);
146
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
147
+
148
+ const mintHash = await writeClient.stakeAndMintApUSD(0n, BNB_AMOUNT);
149
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
150
+ expect(mintReceipt.status).toBe("success");
151
+
152
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
153
+ const apUSDMinted = apUSDAfter - apUSDBefore;
154
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
155
+ expect(apUSDMinted).toBeGreaterThan(0n);
156
+
157
+ // Step 2: Redeem apUSD → slisBNB
158
+ console.log(` Redeeming ${formatAmount(apUSDMinted)} apUSD...`);
159
+ await approve(TOKENS.apUSD, apUSDMinted);
160
+
161
+ const slisBNBBefore = await getBalance(TOKENS.slisBNB);
162
+
163
+ const redeemHash = await writeClient.redeemApUSD({
164
+ lst: TOKENS.slisBNB,
165
+ apUSDAmount: apUSDMinted,
166
+ minOut: 0n,
167
+ });
168
+ const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
169
+ expect(redeemReceipt.status).toBe("success");
170
+
171
+ const slisBNBAfter = await getBalance(TOKENS.slisBNB);
172
+ const slisBNBReceived = slisBNBAfter - slisBNBBefore;
173
+ console.log(` Received: ${formatAmount(slisBNBReceived)} slisBNB`);
174
+ expect(slisBNBReceived).toBeGreaterThan(0n);
175
+
176
+ // Step 3: Direct mint apUSD with slisBNB
177
+ console.log(` Direct minting apUSD with slisBNB...`);
178
+ await approve(TOKENS.slisBNB, slisBNBReceived);
179
+
180
+ const apUSDBefore2 = await getBalance(TOKENS.apUSD);
181
+ const directMintHash = await writeClient.mintApUSD({
182
+ lst: TOKENS.slisBNB,
183
+ lstAmount: slisBNBReceived,
184
+ minOut: 0n,
185
+ });
186
+ await publicClient.waitForTransactionReceipt({ hash: directMintHash });
187
+
188
+ const apUSDAfter2 = await getBalance(TOKENS.apUSD);
189
+ const directMinted = apUSDAfter2 - apUSDBefore2;
190
+ console.log(` Minted: ${formatAmount(directMinted)} apUSD`);
191
+ expect(directMinted).toBeGreaterThan(0n);
192
+
193
+ // Step 4: Lista unstake request
194
+ console.log(` Requesting Lista unstake...`);
195
+ await approve(TOKENS.apUSD, directMinted);
196
+
197
+ const indicesBefore = await readClient.getUserWithdrawalIndices(account.address);
198
+ const unstakeHash = await writeClient.redeemApUSDAndRequestUnstake(directMinted);
199
+ await publicClient.waitForTransactionReceipt({ hash: unstakeHash });
200
+
201
+ const indicesAfter = await readClient.getUserWithdrawalIndices(account.address);
202
+ expect(indicesAfter.length).toBeGreaterThan(indicesBefore.length);
203
+ console.log(` Unstake requested ✓`);
204
+ }, 300000);
205
+
206
+ // Test 2: BNB → xBNB → slisBNB → mintXBNB
207
+ it.skipIf(!HAS_PRIVATE_KEY)("BNB → xBNB → slisBNB → mintXBNB", async () => {
208
+ console.log(`\n[E2E 2] BNB → xBNB → slisBNB → mintXBNB`);
209
+
210
+ // Step 1: Stake BNB → xBNB
211
+ console.log(` Staking ${formatAmount(BNB_AMOUNT)} BNB...`);
212
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
213
+
214
+ const mintHash = await writeClient.stakeAndMintXBNB(0n, BNB_AMOUNT);
215
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
216
+ expect(mintReceipt.status).toBe("success");
217
+
218
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
219
+ const xBNBMinted = xBNBAfter - xBNBBefore;
220
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
221
+ expect(xBNBMinted).toBeGreaterThan(0n);
222
+
223
+ // Step 2: Redeem xBNB → slisBNB
224
+ console.log(` Redeeming ${formatAmount(xBNBMinted, 8)} xBNB...`);
225
+ await approve(TOKENS.xBNB, xBNBMinted);
226
+
227
+ const slisBNBBefore = await getBalance(TOKENS.slisBNB);
228
+
229
+ const redeemHash = await writeClient.redeemXBNB({
230
+ lst: TOKENS.slisBNB,
231
+ xBNBAmount: xBNBMinted,
232
+ minOut: 0n,
233
+ });
234
+ const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
235
+ expect(redeemReceipt.status).toBe("success");
236
+
237
+ const slisBNBAfter = await getBalance(TOKENS.slisBNB);
238
+ const slisBNBReceived2 = slisBNBAfter - slisBNBBefore;
239
+ console.log(` Received: ${formatAmount(slisBNBReceived2)} slisBNB`);
240
+ expect(slisBNBReceived2).toBeGreaterThan(0n);
241
+
242
+ // Step 3: Direct mint xBNB with slisBNB
243
+ console.log(` Direct minting xBNB with slisBNB...`);
244
+ await approve(TOKENS.slisBNB, slisBNBReceived2);
245
+
246
+ const xBNBBefore2 = await getBalance(TOKENS.xBNB);
247
+ const directMintHash = await writeClient.mintXBNB({
248
+ lst: TOKENS.slisBNB,
249
+ lstAmount: slisBNBReceived2,
250
+ minOut: 0n,
251
+ });
252
+ await publicClient.waitForTransactionReceipt({ hash: directMintHash });
253
+
254
+ const xBNBAfter2 = await getBalance(TOKENS.xBNB);
255
+ console.log(` Minted: ${formatAmount(xBNBAfter2 - xBNBBefore2, 8)} xBNB ✓`);
256
+ expect(xBNBAfter2).toBeGreaterThan(xBNBBefore2);
257
+ }, 300000);
258
+
259
+ // Test 3: USDT → apUSD → USDT
260
+ it.skipIf(!HAS_PRIVATE_KEY)("USDT → apUSD → USDT", async () => {
261
+ console.log(`\n[E2E 3] USDT → apUSD → USDT`);
262
+
263
+ // Check USDT balance
264
+ const usdtBalance = await getBalance(TOKENS.USDT);
265
+ if (usdtBalance < USDT_AMOUNT) {
266
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBalance)}), skipping`);
267
+ return;
268
+ }
269
+
270
+ // Step 1: Approve + Swap USDT → apUSD
271
+ console.log(` Swapping ${formatAmount(USDT_AMOUNT)} USDT...`);
272
+ await approve(TOKENS.USDT, USDT_AMOUNT);
273
+
274
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
275
+
276
+ const mintHash = await writeClient.swapAndMintApUSD({
277
+ swapParams: {
278
+ inputToken: TOKENS.USDT,
279
+ inputAmount: USDT_AMOUNT,
280
+ targetLST: zeroAddress,
281
+ minLSTOut: 0n,
282
+ poolFee: 2500,
283
+ },
284
+ mintParams: {
285
+ minMintOut: 0n,
286
+ recipient: zeroAddress,
287
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
288
+ },
289
+ });
290
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
291
+ expect(mintReceipt.status).toBe("success");
292
+
293
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
294
+ const apUSDMinted = apUSDAfter - apUSDBefore;
295
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
296
+ expect(apUSDMinted).toBeGreaterThan(0n);
297
+
298
+ // Step 2: Redeem apUSD → USDT via V3
299
+ console.log(` Redeeming ${formatAmount(apUSDMinted)} apUSD → USDT...`);
300
+ await approve(TOKENS.apUSD, apUSDMinted);
301
+
302
+ const usdtBefore = await getBalance(TOKENS.USDT);
303
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
304
+
305
+ const redeemHash = await writeClient.redeemApUSDAndSwap({
306
+ lst: TOKENS.slisBNB,
307
+ amount: apUSDMinted,
308
+ path,
309
+ minOut: 0n,
310
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
311
+ });
312
+ const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
313
+ expect(redeemReceipt.status).toBe("success");
314
+
315
+ const usdtAfter = await getBalance(TOKENS.USDT);
316
+ console.log(` Received: ${formatAmount(usdtAfter - usdtBefore)} USDT ✓`);
317
+ expect(usdtAfter).toBeGreaterThan(usdtBefore);
318
+ }, 180000);
319
+
320
+ // Test 4: USDT → xBNB → slisBNB
321
+ it.skipIf(!HAS_PRIVATE_KEY)("USDT → xBNB → slisBNB", async () => {
322
+ console.log(`\n[E2E 4] USDT → xBNB → slisBNB`);
323
+
324
+ // Check USDT balance
325
+ const usdtBalance = await getBalance(TOKENS.USDT);
326
+ if (usdtBalance < USDT_AMOUNT) {
327
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBalance)}), skipping`);
328
+ return;
329
+ }
330
+
331
+ // Step 1: Approve + Swap USDT → xBNB
332
+ console.log(` Swapping ${formatAmount(USDT_AMOUNT)} USDT...`);
333
+ await approve(TOKENS.USDT, USDT_AMOUNT);
334
+
335
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
336
+
337
+ const mintHash = await writeClient.swapAndMintXBNB({
338
+ swapParams: {
339
+ inputToken: TOKENS.USDT,
340
+ inputAmount: USDT_AMOUNT,
341
+ targetLST: zeroAddress,
342
+ minLSTOut: 0n,
343
+ poolFee: 2500,
344
+ },
345
+ mintParams: {
346
+ minMintOut: 0n,
347
+ recipient: zeroAddress,
348
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
349
+ },
350
+ });
351
+ const mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
352
+ expect(mintReceipt.status).toBe("success");
353
+
354
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
355
+ const xBNBMinted = xBNBAfter - xBNBBefore;
356
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
357
+ expect(xBNBMinted).toBeGreaterThan(0n);
358
+
359
+ // Step 2: Redeem xBNB → slisBNB
360
+ console.log(` Redeeming ${formatAmount(xBNBMinted, 8)} xBNB...`);
361
+ await approve(TOKENS.xBNB, xBNBMinted);
362
+
363
+ const slisBNBBefore = await getBalance(TOKENS.slisBNB);
364
+
365
+ const redeemHash = await writeClient.redeemXBNB({
366
+ lst: TOKENS.slisBNB,
367
+ xBNBAmount: xBNBMinted,
368
+ minOut: 0n,
369
+ });
370
+ const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
371
+ expect(redeemReceipt.status).toBe("success");
372
+
373
+ const slisBNBAfter = await getBalance(TOKENS.slisBNB);
374
+ console.log(` Received: ${formatAmount(slisBNBAfter - slisBNBBefore)} slisBNB ✓`);
375
+ expect(slisBNBAfter).toBeGreaterThan(slisBNBBefore);
376
+ }, 180000);
377
+ });
378
+ });