@aspan/sdk 0.1.4

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/src/client.ts ADDED
@@ -0,0 +1,1030 @@
1
+ /**
2
+ * Aspan SDK Client
3
+ * TypeScript client for interacting with the Aspan Protocol
4
+ */
5
+
6
+ import {
7
+ createPublicClient,
8
+ createWalletClient,
9
+ http,
10
+ type Address,
11
+ type PublicClient,
12
+ type WalletClient,
13
+ type Transport,
14
+ type Chain,
15
+ type Account,
16
+ type Hash,
17
+ } from "viem";
18
+ import { bsc, bscTestnet } from "viem/chains";
19
+ import { DiamondABI } from "./abi/diamond";
20
+ import type {
21
+ LSTInfo,
22
+ LSTYieldInfo,
23
+ FeeTier,
24
+ CurrentFeeTier,
25
+ CurrentFees,
26
+ OracleBounds,
27
+ StabilityModeInfo,
28
+ StabilityMode2Info,
29
+ ProtocolStats,
30
+ StabilityPoolStats,
31
+ YieldStats,
32
+ UserStabilityPoolPosition,
33
+ TokenAddresses,
34
+ MintApUSDParams,
35
+ RedeemApUSDParams,
36
+ MintXBNBParams,
37
+ RedeemXBNBParams,
38
+ DepositParams,
39
+ WithdrawParams,
40
+ WithdrawAssetsParams,
41
+ } from "./types";
42
+
43
+ // ============ Configuration ============
44
+
45
+ export interface AspanClientConfig {
46
+ /** Diamond contract address */
47
+ diamondAddress: Address;
48
+ /** Chain to connect to */
49
+ chain?: Chain;
50
+ /** RPC URL (optional, uses default if not provided) */
51
+ rpcUrl?: string;
52
+ }
53
+
54
+ export interface AspanWriteClientConfig extends AspanClientConfig {
55
+ /** Account for signing transactions */
56
+ account: Account;
57
+ }
58
+
59
+ // ============ Read-Only Client ============
60
+
61
+ /**
62
+ * Read-only client for querying Aspan Protocol state
63
+ */
64
+ export class AspanReadClient {
65
+ protected readonly publicClient: PublicClient;
66
+ protected readonly diamondAddress: Address;
67
+ protected readonly chain: Chain;
68
+
69
+ // Known error signatures for zero supply states
70
+ private static readonly ZERO_SUPPLY_ERROR_SIGNATURES = [
71
+ "0x403e7fa6", // Custom error when xBNB supply is zero
72
+ ];
73
+
74
+ constructor(config: AspanClientConfig) {
75
+ this.diamondAddress = config.diamondAddress;
76
+ this.chain = config.chain ?? bsc;
77
+
78
+ this.publicClient = createPublicClient({
79
+ chain: this.chain,
80
+ transport: http(config.rpcUrl),
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Check if error is a known zero supply error from the contract
86
+ */
87
+ protected isZeroSupplyError(error: unknown): boolean {
88
+ if (error && typeof error === "object" && "message" in error) {
89
+ const message = (error as { message: string }).message;
90
+ return AspanReadClient.ZERO_SUPPLY_ERROR_SIGNATURES.some((sig) =>
91
+ message.includes(sig)
92
+ );
93
+ }
94
+ return false;
95
+ }
96
+
97
+ // ============ Protocol Stats ============
98
+
99
+ /**
100
+ * Get comprehensive protocol statistics
101
+ */
102
+ async getProtocolStats(): Promise<ProtocolStats> {
103
+ const [
104
+ tvlInBNB,
105
+ tvlInUSD,
106
+ collateralRatio,
107
+ apUSDSupply,
108
+ xBNBSupply,
109
+ xBNBPriceBNB,
110
+ xBNBPriceUSD,
111
+ effectiveLeverage,
112
+ ] = await Promise.all([
113
+ this.getTVLInBNB(),
114
+ this.getTVLInUSD(),
115
+ this.getCollateralRatio(),
116
+ this.getApUSDSupply(),
117
+ this.getXBNBSupply(),
118
+ this.getXBNBPriceBNB(),
119
+ this.getXBNBPriceUSD(),
120
+ this.getEffectiveLeverage(),
121
+ ]);
122
+
123
+ return {
124
+ tvlInBNB,
125
+ tvlInUSD,
126
+ collateralRatio,
127
+ apUSDSupply,
128
+ xBNBSupply,
129
+ xBNBPriceBNB,
130
+ xBNBPriceUSD,
131
+ effectiveLeverage,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Get stability pool statistics
137
+ */
138
+ async getStabilityPoolStats(): Promise<StabilityPoolStats> {
139
+ const [totalStaked, exchangeRate, pendingYield] = await Promise.all([
140
+ this.getTotalStaked(),
141
+ this.getExchangeRate(),
142
+ this.getPendingYield(),
143
+ ]);
144
+
145
+ return {
146
+ totalStaked,
147
+ exchangeRate,
148
+ pendingYield,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Get yield statistics
154
+ */
155
+ async getYieldStats(): Promise<YieldStats> {
156
+ const [
157
+ totalYieldGenerated,
158
+ pendingYield,
159
+ lastHarvestTimestamp,
160
+ minHarvestInterval,
161
+ previewedHarvest,
162
+ ] = await Promise.all([
163
+ this.getTotalYieldGenerated(),
164
+ this.getPendingYield(),
165
+ this.getLastHarvestTimestamp(),
166
+ this.getMinHarvestInterval(),
167
+ this.previewHarvest(),
168
+ ]);
169
+
170
+ return {
171
+ totalYieldGenerated,
172
+ pendingYield,
173
+ lastHarvestTimestamp,
174
+ minHarvestInterval,
175
+ previewedHarvest,
176
+ };
177
+ }
178
+
179
+ // ============ Pool View Functions ============
180
+
181
+ async getTVLInBNB(): Promise<bigint> {
182
+ try {
183
+ return await this.publicClient.readContract({
184
+ address: this.diamondAddress,
185
+ abi: DiamondABI,
186
+ functionName: "getTVLInBNB",
187
+ });
188
+ } catch (error) {
189
+ if (this.isZeroSupplyError(error)) {
190
+ return 0n;
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ async getTVLInUSD(): Promise<bigint> {
197
+ try {
198
+ return await this.publicClient.readContract({
199
+ address: this.diamondAddress,
200
+ abi: DiamondABI,
201
+ functionName: "getTVLInUSD",
202
+ });
203
+ } catch (error) {
204
+ if (this.isZeroSupplyError(error)) {
205
+ return 0n;
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ async getCollateralRatio(): Promise<bigint> {
212
+ try {
213
+ const cr = await this.publicClient.readContract({
214
+ address: this.diamondAddress,
215
+ abi: DiamondABI,
216
+ functionName: "getCollateralRatio",
217
+ });
218
+ // Contract returns max uint256 when supply is zero (infinite CR)
219
+ // Treat anything above 1000000% (10000 * 1e18) as zero/undefined
220
+ if (cr > 10000n * 10n ** 18n) {
221
+ return 0n;
222
+ }
223
+ return cr;
224
+ } catch (error) {
225
+ // CR undefined when no apUSD supply exists
226
+ if (this.isZeroSupplyError(error)) {
227
+ return 0n;
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ async getXBNBPriceBNB(): Promise<bigint> {
234
+ try {
235
+ return await this.publicClient.readContract({
236
+ address: this.diamondAddress,
237
+ abi: DiamondABI,
238
+ functionName: "getXBNBPriceBNB",
239
+ });
240
+ } catch (error) {
241
+ // Price undefined when no xBNB exists
242
+ if (this.isZeroSupplyError(error)) {
243
+ return 0n;
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ async getXBNBPriceUSD(): Promise<bigint> {
250
+ try {
251
+ return await this.publicClient.readContract({
252
+ address: this.diamondAddress,
253
+ abi: DiamondABI,
254
+ functionName: "getXBNBPriceUSD",
255
+ });
256
+ } catch (error) {
257
+ // Price undefined when no xBNB exists
258
+ if (this.isZeroSupplyError(error)) {
259
+ return 0n;
260
+ }
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ async getEffectiveLeverage(): Promise<bigint> {
266
+ try {
267
+ return await this.publicClient.readContract({
268
+ address: this.diamondAddress,
269
+ abi: DiamondABI,
270
+ functionName: "getEffectiveLeverage",
271
+ });
272
+ } catch (error) {
273
+ // Leverage undefined when no xBNB exists
274
+ if (this.isZeroSupplyError(error)) {
275
+ return 0n;
276
+ }
277
+ throw error;
278
+ }
279
+ }
280
+
281
+ async getApUSDSupply(): Promise<bigint> {
282
+ try {
283
+ return await this.publicClient.readContract({
284
+ address: this.diamondAddress,
285
+ abi: DiamondABI,
286
+ functionName: "getApUSDSupply",
287
+ });
288
+ } catch (error) {
289
+ // Contract reverts with custom error when no apUSD has been minted
290
+ if (this.isZeroSupplyError(error)) {
291
+ return 0n;
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ async getXBNBSupply(): Promise<bigint> {
298
+ try {
299
+ return await this.publicClient.readContract({
300
+ address: this.diamondAddress,
301
+ abi: DiamondABI,
302
+ functionName: "getXBNBSupply",
303
+ });
304
+ } catch (error) {
305
+ // Contract reverts with custom error when no xBNB has been minted
306
+ if (this.isZeroSupplyError(error)) {
307
+ return 0n;
308
+ }
309
+ throw error;
310
+ }
311
+ }
312
+
313
+ async getLSTCollateral(lstToken: Address): Promise<bigint> {
314
+ try {
315
+ return await this.publicClient.readContract({
316
+ address: this.diamondAddress,
317
+ abi: DiamondABI,
318
+ functionName: "getLSTCollateral",
319
+ args: [lstToken],
320
+ });
321
+ } catch (error) {
322
+ if (this.isZeroSupplyError(error)) {
323
+ return 0n;
324
+ }
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ async getCurrentFees(): Promise<CurrentFees> {
330
+ try {
331
+ const result = await this.publicClient.readContract({
332
+ address: this.diamondAddress,
333
+ abi: DiamondABI,
334
+ functionName: "getCurrentFees",
335
+ });
336
+
337
+ return {
338
+ currentCR: result[0],
339
+ tierMinCR: result[1],
340
+ apUSDMintFee: result[2],
341
+ apUSDRedeemFee: result[3],
342
+ xBNBMintFee: result[4],
343
+ xBNBRedeemFee: result[5],
344
+ apUSDMintDisabled: result[6],
345
+ };
346
+ } catch (error) {
347
+ // Return default fees when protocol is empty
348
+ if (this.isZeroSupplyError(error)) {
349
+ return {
350
+ currentCR: 0n,
351
+ tierMinCR: 0n,
352
+ apUSDMintFee: 0,
353
+ apUSDRedeemFee: 0,
354
+ xBNBMintFee: 0,
355
+ xBNBRedeemFee: 0,
356
+ apUSDMintDisabled: false,
357
+ };
358
+ }
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ // ============ Oracle View Functions ============
364
+
365
+ async getBNBPriceUSD(): Promise<bigint> {
366
+ return this.publicClient.readContract({
367
+ address: this.diamondAddress,
368
+ abi: DiamondABI,
369
+ functionName: "getBNBPriceUSD",
370
+ });
371
+ }
372
+
373
+ async getLSTPriceUSD(lstToken: Address): Promise<bigint> {
374
+ return this.publicClient.readContract({
375
+ address: this.diamondAddress,
376
+ abi: DiamondABI,
377
+ functionName: "getLSTPriceUSD",
378
+ args: [lstToken],
379
+ });
380
+ }
381
+
382
+ async getLSTInfo(lstToken: Address): Promise<LSTInfo> {
383
+ const result = await this.publicClient.readContract({
384
+ address: this.diamondAddress,
385
+ abi: DiamondABI,
386
+ functionName: "getLSTInfo",
387
+ args: [lstToken],
388
+ });
389
+
390
+ return {
391
+ isAccepted: result[0],
392
+ priceFeed: result[1],
393
+ manualPriceUSD: result[2],
394
+ collateralAmount: result[3],
395
+ decimals: result[4],
396
+ exchangeRateProvider: result[5],
397
+ useIntrinsicValue: result[6],
398
+ };
399
+ }
400
+
401
+ async getSupportedLSTs(): Promise<readonly Address[]> {
402
+ return this.publicClient.readContract({
403
+ address: this.diamondAddress,
404
+ abi: DiamondABI,
405
+ functionName: "getSupportedLSTs",
406
+ });
407
+ }
408
+
409
+ async isLSTSupported(lstToken: Address): Promise<boolean> {
410
+ return this.publicClient.readContract({
411
+ address: this.diamondAddress,
412
+ abi: DiamondABI,
413
+ functionName: "isLSTSupported",
414
+ args: [lstToken],
415
+ });
416
+ }
417
+
418
+ async getBNBPriceFeed(): Promise<Address> {
419
+ return this.publicClient.readContract({
420
+ address: this.diamondAddress,
421
+ abi: DiamondABI,
422
+ functionName: "bnbPriceFeed",
423
+ });
424
+ }
425
+
426
+ async getOracleBounds(priceFeed: Address): Promise<OracleBounds> {
427
+ const result = await this.publicClient.readContract({
428
+ address: this.diamondAddress,
429
+ abi: DiamondABI,
430
+ functionName: "getOracleBounds",
431
+ args: [priceFeed],
432
+ });
433
+
434
+ return {
435
+ minPrice: result[0],
436
+ maxPrice: result[1],
437
+ };
438
+ }
439
+
440
+ // ============ Stability Pool View Functions ============
441
+
442
+ async getShares(user: Address): Promise<bigint> {
443
+ return this.publicClient.readContract({
444
+ address: this.diamondAddress,
445
+ abi: DiamondABI,
446
+ functionName: "getShares",
447
+ args: [user],
448
+ });
449
+ }
450
+
451
+ async getBalance(user: Address): Promise<bigint> {
452
+ return this.publicClient.readContract({
453
+ address: this.diamondAddress,
454
+ abi: DiamondABI,
455
+ functionName: "getBalance",
456
+ args: [user],
457
+ });
458
+ }
459
+
460
+ async getUserStabilityPoolPosition(
461
+ user: Address
462
+ ): Promise<UserStabilityPoolPosition> {
463
+ const [shares, balance] = await Promise.all([
464
+ this.getShares(user),
465
+ this.getBalance(user),
466
+ ]);
467
+
468
+ return { shares, balance };
469
+ }
470
+
471
+ async getExchangeRate(): Promise<bigint> {
472
+ try {
473
+ return await this.publicClient.readContract({
474
+ address: this.diamondAddress,
475
+ abi: DiamondABI,
476
+ functionName: "getExchangeRate",
477
+ });
478
+ } catch (error) {
479
+ // Exchange rate defaults to 1e18 when stability pool is empty
480
+ if (this.isZeroSupplyError(error)) {
481
+ return 10n ** 18n; // 1:1 ratio
482
+ }
483
+ throw error;
484
+ }
485
+ }
486
+
487
+ async getTotalStaked(): Promise<bigint> {
488
+ try {
489
+ return await this.publicClient.readContract({
490
+ address: this.diamondAddress,
491
+ abi: DiamondABI,
492
+ functionName: "getTotalStaked",
493
+ });
494
+ } catch (error) {
495
+ if (this.isZeroSupplyError(error)) {
496
+ return 0n;
497
+ }
498
+ throw error;
499
+ }
500
+ }
501
+
502
+ async previewDeposit(assets: bigint): Promise<bigint> {
503
+ try {
504
+ return await this.publicClient.readContract({
505
+ address: this.diamondAddress,
506
+ abi: DiamondABI,
507
+ functionName: "previewDeposit",
508
+ args: [assets],
509
+ });
510
+ } catch (error) {
511
+ if (this.isZeroSupplyError(error)) {
512
+ return assets; // 1:1 when pool is empty
513
+ }
514
+ throw error;
515
+ }
516
+ }
517
+
518
+ async previewRedeem(shares: bigint): Promise<bigint> {
519
+ try {
520
+ return await this.publicClient.readContract({
521
+ address: this.diamondAddress,
522
+ abi: DiamondABI,
523
+ functionName: "previewRedeem",
524
+ args: [shares],
525
+ });
526
+ } catch (error) {
527
+ if (this.isZeroSupplyError(error)) {
528
+ return shares; // 1:1 when pool is empty
529
+ }
530
+ throw error;
531
+ }
532
+ }
533
+
534
+ async getPendingYield(): Promise<bigint> {
535
+ try {
536
+ return await this.publicClient.readContract({
537
+ address: this.diamondAddress,
538
+ abi: DiamondABI,
539
+ functionName: "getPendingYield",
540
+ });
541
+ } catch (error) {
542
+ if (this.isZeroSupplyError(error)) {
543
+ return 0n;
544
+ }
545
+ throw error;
546
+ }
547
+ }
548
+
549
+ // ============ Yield View Functions ============
550
+
551
+ async getTotalYieldGenerated(): Promise<bigint> {
552
+ try {
553
+ return await this.publicClient.readContract({
554
+ address: this.diamondAddress,
555
+ abi: DiamondABI,
556
+ functionName: "getTotalYieldGenerated",
557
+ });
558
+ } catch (error) {
559
+ if (this.isZeroSupplyError(error)) {
560
+ return 0n;
561
+ }
562
+ throw error;
563
+ }
564
+ }
565
+
566
+ async getLSTYieldInfo(lstToken: Address): Promise<LSTYieldInfo> {
567
+ try {
568
+ const result = await this.publicClient.readContract({
569
+ address: this.diamondAddress,
570
+ abi: DiamondABI,
571
+ functionName: "getLSTYieldInfo",
572
+ args: [lstToken],
573
+ });
574
+
575
+ return {
576
+ lastExchangeRate: result[0],
577
+ lastUpdateTimestamp: result[1],
578
+ };
579
+ } catch (error) {
580
+ if (this.isZeroSupplyError(error)) {
581
+ return {
582
+ lastExchangeRate: 0n,
583
+ lastUpdateTimestamp: 0n,
584
+ };
585
+ }
586
+ throw error;
587
+ }
588
+ }
589
+
590
+ async getMinHarvestInterval(): Promise<bigint> {
591
+ try {
592
+ return await this.publicClient.readContract({
593
+ address: this.diamondAddress,
594
+ abi: DiamondABI,
595
+ functionName: "getMinHarvestInterval",
596
+ });
597
+ } catch (error) {
598
+ if (this.isZeroSupplyError(error)) {
599
+ return 0n;
600
+ }
601
+ throw error;
602
+ }
603
+ }
604
+
605
+ async getLastHarvestTimestamp(): Promise<bigint> {
606
+ try {
607
+ return await this.publicClient.readContract({
608
+ address: this.diamondAddress,
609
+ abi: DiamondABI,
610
+ functionName: "getLastHarvestTimestamp",
611
+ });
612
+ } catch (error) {
613
+ if (this.isZeroSupplyError(error)) {
614
+ return 0n;
615
+ }
616
+ throw error;
617
+ }
618
+ }
619
+
620
+ async previewHarvest(): Promise<bigint> {
621
+ try {
622
+ return await this.publicClient.readContract({
623
+ address: this.diamondAddress,
624
+ abi: DiamondABI,
625
+ functionName: "previewHarvest",
626
+ });
627
+ } catch (error) {
628
+ // No yield to harvest when no collateral exists
629
+ if (this.isZeroSupplyError(error)) {
630
+ return 0n;
631
+ }
632
+ throw error;
633
+ }
634
+ }
635
+
636
+ // ============ Config View Functions ============
637
+
638
+ async getTokens(): Promise<TokenAddresses> {
639
+ const [tokens, sApUSD] = await Promise.all([
640
+ this.publicClient.readContract({
641
+ address: this.diamondAddress,
642
+ abi: DiamondABI,
643
+ functionName: "getTokens",
644
+ }),
645
+ this.getSApUSD(),
646
+ ]);
647
+
648
+ return {
649
+ apUSD: tokens[0],
650
+ xBNB: tokens[1],
651
+ sApUSD,
652
+ };
653
+ }
654
+
655
+ async getSApUSD(): Promise<Address> {
656
+ return this.publicClient.readContract({
657
+ address: this.diamondAddress,
658
+ abi: DiamondABI,
659
+ functionName: "getSApUSD",
660
+ });
661
+ }
662
+
663
+ async getStabilityPool(): Promise<Address> {
664
+ return this.publicClient.readContract({
665
+ address: this.diamondAddress,
666
+ abi: DiamondABI,
667
+ functionName: "getStabilityPool",
668
+ });
669
+ }
670
+
671
+ async getTreasury(): Promise<Address> {
672
+ return this.publicClient.readContract({
673
+ address: this.diamondAddress,
674
+ abi: DiamondABI,
675
+ functionName: "getTreasury",
676
+ });
677
+ }
678
+
679
+ async getFeeTierCount(): Promise<bigint> {
680
+ return this.publicClient.readContract({
681
+ address: this.diamondAddress,
682
+ abi: DiamondABI,
683
+ functionName: "getFeeTierCount",
684
+ });
685
+ }
686
+
687
+ async getFeeTier(index: bigint): Promise<FeeTier> {
688
+ const result = await this.publicClient.readContract({
689
+ address: this.diamondAddress,
690
+ abi: DiamondABI,
691
+ functionName: "getFeeTier",
692
+ args: [index],
693
+ });
694
+
695
+ return {
696
+ minCR: result[0],
697
+ apUSDMintFee: result[1],
698
+ apUSDRedeemFee: result[2],
699
+ xBNBMintFee: result[3],
700
+ xBNBRedeemFee: result[4],
701
+ apUSDMintDisabled: result[5],
702
+ };
703
+ }
704
+
705
+ async getCurrentFeeTier(): Promise<CurrentFeeTier> {
706
+ try {
707
+ const result = await this.publicClient.readContract({
708
+ address: this.diamondAddress,
709
+ abi: DiamondABI,
710
+ functionName: "getCurrentFeeTier",
711
+ });
712
+
713
+ return {
714
+ minCR: result[0],
715
+ apUSDMintFee: result[1],
716
+ apUSDRedeemFee: result[2],
717
+ xBNBMintFee: result[3],
718
+ xBNBRedeemFee: result[4],
719
+ apUSDMintDisabled: result[5],
720
+ currentCR: result[6],
721
+ };
722
+ } catch (error) {
723
+ // Return default fee tier when protocol is empty
724
+ if (this.isZeroSupplyError(error)) {
725
+ return {
726
+ minCR: 0n,
727
+ apUSDMintFee: 0,
728
+ apUSDRedeemFee: 0,
729
+ xBNBMintFee: 0,
730
+ xBNBRedeemFee: 0,
731
+ apUSDMintDisabled: false,
732
+ currentCR: 0n,
733
+ };
734
+ }
735
+ throw error;
736
+ }
737
+ }
738
+
739
+ async getMaxPriceAge(): Promise<bigint> {
740
+ return this.publicClient.readContract({
741
+ address: this.diamondAddress,
742
+ abi: DiamondABI,
743
+ functionName: "getMaxPriceAge",
744
+ });
745
+ }
746
+
747
+ async getMinDepositPeriod(): Promise<bigint> {
748
+ return this.publicClient.readContract({
749
+ address: this.diamondAddress,
750
+ abi: DiamondABI,
751
+ functionName: "getMinDepositPeriod",
752
+ });
753
+ }
754
+
755
+ async isPaused(): Promise<boolean> {
756
+ return this.publicClient.readContract({
757
+ address: this.diamondAddress,
758
+ abi: DiamondABI,
759
+ functionName: "isPaused",
760
+ });
761
+ }
762
+
763
+ // ============ Stability Mode View Functions ============
764
+
765
+ async getStabilityMode(): Promise<StabilityModeInfo> {
766
+ try {
767
+ const result = await this.publicClient.readContract({
768
+ address: this.diamondAddress,
769
+ abi: DiamondABI,
770
+ functionName: "getStabilityMode",
771
+ });
772
+
773
+ return {
774
+ mode: result[0],
775
+ currentCR: result[1],
776
+ };
777
+ } catch (error) {
778
+ // Return normal mode when protocol is empty
779
+ if (this.isZeroSupplyError(error)) {
780
+ return {
781
+ mode: 0, // Normal mode
782
+ currentCR: 0n,
783
+ };
784
+ }
785
+ throw error;
786
+ }
787
+ }
788
+
789
+ async canTriggerStabilityMode2(): Promise<StabilityMode2Info> {
790
+ try {
791
+ const result = await this.publicClient.readContract({
792
+ address: this.diamondAddress,
793
+ abi: DiamondABI,
794
+ functionName: "canTriggerStabilityMode2",
795
+ });
796
+
797
+ return {
798
+ canTrigger: result[0],
799
+ currentCR: result[1],
800
+ potentialConversion: result[2],
801
+ };
802
+ } catch (error) {
803
+ // Cannot trigger when protocol is empty
804
+ if (this.isZeroSupplyError(error)) {
805
+ return {
806
+ canTrigger: false,
807
+ currentCR: 0n,
808
+ potentialConversion: 0n,
809
+ };
810
+ }
811
+ throw error;
812
+ }
813
+ }
814
+
815
+ // ============ Ownership View Functions ============
816
+
817
+ async getOwner(): Promise<Address> {
818
+ return this.publicClient.readContract({
819
+ address: this.diamondAddress,
820
+ abi: DiamondABI,
821
+ functionName: "owner",
822
+ });
823
+ }
824
+ }
825
+
826
+ // ============ Write Client ============
827
+
828
+ /**
829
+ * Full client with write capabilities for interacting with Aspan Protocol
830
+ */
831
+ export class AspanClient extends AspanReadClient {
832
+ private readonly walletClient: WalletClient<Transport, Chain, Account>;
833
+
834
+ constructor(config: AspanWriteClientConfig) {
835
+ super(config);
836
+
837
+ this.walletClient = createWalletClient({
838
+ account: config.account,
839
+ chain: this.chain,
840
+ transport: http(config.rpcUrl),
841
+ });
842
+ }
843
+
844
+ // ============ Pool Write Functions ============
845
+
846
+ /**
847
+ * Mint apUSD by depositing LST
848
+ * @param params Mint parameters
849
+ * @returns Transaction hash
850
+ */
851
+ async mintApUSD(params: MintApUSDParams): Promise<Hash> {
852
+ return this.walletClient.writeContract({
853
+ address: this.diamondAddress,
854
+ abi: DiamondABI,
855
+ functionName: "mintApUSD",
856
+ args: [params.lstToken, params.lstAmount],
857
+ });
858
+ }
859
+
860
+ /**
861
+ * Redeem apUSD for LST
862
+ * @param params Redeem parameters
863
+ * @returns Transaction hash
864
+ */
865
+ async redeemApUSD(params: RedeemApUSDParams): Promise<Hash> {
866
+ return this.walletClient.writeContract({
867
+ address: this.diamondAddress,
868
+ abi: DiamondABI,
869
+ functionName: "redeemApUSD",
870
+ args: [params.lstToken, params.apUSDAmount],
871
+ });
872
+ }
873
+
874
+ /**
875
+ * Mint xBNB by depositing LST
876
+ * @param params Mint parameters
877
+ * @returns Transaction hash
878
+ */
879
+ async mintXBNB(params: MintXBNBParams): Promise<Hash> {
880
+ return this.walletClient.writeContract({
881
+ address: this.diamondAddress,
882
+ abi: DiamondABI,
883
+ functionName: "mintXBNB",
884
+ args: [params.lstToken, params.lstAmount],
885
+ });
886
+ }
887
+
888
+ /**
889
+ * Redeem xBNB for LST
890
+ * @param params Redeem parameters
891
+ * @returns Transaction hash
892
+ */
893
+ async redeemXBNB(params: RedeemXBNBParams): Promise<Hash> {
894
+ return this.walletClient.writeContract({
895
+ address: this.diamondAddress,
896
+ abi: DiamondABI,
897
+ functionName: "redeemXBNB",
898
+ args: [params.lstToken, params.xBNBAmount],
899
+ });
900
+ }
901
+
902
+ // ============ Stability Pool Write Functions ============
903
+
904
+ /**
905
+ * Deposit apUSD to stability pool to earn yield
906
+ * @param params Deposit parameters
907
+ * @returns Transaction hash
908
+ */
909
+ async deposit(params: DepositParams): Promise<Hash> {
910
+ return this.walletClient.writeContract({
911
+ address: this.diamondAddress,
912
+ abi: DiamondABI,
913
+ functionName: "deposit",
914
+ args: [params.apUSDAmount],
915
+ });
916
+ }
917
+
918
+ /**
919
+ * Withdraw from stability pool by shares
920
+ * @param params Withdraw parameters
921
+ * @returns Transaction hash
922
+ */
923
+ async withdraw(params: WithdrawParams): Promise<Hash> {
924
+ return this.walletClient.writeContract({
925
+ address: this.diamondAddress,
926
+ abi: DiamondABI,
927
+ functionName: "withdraw",
928
+ args: [params.shares],
929
+ });
930
+ }
931
+
932
+ /**
933
+ * Withdraw from stability pool by asset amount
934
+ * @param params Withdraw parameters
935
+ * @returns Transaction hash
936
+ */
937
+ async withdrawAssets(params: WithdrawAssetsParams): Promise<Hash> {
938
+ return this.walletClient.writeContract({
939
+ address: this.diamondAddress,
940
+ abi: DiamondABI,
941
+ functionName: "withdrawAssets",
942
+ args: [params.assets],
943
+ });
944
+ }
945
+
946
+ /**
947
+ * Harvest yield from LSTs
948
+ * @returns Transaction hash
949
+ */
950
+ async harvestYield(): Promise<Hash> {
951
+ return this.walletClient.writeContract({
952
+ address: this.diamondAddress,
953
+ abi: DiamondABI,
954
+ functionName: "harvestYield",
955
+ });
956
+ }
957
+
958
+ // ============ Transaction Helpers ============
959
+
960
+ /**
961
+ * Wait for transaction confirmation
962
+ * @param hash Transaction hash
963
+ * @returns Transaction receipt
964
+ */
965
+ async waitForTransaction(hash: Hash) {
966
+ return this.publicClient.waitForTransactionReceipt({ hash });
967
+ }
968
+ }
969
+
970
+ // ============ Factory Functions ============
971
+
972
+ /**
973
+ * Create a read-only client for BSC mainnet
974
+ */
975
+ export function createAspanReadClient(
976
+ diamondAddress: Address,
977
+ rpcUrl?: string
978
+ ): AspanReadClient {
979
+ return new AspanReadClient({
980
+ diamondAddress,
981
+ chain: bsc,
982
+ rpcUrl,
983
+ });
984
+ }
985
+
986
+ /**
987
+ * Create a full client for BSC mainnet
988
+ */
989
+ export function createAspanClient(
990
+ diamondAddress: Address,
991
+ account: Account,
992
+ rpcUrl?: string
993
+ ): AspanClient {
994
+ return new AspanClient({
995
+ diamondAddress,
996
+ account,
997
+ chain: bsc,
998
+ rpcUrl,
999
+ });
1000
+ }
1001
+
1002
+ /**
1003
+ * Create a read-only client for BSC testnet
1004
+ */
1005
+ export function createAspanTestnetReadClient(
1006
+ diamondAddress: Address,
1007
+ rpcUrl?: string
1008
+ ): AspanReadClient {
1009
+ return new AspanReadClient({
1010
+ diamondAddress,
1011
+ chain: bscTestnet,
1012
+ rpcUrl,
1013
+ });
1014
+ }
1015
+
1016
+ /**
1017
+ * Create a full client for BSC testnet
1018
+ */
1019
+ export function createAspanTestnetClient(
1020
+ diamondAddress: Address,
1021
+ account: Account,
1022
+ rpcUrl?: string
1023
+ ): AspanClient {
1024
+ return new AspanClient({
1025
+ diamondAddress,
1026
+ account,
1027
+ chain: bscTestnet,
1028
+ rpcUrl,
1029
+ });
1030
+ }