@kasufinance/kasu-sdk 2.8.0 → 2.10.0
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 +120 -9
- package/dist/bundle.cjs.js +2068 -478
- package/dist/bundle.esm.js +2030 -480
- package/dist/domain/batch-proof.d.ts +210 -0
- package/dist/domain/batch-proof.js +384 -0
- package/dist/domain/batch-proof.js.map +1 -0
- package/dist/domain/explorer-urls.d.ts +33 -0
- package/dist/domain/explorer-urls.js +38 -0
- package/dist/domain/explorer-urls.js.map +1 -0
- package/dist/domain/fixed-term.d.ts +74 -0
- package/dist/domain/fixed-term.js +86 -0
- package/dist/domain/fixed-term.js.map +1 -0
- package/dist/domain/index.d.ts +12 -2
- package/dist/domain/index.js +7 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/partners.d.ts +29 -0
- package/dist/domain/partners.js +20 -0
- package/dist/domain/partners.js.map +1 -1
- package/dist/domain/requests.d.ts +24 -0
- package/dist/domain/requests.js +33 -0
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/sponsored-calls.d.ts +74 -0
- package/dist/domain/sponsored-calls.js +156 -0
- package/dist/domain/sponsored-calls.js.map +1 -0
- package/dist/domain/yields.d.ts +121 -0
- package/dist/domain/yields.js +151 -0
- package/dist/domain/yields.js.map +1 -0
- package/dist/facade/calls.d.ts +59 -0
- package/dist/facade/calls.js +89 -0
- package/dist/facade/calls.js.map +1 -0
- package/dist/facade/chain-configs.js +5 -0
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/flows.d.ts +24 -3
- package/dist/facade/flows.js +22 -2
- package/dist/facade/flows.js.map +1 -1
- package/dist/facade/index.d.ts +3 -1
- package/dist/facade/index.js +1 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +8 -2
- package/dist/facade/kasu.js +17 -5
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/strategies.d.ts +43 -2
- package/dist/facade/strategies.js +80 -4
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +18 -0
- package/dist/flows/batch-errors.d.ts +74 -0
- package/dist/flows/batch-errors.js +83 -0
- package/dist/flows/batch-errors.js.map +1 -0
- package/dist/flows/batch-submission.d.ts +223 -0
- package/dist/flows/batch-submission.js +321 -0
- package/dist/flows/batch-submission.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +13 -20
- package/dist/flows/deposit-flow.js +14 -25
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/flows/index.d.ts +6 -2
- package/dist/flows/index.js +4 -2
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/withdraw-flow.d.ts +95 -4
- package/dist/flows/withdraw-flow.js +107 -2
- package/dist/flows/withdraw-flow.js.map +1 -1
- package/dist/services/DataService/queries.js +1 -0
- package/dist/services/DataService/queries.js.map +1 -1
- package/dist/services/UserLending/user-lending.d.ts +13 -0
- package/dist/services/UserLending/user-lending.js +25 -4
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/batch-proof.ts +576 -0
- package/src/domain/explorer-urls.ts +74 -0
- package/src/domain/fixed-term.ts +115 -0
- package/src/domain/index.ts +56 -0
- package/src/domain/partners.ts +35 -0
- package/src/domain/requests.ts +37 -0
- package/src/domain/sponsored-calls.ts +213 -0
- package/src/domain/yields.ts +189 -0
- package/src/facade/calls.ts +135 -0
- package/src/facade/chain-configs.ts +5 -0
- package/src/facade/flows.ts +23 -2
- package/src/facade/index.ts +3 -0
- package/src/facade/kasu.ts +31 -2
- package/src/facade/strategies.ts +98 -7
- package/src/facade/types.ts +19 -0
- package/src/flows/batch-errors.ts +118 -0
- package/src/flows/batch-submission.ts +511 -0
- package/src/flows/deposit-flow.ts +27 -33
- package/src/flows/index.ts +33 -2
- package/src/flows/withdraw-flow.ts +219 -10
- package/src/services/DataService/queries.ts +1 -0
- package/src/services/UserLending/user-lending.ts +38 -10
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward-looking interest on a lender's CURRENT balances — the figures a
|
|
3
|
+
* portfolio shows as "weekly", "daily" and the moving part of "lifetime"
|
|
4
|
+
* interest, and the "≈ $Y a year" line beside a lend amount.
|
|
5
|
+
*
|
|
6
|
+
* Every figure is NET of the platform performance fee, the way the chain
|
|
7
|
+
* credits it: `LendingPool._applyTrancheInterest` mints the lender
|
|
8
|
+
* `interest × (1 − performanceFee)` at each cycle close, and only that net
|
|
9
|
+
* amount ever reaches the balance. A gross projection would out-run what the
|
|
10
|
+
* next close actually credits.
|
|
11
|
+
*
|
|
12
|
+
* Units: balances and results are display units of the deployment's stable
|
|
13
|
+
* asset (not base units); rates are 0..1 fractions; `feePercent` is a
|
|
14
|
+
* PERCENTAGE IN 0..100 — see `netEffectiveApy`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Seconds in one epoch's nominal week. The per-epoch rate covers exactly this
|
|
19
|
+
* window, and the accrual spreads an epoch's interest across it.
|
|
20
|
+
*/
|
|
21
|
+
export const SECONDS_PER_WEEK = 604_800;
|
|
22
|
+
|
|
23
|
+
/** One slice of a position that earns at a single rate. */
|
|
24
|
+
export interface InterestSlice {
|
|
25
|
+
/** The lender's current balance in this slice, in stable display units. */
|
|
26
|
+
balance: number;
|
|
27
|
+
/**
|
|
28
|
+
* The RAW per-epoch rate the chain applies to this slice at each close,
|
|
29
|
+
* as a 0..1 fraction (`0.00285` ≈ 0.285% an epoch) — the subgraph's
|
|
30
|
+
* `TrancheData.interestRate` or a fixed-term config's
|
|
31
|
+
* `epochInterestRate`. Never derived from an APY.
|
|
32
|
+
*
|
|
33
|
+
* NON-FINITE means UNKNOWN, not zero. A zero is a genuinely zero
|
|
34
|
+
* configured rate and contributes a real 0; the two must stay
|
|
35
|
+
* distinguishable.
|
|
36
|
+
*/
|
|
37
|
+
epochRate: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One pool's contribution to the current cycle's accrual. */
|
|
41
|
+
export interface PoolAccrual {
|
|
42
|
+
/** That pool's projected net interest for one epoch (`weeklyInterest`). */
|
|
43
|
+
weeklyInterest: number;
|
|
44
|
+
/** Unix SECONDS at which THAT pool last cleared a cycle. */
|
|
45
|
+
clearingTimestamp: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isUsableFeePercent(feePercent: number | null): feePercent is number {
|
|
49
|
+
return (
|
|
50
|
+
feePercent !== null &&
|
|
51
|
+
Number.isFinite(feePercent) &&
|
|
52
|
+
feePercent >= 0 &&
|
|
53
|
+
feePercent <= 100
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Interest the slices accrue over ONE epoch, net of the fee.
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
* weekly = Σ(slice.balance × slice.epochRate) × (1 − feePercent / 100)
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* **The rate is the raw per-epoch one, never the APY.** An APY is that rate
|
|
65
|
+
* already compounded over `EPOCHS_IN_YEAR`; dividing it by 365 charges a day
|
|
66
|
+
* of compounded annual growth to a single day and overstates it.
|
|
67
|
+
*
|
|
68
|
+
* FAILS CLOSED, returning `null` rather than a wrong number:
|
|
69
|
+
* - a `null`, non-finite or out-of-domain fee — a fee-less projection dressed
|
|
70
|
+
* up as the lender's net earnings is the failure this guards;
|
|
71
|
+
* - any slice with a POSITIVE balance and a non-finite rate — skipping it
|
|
72
|
+
* would return a silently partial total, quietly missing that slice.
|
|
73
|
+
*
|
|
74
|
+
* Non-finite and non-positive BALANCES are skipped: a slice with nothing in it
|
|
75
|
+
* earns nothing whatever its rate, so it cannot hide earnings. A portfolio
|
|
76
|
+
* with nothing lent is therefore a legitimate `0`, never `null`.
|
|
77
|
+
*/
|
|
78
|
+
export function weeklyInterest(
|
|
79
|
+
slices: readonly InterestSlice[],
|
|
80
|
+
feePercent: number | null,
|
|
81
|
+
): number | null {
|
|
82
|
+
if (!isUsableFeePercent(feePercent)) return null;
|
|
83
|
+
let gross = 0;
|
|
84
|
+
for (const slice of slices) {
|
|
85
|
+
if (!Number.isFinite(slice.balance) || slice.balance <= 0) continue;
|
|
86
|
+
if (!Number.isFinite(slice.epochRate)) return null;
|
|
87
|
+
gross += slice.balance * slice.epochRate;
|
|
88
|
+
}
|
|
89
|
+
return gross * (1 - feePercent / 100);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A day's share of the epoch's net interest: a seventh of it, NOT a
|
|
94
|
+
* de-compounded daily rate. The chain mints interest once per epoch, so
|
|
95
|
+
* nothing compounds within the cycle and the accrual is linear across it.
|
|
96
|
+
*
|
|
97
|
+
* Fails closed with its input: `null` or non-finite → `null`.
|
|
98
|
+
*/
|
|
99
|
+
export function dailyInterest(weekly: number | null): number | null {
|
|
100
|
+
if (weekly === null || !Number.isFinite(weekly)) return null;
|
|
101
|
+
return weekly / 7;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Interest accrued since each pool's OWN last cycle close — the moving part
|
|
106
|
+
* of lifetime interest (lifetime = settled on chain + this).
|
|
107
|
+
*
|
|
108
|
+
* ```
|
|
109
|
+
* accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* **Per pool, because pools clear independently.** One shared anchor accrues
|
|
113
|
+
* a stale pool's share from a close it never had, and when another pool
|
|
114
|
+
* clears it drops the total by the stale pool's whole weekly share. Anchored
|
|
115
|
+
* per pool, the total is monotonic across any single pool's clearing: that
|
|
116
|
+
* pool swaps its accrued share for a capitalised one, and no other term moves.
|
|
117
|
+
*
|
|
118
|
+
* **The one-epoch cap is load-bearing.** Clearings are not exactly weekly,
|
|
119
|
+
* and `weeklyInterest` is per EPOCH, so extrapolating past seven days would
|
|
120
|
+
* promise interest the close will not credit; a late clearing under-reports
|
|
121
|
+
* instead. The low clamp makes a close ahead of the local clock zero, never
|
|
122
|
+
* negative.
|
|
123
|
+
*
|
|
124
|
+
* A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
|
|
125
|
+
* nothing. Callers fail closed upstream by not offering an accrual for a pool
|
|
126
|
+
* whose weekly interest or anchor is unknown.
|
|
127
|
+
*/
|
|
128
|
+
export function accruedInterest(
|
|
129
|
+
accruals: readonly PoolAccrual[],
|
|
130
|
+
nowSeconds: number,
|
|
131
|
+
): number {
|
|
132
|
+
if (!Number.isFinite(nowSeconds)) return 0;
|
|
133
|
+
let accrued = 0;
|
|
134
|
+
for (const { weeklyInterest: weekly, clearingTimestamp } of accruals) {
|
|
135
|
+
if (!Number.isFinite(weekly) || !Number.isFinite(clearingTimestamp)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const elapsed = Math.min(
|
|
139
|
+
Math.max(nowSeconds - clearingTimestamp, 0),
|
|
140
|
+
SECONDS_PER_WEEK,
|
|
141
|
+
);
|
|
142
|
+
accrued += (weekly * elapsed) / SECONDS_PER_WEEK;
|
|
143
|
+
}
|
|
144
|
+
return accrued;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
|
|
149
|
+
* Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
|
|
150
|
+
* earnings, so the fee must already be off the rate.
|
|
151
|
+
*
|
|
152
|
+
* FAILS CLOSED: a non-finite or negative amount or rate returns `NaN`, which
|
|
153
|
+
* the consumer renders as "no figure" — exactly what `netEffectiveApy`
|
|
154
|
+
* returns when the fee is unknown, so that state carries straight through.
|
|
155
|
+
*
|
|
156
|
+
* @param amount display units of the stable asset.
|
|
157
|
+
* @param netApy the net effective annual rate as a 0..1 fraction.
|
|
158
|
+
*/
|
|
159
|
+
export function projectedAnnualEarnings(
|
|
160
|
+
amount: number,
|
|
161
|
+
netApy: number,
|
|
162
|
+
): number {
|
|
163
|
+
if (!Number.isFinite(amount) || amount < 0) return NaN;
|
|
164
|
+
if (!Number.isFinite(netApy) || netApy < 0) return NaN;
|
|
165
|
+
return amount * netApy;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Balance-weighted average APY across slices, skipping non-finite and
|
|
170
|
+
* non-positive balances and non-finite rates; `0` when nothing carries a
|
|
171
|
+
* positive balance. Whatever basis goes in comes out — feed it NET rates for
|
|
172
|
+
* an average effective (net) rate.
|
|
173
|
+
*/
|
|
174
|
+
export function weightedAverageApy(
|
|
175
|
+
slices: readonly { balance: number; apy: number }[],
|
|
176
|
+
): number {
|
|
177
|
+
let totalBalance = 0;
|
|
178
|
+
let weighted = 0;
|
|
179
|
+
for (const slice of slices) {
|
|
180
|
+
if (!Number.isFinite(slice.balance) || !Number.isFinite(slice.apy)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (slice.balance <= 0) continue;
|
|
184
|
+
totalBalance += slice.balance;
|
|
185
|
+
weighted += slice.balance * slice.apy;
|
|
186
|
+
}
|
|
187
|
+
if (totalBalance <= 0) return 0;
|
|
188
|
+
return weighted / totalBalance;
|
|
189
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { BigNumber, BigNumberish } from 'ethers';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
encodeCancelDepositCall,
|
|
5
|
+
encodeCancelWithdrawalCall,
|
|
6
|
+
encodeRequestWithdrawalCall,
|
|
7
|
+
encodeWithdrawAtExpiryCall,
|
|
8
|
+
SponsoredCall,
|
|
9
|
+
SponsoredCallInputError,
|
|
10
|
+
} from '../domain/sponsored-calls';
|
|
11
|
+
import { UserLending } from '../services/UserLending/user-lending';
|
|
12
|
+
|
|
13
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
14
|
+
|
|
15
|
+
/** What `kasu.calls.requestWithdrawal` takes — `WithdrawFlowInput`'s shape. */
|
|
16
|
+
export interface RequestWithdrawalCallParams {
|
|
17
|
+
poolId: string;
|
|
18
|
+
trancheId: string;
|
|
19
|
+
/**
|
|
20
|
+
* Base units of the stable asset, or `'max'` for every share the lender
|
|
21
|
+
* holds in the tranche. Same code as `WithdrawFlowInput.amount`.
|
|
22
|
+
*/
|
|
23
|
+
amount: BigNumber | 'max';
|
|
24
|
+
/** The lender — whose shares `'max'` reads. The relay sends as them. */
|
|
25
|
+
userAddress: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Encoded, ready-to-sponsor `LendingPoolManager` calls — read, then encode,
|
|
30
|
+
* NEVER send. Each returns `{ kind, call }`: post `kind` and `[call]` to a
|
|
31
|
+
* sponsored relay (or `submitSponsoredCall`), unchanged.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const { kind, call } = kasu.calls.cancelDepositRequest(poolId, dNftId);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* `call.to` is always THIS chain's `LendingPoolManager`, from the chain
|
|
38
|
+
* config — the same address the deposit flow approves and calls — so no app
|
|
39
|
+
* supplies the one argument whose wrong value the relay refuses outright.
|
|
40
|
+
*
|
|
41
|
+
* Works on a read-only instance: the only chain access is the share read a
|
|
42
|
+
* withdrawal needs, and nothing is signed or sent here.
|
|
43
|
+
*/
|
|
44
|
+
export class CallsFacade {
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly _userLending: UserLending,
|
|
47
|
+
private readonly _lendingPoolManager: string,
|
|
48
|
+
) {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `requestWithdrawal(pool, tranche, shares)` for an asset amount or
|
|
52
|
+
* `'max'`, converted into shares exactly as the direct path does it
|
|
53
|
+
* (`UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax`): the
|
|
54
|
+
* tranche's `convertToShares(amount)`, or the lender's whole
|
|
55
|
+
* `balanceOf(userAddress)`, read now.
|
|
56
|
+
*
|
|
57
|
+
* Rejects with `SponsoredCallInputError` — before anything is sent — for
|
|
58
|
+
* a non-positive amount, a malformed address, or a conversion that comes
|
|
59
|
+
* to ZERO shares (an empty position, or dust below one share), which the
|
|
60
|
+
* contract would revert and the relay refuses.
|
|
61
|
+
*/
|
|
62
|
+
async requestWithdrawal(
|
|
63
|
+
params: RequestWithdrawalCallParams,
|
|
64
|
+
): Promise<SponsoredCall> {
|
|
65
|
+
const { poolId, trancheId, amount, userAddress } = params;
|
|
66
|
+
if (typeof userAddress !== 'string' || !ADDRESS_RE.test(userAddress)) {
|
|
67
|
+
throw new SponsoredCallInputError('user');
|
|
68
|
+
}
|
|
69
|
+
if (typeof poolId !== 'string' || !ADDRESS_RE.test(poolId)) {
|
|
70
|
+
throw new SponsoredCallInputError('pool');
|
|
71
|
+
}
|
|
72
|
+
if (typeof trancheId !== 'string' || !ADDRESS_RE.test(trancheId)) {
|
|
73
|
+
throw new SponsoredCallInputError('tranche');
|
|
74
|
+
}
|
|
75
|
+
let shares: BigNumber;
|
|
76
|
+
if (amount === 'max') {
|
|
77
|
+
// Lowercased, as `WithdrawFlow` hands `withdrawMax` the address.
|
|
78
|
+
shares = await this._userLending.getWithdrawalSharesMax(
|
|
79
|
+
trancheId,
|
|
80
|
+
userAddress.toLowerCase(),
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
if (!BigNumber.isBigNumber(amount) || amount.lte(0)) {
|
|
84
|
+
throw new SponsoredCallInputError('non-positive-amount');
|
|
85
|
+
}
|
|
86
|
+
shares = await this._userLending.getWithdrawalSharesForAssets(
|
|
87
|
+
trancheId,
|
|
88
|
+
amount,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return encodeRequestWithdrawalCall(
|
|
92
|
+
this._lendingPoolManager,
|
|
93
|
+
poolId,
|
|
94
|
+
trancheId,
|
|
95
|
+
shares,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `cancelDepositRequest(pool, dNftId)` — the pending deposit's NFT id. */
|
|
100
|
+
cancelDepositRequest(poolId: string, dNftId: BigNumberish): SponsoredCall {
|
|
101
|
+
return encodeCancelDepositCall(
|
|
102
|
+
this._lendingPoolManager,
|
|
103
|
+
poolId,
|
|
104
|
+
dNftId,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `cancelWithdrawalRequest(pool, wNftId)` — the pending withdrawal's NFT id. */
|
|
109
|
+
cancelWithdrawalRequest(
|
|
110
|
+
poolId: string,
|
|
111
|
+
wNftId: BigNumberish,
|
|
112
|
+
): SponsoredCall {
|
|
113
|
+
return encodeCancelWithdrawalCall(
|
|
114
|
+
this._lendingPoolManager,
|
|
115
|
+
poolId,
|
|
116
|
+
wNftId,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `requestFixedTermDepositWithdrawal(pool, fixedTermDepositId)` — what
|
|
122
|
+
* `UserLending.withdrawAtExpiry(pool, lockId)` sends: the pool and the
|
|
123
|
+
* LOCK id, no tranche.
|
|
124
|
+
*/
|
|
125
|
+
withdrawAtExpiry(
|
|
126
|
+
poolId: string,
|
|
127
|
+
fixedTermDepositId: BigNumberish,
|
|
128
|
+
): SponsoredCall {
|
|
129
|
+
return encodeWithdrawAtExpiryCall(
|
|
130
|
+
this._lendingPoolManager,
|
|
131
|
+
poolId,
|
|
132
|
+
fixedTermDepositId,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -59,6 +59,7 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
|
|
|
59
59
|
'https://base-rpc.publicnode.com',
|
|
60
60
|
'https://mainnet.base.org',
|
|
61
61
|
],
|
|
62
|
+
blockExplorerUrl: 'https://basescan.org',
|
|
62
63
|
},
|
|
63
64
|
|
|
64
65
|
xdc: {
|
|
@@ -99,6 +100,7 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
|
|
|
99
100
|
currencyCode: 'AUD',
|
|
100
101
|
},
|
|
101
102
|
rpcUrls: XDC_RPC_URLS,
|
|
103
|
+
blockExplorerUrl: 'https://xdcscan.com',
|
|
102
104
|
},
|
|
103
105
|
|
|
104
106
|
'xdc-usdc': {
|
|
@@ -132,6 +134,7 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
|
|
|
132
134
|
currencyCode: 'USD',
|
|
133
135
|
},
|
|
134
136
|
rpcUrls: XDC_RPC_URLS,
|
|
137
|
+
blockExplorerUrl: 'https://xdcscan.com',
|
|
135
138
|
},
|
|
136
139
|
|
|
137
140
|
plume: {
|
|
@@ -174,6 +177,8 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
|
|
|
174
177
|
// pointing at an endpoint nobody maintains. Pass your own
|
|
175
178
|
// `signerOrProvider` to read the frozen history.
|
|
176
179
|
rpcUrls: [],
|
|
180
|
+
// The explorer still serves the frozen history.
|
|
181
|
+
blockExplorerUrl: 'https://explorer.plume.org',
|
|
177
182
|
retired: true,
|
|
178
183
|
},
|
|
179
184
|
};
|
package/src/facade/flows.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
WithdrawPorts,
|
|
11
11
|
} from '../flows';
|
|
12
12
|
|
|
13
|
+
import { CallsFacade } from './calls';
|
|
13
14
|
import { DepositsFacade } from './deposits';
|
|
14
15
|
import { READ_ONLY_MESSAGE } from './read-only';
|
|
15
16
|
import { KycParams } from './types';
|
|
@@ -30,8 +31,9 @@ export type DepositFlowPortOverrides = Pick<
|
|
|
30
31
|
Partial<DepositPorts>;
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
* Every withdraw port has an SDK default except `getKycSignature
|
|
34
|
-
* no default at all
|
|
34
|
+
* Every withdraw port has an SDK default except `getKycSignature` and
|
|
35
|
+
* `submitCall`, which have no default at all: supplying `getKycSignature`
|
|
36
|
+
* turns the KYC pre-check on, supplying `submitCall` the sponsored path.
|
|
35
37
|
*/
|
|
36
38
|
export type WithdrawFlowPortOverrides = Partial<WithdrawPorts>;
|
|
37
39
|
|
|
@@ -81,6 +83,13 @@ export class FlowsFacade {
|
|
|
81
83
|
* in which case a run never batches: it does not guess a token.
|
|
82
84
|
*/
|
|
83
85
|
private readonly _stableAsset?: string,
|
|
86
|
+
/**
|
|
87
|
+
* This instance's call encoders — the sponsored withdraw path's
|
|
88
|
+
* default `encodeWithdrawal`. Optional so a hand-built facade keeps
|
|
89
|
+
* constructing; without it a sponsored run fails (`failed`, nothing
|
|
90
|
+
* sent) unless the consumer passes `encodeWithdrawal` itself.
|
|
91
|
+
*/
|
|
92
|
+
private readonly _calls?: CallsFacade,
|
|
84
93
|
) {}
|
|
85
94
|
|
|
86
95
|
/**
|
|
@@ -148,6 +157,9 @@ export class FlowsFacade {
|
|
|
148
157
|
/**
|
|
149
158
|
* A withdrawal pipeline. Both write ports and `buildKycParams` default to
|
|
150
159
|
* this instance; supplying `getKycSignature` turns the KYC pre-check on.
|
|
160
|
+
* Supplying `submitCall` turns the sponsored path on, and its encoder
|
|
161
|
+
* defaults to `kasu.calls.requestWithdrawal` on this instance — which
|
|
162
|
+
* reads, never writes, so it works on a read-only instance too.
|
|
151
163
|
* Defaults are applied per key, for the reason `deposit()` gives.
|
|
152
164
|
*/
|
|
153
165
|
withdraw(ports: WithdrawFlowPortOverrides = {}): WithdrawFlow {
|
|
@@ -173,9 +185,18 @@ export class FlowsFacade {
|
|
|
173
185
|
userAddress: string,
|
|
174
186
|
): Promise<WaitableTransaction> =>
|
|
175
187
|
this._deposits.withdrawMax(poolId, trancheId, userAddress)),
|
|
188
|
+
submitCall: ports.submitCall,
|
|
189
|
+
encodeWithdrawal: ports.encodeWithdrawal ?? this._defaultEncoder(),
|
|
176
190
|
});
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
/** `kasu.calls.requestWithdrawal`, when this facade was given the encoders. */
|
|
194
|
+
private _defaultEncoder(): WithdrawPorts['encodeWithdrawal'] {
|
|
195
|
+
const calls = this._calls;
|
|
196
|
+
if (!calls) return undefined;
|
|
197
|
+
return (params) => calls.requestWithdrawal(params);
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
private _assertWritable(): void {
|
|
180
201
|
if (this._isReadOnly) {
|
|
181
202
|
throw new Error(READ_ONLY_MESSAGE);
|
package/src/facade/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { StrategiesFacade } from './strategies';
|
|
|
6
6
|
export { DepositsFacade } from './deposits';
|
|
7
7
|
export { PortfolioFacade } from './user-portfolio';
|
|
8
8
|
export { FlowsFacade } from './flows';
|
|
9
|
+
export { CallsFacade } from './calls';
|
|
10
|
+
export type { RequestWithdrawalCallParams } from './calls';
|
|
9
11
|
export type {
|
|
10
12
|
DepositFlowPortOverrides,
|
|
11
13
|
WithdrawFlowPortOverrides,
|
|
@@ -35,4 +37,5 @@ export type {
|
|
|
35
37
|
KycParams,
|
|
36
38
|
UserPositions,
|
|
37
39
|
PlatformStats,
|
|
40
|
+
StrategyRiskPerformance,
|
|
38
41
|
} from './types';
|
package/src/facade/kasu.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { IERC20MetadataAbi } from '../contracts';
|
|
|
6
6
|
import { KasuSdk } from '../kasu-sdk';
|
|
7
7
|
import { SdkConfig, SdkConfigOptions } from '../sdk-config';
|
|
8
8
|
|
|
9
|
+
import { CallsFacade } from './calls';
|
|
9
10
|
import { CHAIN_CONFIGS } from './chain-configs';
|
|
10
11
|
import { DepositsFacade } from './deposits';
|
|
11
12
|
import { FlowsFacade } from './flows';
|
|
@@ -21,8 +22,8 @@ import { PortfolioFacade } from './user-portfolio';
|
|
|
21
22
|
/**
|
|
22
23
|
* High-level entry point for external integrators.
|
|
23
24
|
*
|
|
24
|
-
* Provides
|
|
25
|
-
* `flows` — and exposes the underlying `KasuSdk` services via `.services` for
|
|
25
|
+
* Provides five domain facades — `strategies`, `deposits`, `portfolio`,
|
|
26
|
+
* `flows`, `calls` — and exposes the underlying `KasuSdk` services via `.services` for
|
|
26
27
|
* power-users.
|
|
27
28
|
*
|
|
28
29
|
* ```ts
|
|
@@ -49,6 +50,11 @@ export class Kasu {
|
|
|
49
50
|
public readonly portfolio: PortfolioFacade;
|
|
50
51
|
/** Headless deposit / withdraw state machines. */
|
|
51
52
|
public readonly flows: FlowsFacade;
|
|
53
|
+
/**
|
|
54
|
+
* Encoded `LendingPoolManager` calls for a sponsored relay — withdraw,
|
|
55
|
+
* cancel, withdraw at expiry. Read, then encode; never sends.
|
|
56
|
+
*/
|
|
57
|
+
public readonly calls: CallsFacade;
|
|
52
58
|
|
|
53
59
|
private readonly _sdk: KasuSdk;
|
|
54
60
|
private readonly _chainConfig: ChainConfigEntry;
|
|
@@ -74,6 +80,10 @@ export class Kasu {
|
|
|
74
80
|
this.strategies = new StrategiesFacade(
|
|
75
81
|
sdk.DataService,
|
|
76
82
|
sdk.UserLending,
|
|
83
|
+
normaliseMapping(
|
|
84
|
+
configOverrides.poolMetadataMapping ??
|
|
85
|
+
chainConfig.poolMetadataMapping,
|
|
86
|
+
),
|
|
77
87
|
);
|
|
78
88
|
|
|
79
89
|
// Derived ONCE and handed to every facade that needs it. Each of them
|
|
@@ -93,12 +103,20 @@ export class Kasu {
|
|
|
93
103
|
sdk.Portfolio,
|
|
94
104
|
);
|
|
95
105
|
|
|
106
|
+
// The same `LendingPoolManager` the deposit flow approves and calls:
|
|
107
|
+
// the chain config's, which is what a relay allow-lists `to` against.
|
|
108
|
+
this.calls = new CallsFacade(
|
|
109
|
+
sdk.UserLending,
|
|
110
|
+
chainConfig.contracts.LendingPoolManager,
|
|
111
|
+
);
|
|
112
|
+
|
|
96
113
|
this.flows = new FlowsFacade(
|
|
97
114
|
this.deposits,
|
|
98
115
|
() => erc20Of(chainConfig, signerOrProvider),
|
|
99
116
|
isReadOnly,
|
|
100
117
|
chainConfig.contracts.LendingPoolManager,
|
|
101
118
|
stableAssetOf(chainConfig)?.address,
|
|
119
|
+
this.calls,
|
|
102
120
|
);
|
|
103
121
|
}
|
|
104
122
|
|
|
@@ -326,3 +344,14 @@ function resolveChainConfig(
|
|
|
326
344
|
}
|
|
327
345
|
return chain;
|
|
328
346
|
}
|
|
347
|
+
|
|
348
|
+
/** Lowercase both sides, so a lookup by any casing of a pool id matches. */
|
|
349
|
+
function normaliseMapping(
|
|
350
|
+
mapping: Record<string, string> | undefined,
|
|
351
|
+
): Record<string, string> {
|
|
352
|
+
const out: Record<string, string> = {};
|
|
353
|
+
for (const [from, to] of Object.entries(mapping ?? {})) {
|
|
354
|
+
out[from.toLowerCase()] = to.toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
package/src/facade/strategies.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { selectVisiblePools } from '../domain/pools';
|
|
2
2
|
import { DataService } from '../services/DataService/data-service';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PoolDelegateProfileAndHistory,
|
|
5
|
+
PoolOverview,
|
|
6
|
+
} from '../services/DataService/types';
|
|
4
7
|
import { UserLending } from '../services/UserLending/user-lending';
|
|
5
8
|
|
|
6
9
|
import {
|
|
@@ -8,6 +11,7 @@ import {
|
|
|
8
11
|
FixedTermOption,
|
|
9
12
|
PlatformStats,
|
|
10
13
|
Strategy,
|
|
14
|
+
StrategyRiskPerformance,
|
|
11
15
|
StrategyTranche,
|
|
12
16
|
} from './types';
|
|
13
17
|
|
|
@@ -20,8 +24,82 @@ export class StrategiesFacade {
|
|
|
20
24
|
constructor(
|
|
21
25
|
private _dataService: DataService,
|
|
22
26
|
private _userLending: UserLending,
|
|
27
|
+
/**
|
|
28
|
+
* Chain pool id → the pool id its CMS record is keyed by (XDC pools
|
|
29
|
+
* share their Base counterpart's editorial record). Lowercase keys.
|
|
30
|
+
*/
|
|
31
|
+
private _poolMetadataMapping: Record<string, string> = {},
|
|
23
32
|
) {}
|
|
24
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The pool's loss rate and first-loss capital, from its CMS
|
|
36
|
+
* risk-management record. `null` when the pool has no record.
|
|
37
|
+
*
|
|
38
|
+
* Each figure is a NUMBER or `null` — never a guess. The CMS returns
|
|
39
|
+
* "numeric" fields as a number, a numeric string, an empty string or
|
|
40
|
+
* null, and an empty string must not become a confident `0`: anything that
|
|
41
|
+
* is not a finite number reads as `null` (render "—"). `poolLossRate` is a
|
|
42
|
+
* 0..1 fraction.
|
|
43
|
+
*
|
|
44
|
+
* Requires `directusUrl`; without it the call rejects with
|
|
45
|
+
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
|
|
46
|
+
*/
|
|
47
|
+
async getRiskPerformance(
|
|
48
|
+
poolId: string,
|
|
49
|
+
): Promise<StrategyRiskPerformance | null> {
|
|
50
|
+
const metadataId = this._metadataIdOf(poolId);
|
|
51
|
+
const records = await this._dataService.getRiskManagement();
|
|
52
|
+
const record = records.find(
|
|
53
|
+
(r) => String(r.poolIdFK).toLowerCase() === metadataId,
|
|
54
|
+
);
|
|
55
|
+
if (!record) return null;
|
|
56
|
+
const performance = record.riskPerformance as
|
|
57
|
+
| Partial<Record<'poolLossRate' | 'firstLossCapital', unknown>>
|
|
58
|
+
| null
|
|
59
|
+
| undefined;
|
|
60
|
+
return {
|
|
61
|
+
poolLossRate: finiteOrNull(performance?.poolLossRate),
|
|
62
|
+
firstLossCapital: finiteOrNull(performance?.firstLossCapital),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The credit originator's profile and lending history for a pool — the
|
|
68
|
+
* CMS record whose `otherKASUPools` lists it. `null` when none does.
|
|
69
|
+
*
|
|
70
|
+
* Matched on the chain pool id, then on its metadata id, case-
|
|
71
|
+
* insensitively. The record is returned as the service maps it; its
|
|
72
|
+
* numeric fields are CMS-authored, so read them defensively.
|
|
73
|
+
*
|
|
74
|
+
* The service filters `otherKASUPools` through the CURRENT chain's
|
|
75
|
+
* subgraph with the CMS record's (Base-keyed) ids, so on an XDC
|
|
76
|
+
* deployment every profile lists no pools and this resolves `null` —
|
|
77
|
+
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
78
|
+
* candidate becomes live once the service resolves the mapping.
|
|
79
|
+
*/
|
|
80
|
+
async getOriginator(
|
|
81
|
+
poolId: string,
|
|
82
|
+
): Promise<PoolDelegateProfileAndHistory | null> {
|
|
83
|
+
const candidates = new Set([
|
|
84
|
+
poolId.toLowerCase(),
|
|
85
|
+
this._metadataIdOf(poolId),
|
|
86
|
+
]);
|
|
87
|
+
const profiles =
|
|
88
|
+
await this._dataService.getPoolDelegateProfileAndHistory();
|
|
89
|
+
return (
|
|
90
|
+
profiles.find((profile) =>
|
|
91
|
+
profile.otherKASUPools.some((pool) =>
|
|
92
|
+
candidates.has(String(pool.id).toLowerCase()),
|
|
93
|
+
),
|
|
94
|
+
) ?? null
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private _metadataIdOf(poolId: string): string {
|
|
99
|
+
const lower = poolId.toLowerCase();
|
|
100
|
+
return (this._poolMetadataMapping[lower] ?? lower).toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
25
103
|
/**
|
|
26
104
|
* Fetch all active lending strategies.
|
|
27
105
|
*
|
|
@@ -96,13 +174,12 @@ export class StrategiesFacade {
|
|
|
96
174
|
calculateDepositLimits(tranche: StrategyTranche): DepositLimits {
|
|
97
175
|
const raw = tranche._raw;
|
|
98
176
|
const min =
|
|
99
|
-
parseFloat(raw.minimumDeposit) < 1
|
|
100
|
-
? '1'
|
|
101
|
-
: raw.minimumDeposit;
|
|
177
|
+
parseFloat(raw.minimumDeposit) < 1 ? '1' : raw.minimumDeposit;
|
|
102
178
|
|
|
103
|
-
const max =
|
|
104
|
-
|
|
105
|
-
|
|
179
|
+
const max =
|
|
180
|
+
parseFloat(raw.maximumDeposit) > parseFloat(raw.poolCapacity)
|
|
181
|
+
? raw.poolCapacity
|
|
182
|
+
: raw.maximumDeposit;
|
|
106
183
|
|
|
107
184
|
return {
|
|
108
185
|
min,
|
|
@@ -153,3 +230,17 @@ export class StrategiesFacade {
|
|
|
153
230
|
};
|
|
154
231
|
}
|
|
155
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A CMS "numeric" field as a finite number, or `null`. An empty or blank
|
|
236
|
+
* string is UNAUTHORED, not zero.
|
|
237
|
+
*/
|
|
238
|
+
function finiteOrNull(value: unknown): number | null {
|
|
239
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
240
|
+
if (typeof value === 'string') {
|
|
241
|
+
if (value.trim() === '') return null;
|
|
242
|
+
const numeric = Number(value);
|
|
243
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
package/src/facade/types.ts
CHANGED
|
@@ -66,6 +66,13 @@ export interface ChainConfigEntry {
|
|
|
66
66
|
* Empty on a retired deployment, which has no read-only default.
|
|
67
67
|
*/
|
|
68
68
|
rpcUrls: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Origin of the chain's block explorer, `https://…` with NO trailing
|
|
71
|
+
* slash (`https://basescan.org`). Never append a path to it by hand: build
|
|
72
|
+
* links with `explorerTxUrl` / `explorerAddressUrl`, which validate the
|
|
73
|
+
* hash or address first, so no app turns an unchecked string into a URL.
|
|
74
|
+
*/
|
|
75
|
+
blockExplorerUrl: string;
|
|
69
76
|
/**
|
|
70
77
|
* True for a wound-down deployment kept only as frozen history. It has no
|
|
71
78
|
* default RPC, so a read-only `Kasu.create` on it throws.
|
|
@@ -224,3 +231,15 @@ export interface UserPositions {
|
|
|
224
231
|
// ---------------------------------------------------------------------------
|
|
225
232
|
|
|
226
233
|
export type PlatformStats = LendingTotals;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A strategy's risk figures, from its CMS risk-management record
|
|
237
|
+
* (`kasu.strategies.getRiskPerformance`). Each is `null` when unauthored or
|
|
238
|
+
* not a finite number — render "—", never `0`.
|
|
239
|
+
*/
|
|
240
|
+
export interface StrategyRiskPerformance {
|
|
241
|
+
/** Realised loss rate, a 0..1 fraction. */
|
|
242
|
+
poolLossRate: number | null;
|
|
243
|
+
/** First-loss capital, exactly as the CMS record authors it. */
|
|
244
|
+
firstLossCapital: number | null;
|
|
245
|
+
}
|