@kasufinance/kasu-sdk 2.13.0 → 2.13.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/dist/bundle.cjs.js +103 -42
- package/dist/bundle.esm.js +103 -42
- package/dist/domain/requests.d.ts +11 -10
- package/dist/services/UserLending/subgraph-types.d.ts +2 -8
- package/dist/services/UserLending/types.d.ts +10 -0
- package/dist/services/UserLending/user-lending.d.ts +14 -0
- package/package.json +1 -1
- package/src/domain/requests.ts +37 -23
- package/src/services/UserLending/queries.ts +2 -7
- package/src/services/UserLending/subgraph-types.ts +2 -8
- package/src/services/UserLending/types.ts +10 -0
- package/src/services/UserLending/user-lending.ts +98 -40
package/dist/bundle.cjs.js
CHANGED
|
@@ -18015,14 +18015,13 @@ const currentEpochFtdAmountQuery = gql `
|
|
|
18015
18015
|
user: $userId
|
|
18016
18016
|
lendingPool_in: $poolIds
|
|
18017
18017
|
epochId: $currentEpoch
|
|
18018
|
+
type: DepositRequest
|
|
18018
18019
|
}
|
|
18019
18020
|
) {
|
|
18020
18021
|
fixedTermConfigId
|
|
18021
18022
|
amountRequested
|
|
18022
18023
|
tranche {
|
|
18023
18024
|
id
|
|
18024
|
-
balance
|
|
18025
|
-
shares
|
|
18026
18025
|
}
|
|
18027
18026
|
}
|
|
18028
18027
|
}
|
|
@@ -18037,11 +18036,7 @@ const currentFtdBalanceQuery = gql `
|
|
|
18037
18036
|
configId
|
|
18038
18037
|
lendingPoolTranche {
|
|
18039
18038
|
id
|
|
18040
|
-
|
|
18041
|
-
}
|
|
18042
|
-
lendingPool {
|
|
18043
|
-
balance
|
|
18044
|
-
tranches {
|
|
18039
|
+
balance
|
|
18045
18040
|
shares
|
|
18046
18041
|
}
|
|
18047
18042
|
}
|
|
@@ -18049,6 +18044,13 @@ const currentFtdBalanceQuery = gql `
|
|
|
18049
18044
|
}
|
|
18050
18045
|
`;
|
|
18051
18046
|
|
|
18047
|
+
/** Thrown when the subgraph reports a request type with no known unit. */
|
|
18048
|
+
const UNKNOWN_REQUEST_TYPE_MESSAGE = 'UserLending: unknown request type, its amount unit is unknown';
|
|
18049
|
+
const KNOWN_REQUEST_TYPES = [
|
|
18050
|
+
'DepositRequest',
|
|
18051
|
+
'WithdrawalRequest',
|
|
18052
|
+
];
|
|
18053
|
+
const isKnownRequestType = (type) => typeof type === 'string' && KNOWN_REQUEST_TYPES.includes(type);
|
|
18052
18054
|
class UserLending {
|
|
18053
18055
|
constructor(_kasuConfig, signerOrProvider) {
|
|
18054
18056
|
this._kasuConfig = _kasuConfig;
|
|
@@ -18210,6 +18212,14 @@ class UserLending {
|
|
|
18210
18212
|
]);
|
|
18211
18213
|
const retn = [];
|
|
18212
18214
|
for (const userRequest of subgraphResult.userRequests) {
|
|
18215
|
+
// A request type this SDK does not know has no known amount unit
|
|
18216
|
+
// (the schema also declares types no live deployment emits yet).
|
|
18217
|
+
// Skip that ONE row before anything is priced or read on chain,
|
|
18218
|
+
// rather than failing — and blanking — the lender's whole list.
|
|
18219
|
+
if (!isKnownRequestType(userRequest.type)) {
|
|
18220
|
+
console.error(`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${String(userRequest.type)} (request ${userRequest.id}); row skipped`);
|
|
18221
|
+
continue;
|
|
18222
|
+
}
|
|
18213
18223
|
const directusPool = directusResult.find(({ id }) => id === userRequest.lendingPool.id);
|
|
18214
18224
|
const trancheName = trancheNames[userRequest.lendingPool.tranches.length - 1][parseInt(userRequest.tranche.orderId)];
|
|
18215
18225
|
const events = [];
|
|
@@ -18305,9 +18315,7 @@ class UserLending {
|
|
|
18305
18315
|
: 'Withdrawal',
|
|
18306
18316
|
trancheName: trancheName,
|
|
18307
18317
|
trancheId: userRequest.tranche.id,
|
|
18308
|
-
requestedAmount:
|
|
18309
|
-
? userRequest.amountRequested
|
|
18310
|
-
: this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares).toString(),
|
|
18318
|
+
requestedAmount: this.requestedAmountInAssets(userRequest),
|
|
18311
18319
|
acceptedAmount: userRequest.amountAccepted,
|
|
18312
18320
|
rejectedAmount: userRequest.amountRejected,
|
|
18313
18321
|
status: userRequest.status,
|
|
@@ -18340,8 +18348,11 @@ class UserLending {
|
|
|
18340
18348
|
for (const userFTD of currentFtdBalance.userLendingPoolTrancheFixedTermDepositLocks) {
|
|
18341
18349
|
const ftdID = parseFloat(userFTD.lendingPoolTrancheFixedTermConfig.configId);
|
|
18342
18350
|
const trancheID = userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche.id.toLowerCase();
|
|
18343
|
-
|
|
18344
|
-
|
|
18351
|
+
// The lock holds shares of ONE tranche, so they are worth that
|
|
18352
|
+
// tranche's share price — not the pool's average across tranches,
|
|
18353
|
+
// which differs whenever the tranches have earned differently.
|
|
18354
|
+
const lockTranche = userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche;
|
|
18355
|
+
const amount = this.convertSharesToAssets(userFTD.trancheShares, lockTranche.balance, lockTranche.shares);
|
|
18345
18356
|
const item = (_a = trancheMap.get(trancheID)) !== null && _a !== void 0 ? _a : [];
|
|
18346
18357
|
if (item[ftdID]) {
|
|
18347
18358
|
item[ftdID] += parseFloat(amount);
|
|
@@ -18356,25 +18367,40 @@ class UserLending {
|
|
|
18356
18367
|
}
|
|
18357
18368
|
getCurrentEpochFtdAmount(poolIds, userId, currentEpoch) {
|
|
18358
18369
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18370
|
+
var _a, _b;
|
|
18359
18371
|
const currentEpochFtdAmountRes = yield this._graph.request(currentEpochFtdAmountQuery, {
|
|
18360
18372
|
userId: userId.toLowerCase(),
|
|
18361
18373
|
poolIds: poolIds.map((id) => id.toLowerCase()),
|
|
18362
18374
|
currentEpoch: parseFloat(currentEpoch),
|
|
18363
18375
|
});
|
|
18364
|
-
|
|
18376
|
+
// Deposit requests only (the query filters on type): a deposit's
|
|
18377
|
+
// `amountRequested` is already an ASSET amount, so it is summed as it
|
|
18378
|
+
// is — never put through a share price. Summed, not assigned: a
|
|
18379
|
+
// cancelled request and its replacement share a tranche and a config
|
|
18380
|
+
// in one epoch, and the cancelled one's 0 must not overwrite the live
|
|
18381
|
+
// amount.
|
|
18382
|
+
const totals = new Map();
|
|
18365
18383
|
for (const userRequest of currentEpochFtdAmountRes.userRequests) {
|
|
18366
18384
|
const ftdID = parseFloat(userRequest.fixedTermConfigId);
|
|
18367
|
-
|
|
18368
|
-
const amountRequested = this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares);
|
|
18369
|
-
if (item) {
|
|
18370
|
-
const clonedItem = [...item];
|
|
18371
|
-
clonedItem[ftdID] = amountRequested;
|
|
18372
|
-
currentEpochFtdAmounts.set(userRequest.tranche.id, clonedItem);
|
|
18385
|
+
if (!Number.isInteger(ftdID) || ftdID < 0)
|
|
18373
18386
|
continue;
|
|
18374
|
-
|
|
18387
|
+
const perConfig = (_a = totals.get(userRequest.tranche.id)) !== null && _a !== void 0 ? _a : new Map();
|
|
18388
|
+
perConfig.set(ftdID, ((_b = perConfig.get(ftdID)) !== null && _b !== void 0 ? _b : ethers.BigNumber.from(0)).add(utils$2.parseUnits(userRequest.amountRequested)));
|
|
18389
|
+
totals.set(userRequest.tranche.id, perConfig);
|
|
18390
|
+
}
|
|
18391
|
+
// A slot whose total is zero is left out — `'0.0'` is a truthy
|
|
18392
|
+
// string, and a caller testing the slot would read it as an amount.
|
|
18393
|
+
// A tranche with no non-zero slot is left out entirely.
|
|
18394
|
+
const currentEpochFtdAmounts = new Map();
|
|
18395
|
+
for (const [trancheId, perConfig] of totals) {
|
|
18375
18396
|
const ftdAmounts = [];
|
|
18376
|
-
|
|
18377
|
-
|
|
18397
|
+
for (const [ftdID, total] of perConfig) {
|
|
18398
|
+
if (total.gt(0))
|
|
18399
|
+
ftdAmounts[ftdID] = utils$2.formatUnits(total);
|
|
18400
|
+
}
|
|
18401
|
+
if (ftdAmounts.length) {
|
|
18402
|
+
currentEpochFtdAmounts.set(trancheId, ftdAmounts);
|
|
18403
|
+
}
|
|
18378
18404
|
}
|
|
18379
18405
|
return currentEpochFtdAmounts;
|
|
18380
18406
|
});
|
|
@@ -18395,6 +18421,31 @@ class UserLending {
|
|
|
18395
18421
|
return amountMap;
|
|
18396
18422
|
});
|
|
18397
18423
|
}
|
|
18424
|
+
/**
|
|
18425
|
+
* A request's outstanding amount in ASSETS (the deployment's stable token).
|
|
18426
|
+
*
|
|
18427
|
+
* The subgraph's `UserRequest.amountRequested` is denominated by request
|
|
18428
|
+
* type. A DEPOSIT request holds assets: the deposit NFT's `assetAmount`,
|
|
18429
|
+
* top-ups added, accepted and rejected amounts taken off. A WITHDRAWAL
|
|
18430
|
+
* request holds tranche SHARES: the withdrawal NFT's `sharesAmount`,
|
|
18431
|
+
* accepted shares taken off. So only a withdrawal is converted, at the
|
|
18432
|
+
* tranche's current share price; a deposit passes through unchanged —
|
|
18433
|
+
* pricing it as shares overstated it by the share price.
|
|
18434
|
+
*/
|
|
18435
|
+
requestedAmountInAssets(userRequest) {
|
|
18436
|
+
switch (userRequest.type) {
|
|
18437
|
+
case 'DepositRequest':
|
|
18438
|
+
return userRequest.amountRequested;
|
|
18439
|
+
case 'WithdrawalRequest':
|
|
18440
|
+
return this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares);
|
|
18441
|
+
default: {
|
|
18442
|
+
// Fail closed: a type this SDK does not know has no known
|
|
18443
|
+
// unit, and pricing it either way would print a wrong figure.
|
|
18444
|
+
const unknownType = userRequest.type;
|
|
18445
|
+
throw new Error(`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${unknownType}`);
|
|
18446
|
+
}
|
|
18447
|
+
}
|
|
18448
|
+
}
|
|
18398
18449
|
convertSharesToAssets(sharesAmount, totalAssets, totalSupply) {
|
|
18399
18450
|
if (utils$2.parseUnits(totalSupply).isZero())
|
|
18400
18451
|
return '0';
|
|
@@ -29463,17 +29514,24 @@ function cancelDeadline(request, nextEpochStart, nowSeconds) {
|
|
|
29463
29514
|
const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
|
|
29464
29515
|
const isForced = (events) => events.some((e) => e.requestType === 'Forced');
|
|
29465
29516
|
const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
|
|
29466
|
-
/**
|
|
29467
|
-
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
29468
|
-
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
29469
|
-
* restored). The original amount survives on the `Initiated` event's
|
|
29470
|
-
* `assetAmount`. Recover from there so cancelled rows still carry the amount
|
|
29471
|
-
* the lender originally asked for.
|
|
29472
|
-
*/
|
|
29517
|
+
/** `assetAmount` of the `Initiated` event, or 0 when there is none. */
|
|
29473
29518
|
const initiatedAmountOf = (events) => {
|
|
29474
29519
|
const initiated = initiatedEvent(events);
|
|
29475
29520
|
return initiated ? Number(initiated.assetAmount || '0') : 0;
|
|
29476
29521
|
};
|
|
29522
|
+
/**
|
|
29523
|
+
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
29524
|
+
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
29525
|
+
* restored). The amount cancelled survives on the `Cancelled` event's
|
|
29526
|
+
* `assetAmount` — everything still outstanding at the cancel, top-ups
|
|
29527
|
+
* included. Only when that event carries no figure does this fall back to the
|
|
29528
|
+
* `Initiated` event, which holds the FIRST submission alone and so misses
|
|
29529
|
+
* every top-up.
|
|
29530
|
+
*/
|
|
29531
|
+
const cancelledAmountOf = (events) => {
|
|
29532
|
+
const cancelledEvent = events.find((e) => e.requestType === 'Cancelled');
|
|
29533
|
+
return num(cancelledEvent === null || cancelledEvent === void 0 ? void 0 : cancelledEvent.assetAmount) || initiatedAmountOf(events);
|
|
29534
|
+
};
|
|
29477
29535
|
/**
|
|
29478
29536
|
* A deposit is REALLOCATED when the timeline carries a `Reallocated` event, or
|
|
29479
29537
|
* an `Accepted` event into a tranche other than the one requested.
|
|
@@ -29498,17 +29556,15 @@ const numOrNull = (str) => {
|
|
|
29498
29556
|
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
29499
29557
|
* no copy.
|
|
29500
29558
|
*
|
|
29501
|
-
* BRANCH ORDER (this IS the behaviour
|
|
29502
|
-
* `deriveTransactionView` check for check):
|
|
29559
|
+
* BRANCH ORDER (this IS the behaviour):
|
|
29503
29560
|
*
|
|
29504
29561
|
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
29505
29562
|
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
29506
29563
|
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
29507
|
-
* 4. a withdrawal partly filled
|
|
29508
|
-
* remainder (cycle
|
|
29564
|
+
* 4. a withdrawal partly filled (paid > 0) with
|
|
29565
|
+
* a LIVE remainder (queued > 0, cycle open) → `partial`, outflow, −accepted
|
|
29509
29566
|
* 5. resolved (`status === 'Processed'`):
|
|
29510
|
-
* withdrawal
|
|
29511
|
-
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
29567
|
+
* withdrawal (nothing left queued) → `complete`, outflow, −accepted
|
|
29512
29568
|
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
29513
29569
|
* deposit, part rejected → `partial`, inflow, +accepted
|
|
29514
29570
|
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
@@ -29536,10 +29592,10 @@ function deriveRequestState(request) {
|
|
|
29536
29592
|
const isWithdrawal = request.requestType === 'Withdrawal';
|
|
29537
29593
|
const cancelled = isCancelled(request.events);
|
|
29538
29594
|
// Cancelled requests have `requestedAmount` zeroed on the request itself;
|
|
29539
|
-
//
|
|
29540
|
-
//
|
|
29595
|
+
// take the amount from the Cancelled event so the row still carries
|
|
29596
|
+
// "1500 cancelled" (1000 + a 500 top-up) instead of "0 cancelled".
|
|
29541
29597
|
const requested = cancelled
|
|
29542
|
-
? num(request.requestedAmount) ||
|
|
29598
|
+
? num(request.requestedAmount) || cancelledAmountOf(request.events)
|
|
29543
29599
|
: num(request.requestedAmount);
|
|
29544
29600
|
const accepted = num(request.acceptedAmount);
|
|
29545
29601
|
const rejected = num(request.rejectedAmount);
|
|
@@ -29590,18 +29646,23 @@ function deriveRequestState(request) {
|
|
|
29590
29646
|
}
|
|
29591
29647
|
// 4. Withdrawal partly filled with a LIVE remainder: the request returns
|
|
29592
29648
|
// to the queue with a live Cancel. Checked BEFORE the resolved branch and
|
|
29593
|
-
// via `isCycleClosed`, never via the sticky raw status.
|
|
29649
|
+
// via `isCycleClosed`, never via the sticky raw status. `requested` is
|
|
29650
|
+
// what is still OUTSTANDING, so "partly filled" is "something paid AND
|
|
29651
|
+
// something still queued" — never a comparison of the two, which would
|
|
29652
|
+
// turn on whether more was paid than is left.
|
|
29594
29653
|
if (isWithdrawal &&
|
|
29595
29654
|
accepted > 0 &&
|
|
29596
|
-
|
|
29655
|
+
requested > 0 &&
|
|
29597
29656
|
!isCycleClosed(request)) {
|
|
29598
29657
|
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'partial' });
|
|
29599
29658
|
}
|
|
29600
29659
|
// 5. Resolved — a rejection IS a resolution.
|
|
29601
29660
|
if (request.status === UserRequestStatus.PROCESSED) {
|
|
29661
|
+
// A withdrawal is resolved only once nothing is left queued — a
|
|
29662
|
+
// partly filled one returns to the queue (branch 4), and one cancelled
|
|
29663
|
+
// with a remainder is branch 1 — so a resolved withdrawal is complete.
|
|
29602
29664
|
if (isWithdrawal) {
|
|
29603
|
-
|
|
29604
|
-
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: partly ? 'partial' : 'complete' });
|
|
29665
|
+
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'complete' });
|
|
29605
29666
|
}
|
|
29606
29667
|
// Deposit: full reject vs partial vs full accept.
|
|
29607
29668
|
if (accepted === 0) {
|
package/dist/bundle.esm.js
CHANGED
|
@@ -18013,14 +18013,13 @@ const currentEpochFtdAmountQuery = gql `
|
|
|
18013
18013
|
user: $userId
|
|
18014
18014
|
lendingPool_in: $poolIds
|
|
18015
18015
|
epochId: $currentEpoch
|
|
18016
|
+
type: DepositRequest
|
|
18016
18017
|
}
|
|
18017
18018
|
) {
|
|
18018
18019
|
fixedTermConfigId
|
|
18019
18020
|
amountRequested
|
|
18020
18021
|
tranche {
|
|
18021
18022
|
id
|
|
18022
|
-
balance
|
|
18023
|
-
shares
|
|
18024
18023
|
}
|
|
18025
18024
|
}
|
|
18026
18025
|
}
|
|
@@ -18035,11 +18034,7 @@ const currentFtdBalanceQuery = gql `
|
|
|
18035
18034
|
configId
|
|
18036
18035
|
lendingPoolTranche {
|
|
18037
18036
|
id
|
|
18038
|
-
|
|
18039
|
-
}
|
|
18040
|
-
lendingPool {
|
|
18041
|
-
balance
|
|
18042
|
-
tranches {
|
|
18037
|
+
balance
|
|
18043
18038
|
shares
|
|
18044
18039
|
}
|
|
18045
18040
|
}
|
|
@@ -18047,6 +18042,13 @@ const currentFtdBalanceQuery = gql `
|
|
|
18047
18042
|
}
|
|
18048
18043
|
`;
|
|
18049
18044
|
|
|
18045
|
+
/** Thrown when the subgraph reports a request type with no known unit. */
|
|
18046
|
+
const UNKNOWN_REQUEST_TYPE_MESSAGE = 'UserLending: unknown request type, its amount unit is unknown';
|
|
18047
|
+
const KNOWN_REQUEST_TYPES = [
|
|
18048
|
+
'DepositRequest',
|
|
18049
|
+
'WithdrawalRequest',
|
|
18050
|
+
];
|
|
18051
|
+
const isKnownRequestType = (type) => typeof type === 'string' && KNOWN_REQUEST_TYPES.includes(type);
|
|
18050
18052
|
class UserLending {
|
|
18051
18053
|
constructor(_kasuConfig, signerOrProvider) {
|
|
18052
18054
|
this._kasuConfig = _kasuConfig;
|
|
@@ -18208,6 +18210,14 @@ class UserLending {
|
|
|
18208
18210
|
]);
|
|
18209
18211
|
const retn = [];
|
|
18210
18212
|
for (const userRequest of subgraphResult.userRequests) {
|
|
18213
|
+
// A request type this SDK does not know has no known amount unit
|
|
18214
|
+
// (the schema also declares types no live deployment emits yet).
|
|
18215
|
+
// Skip that ONE row before anything is priced or read on chain,
|
|
18216
|
+
// rather than failing — and blanking — the lender's whole list.
|
|
18217
|
+
if (!isKnownRequestType(userRequest.type)) {
|
|
18218
|
+
console.error(`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${String(userRequest.type)} (request ${userRequest.id}); row skipped`);
|
|
18219
|
+
continue;
|
|
18220
|
+
}
|
|
18211
18221
|
const directusPool = directusResult.find(({ id }) => id === userRequest.lendingPool.id);
|
|
18212
18222
|
const trancheName = trancheNames[userRequest.lendingPool.tranches.length - 1][parseInt(userRequest.tranche.orderId)];
|
|
18213
18223
|
const events = [];
|
|
@@ -18303,9 +18313,7 @@ class UserLending {
|
|
|
18303
18313
|
: 'Withdrawal',
|
|
18304
18314
|
trancheName: trancheName,
|
|
18305
18315
|
trancheId: userRequest.tranche.id,
|
|
18306
|
-
requestedAmount:
|
|
18307
|
-
? userRequest.amountRequested
|
|
18308
|
-
: this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares).toString(),
|
|
18316
|
+
requestedAmount: this.requestedAmountInAssets(userRequest),
|
|
18309
18317
|
acceptedAmount: userRequest.amountAccepted,
|
|
18310
18318
|
rejectedAmount: userRequest.amountRejected,
|
|
18311
18319
|
status: userRequest.status,
|
|
@@ -18338,8 +18346,11 @@ class UserLending {
|
|
|
18338
18346
|
for (const userFTD of currentFtdBalance.userLendingPoolTrancheFixedTermDepositLocks) {
|
|
18339
18347
|
const ftdID = parseFloat(userFTD.lendingPoolTrancheFixedTermConfig.configId);
|
|
18340
18348
|
const trancheID = userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche.id.toLowerCase();
|
|
18341
|
-
|
|
18342
|
-
|
|
18349
|
+
// The lock holds shares of ONE tranche, so they are worth that
|
|
18350
|
+
// tranche's share price — not the pool's average across tranches,
|
|
18351
|
+
// which differs whenever the tranches have earned differently.
|
|
18352
|
+
const lockTranche = userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche;
|
|
18353
|
+
const amount = this.convertSharesToAssets(userFTD.trancheShares, lockTranche.balance, lockTranche.shares);
|
|
18343
18354
|
const item = (_a = trancheMap.get(trancheID)) !== null && _a !== void 0 ? _a : [];
|
|
18344
18355
|
if (item[ftdID]) {
|
|
18345
18356
|
item[ftdID] += parseFloat(amount);
|
|
@@ -18354,25 +18365,40 @@ class UserLending {
|
|
|
18354
18365
|
}
|
|
18355
18366
|
getCurrentEpochFtdAmount(poolIds, userId, currentEpoch) {
|
|
18356
18367
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18368
|
+
var _a, _b;
|
|
18357
18369
|
const currentEpochFtdAmountRes = yield this._graph.request(currentEpochFtdAmountQuery, {
|
|
18358
18370
|
userId: userId.toLowerCase(),
|
|
18359
18371
|
poolIds: poolIds.map((id) => id.toLowerCase()),
|
|
18360
18372
|
currentEpoch: parseFloat(currentEpoch),
|
|
18361
18373
|
});
|
|
18362
|
-
|
|
18374
|
+
// Deposit requests only (the query filters on type): a deposit's
|
|
18375
|
+
// `amountRequested` is already an ASSET amount, so it is summed as it
|
|
18376
|
+
// is — never put through a share price. Summed, not assigned: a
|
|
18377
|
+
// cancelled request and its replacement share a tranche and a config
|
|
18378
|
+
// in one epoch, and the cancelled one's 0 must not overwrite the live
|
|
18379
|
+
// amount.
|
|
18380
|
+
const totals = new Map();
|
|
18363
18381
|
for (const userRequest of currentEpochFtdAmountRes.userRequests) {
|
|
18364
18382
|
const ftdID = parseFloat(userRequest.fixedTermConfigId);
|
|
18365
|
-
|
|
18366
|
-
const amountRequested = this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares);
|
|
18367
|
-
if (item) {
|
|
18368
|
-
const clonedItem = [...item];
|
|
18369
|
-
clonedItem[ftdID] = amountRequested;
|
|
18370
|
-
currentEpochFtdAmounts.set(userRequest.tranche.id, clonedItem);
|
|
18383
|
+
if (!Number.isInteger(ftdID) || ftdID < 0)
|
|
18371
18384
|
continue;
|
|
18372
|
-
|
|
18385
|
+
const perConfig = (_a = totals.get(userRequest.tranche.id)) !== null && _a !== void 0 ? _a : new Map();
|
|
18386
|
+
perConfig.set(ftdID, ((_b = perConfig.get(ftdID)) !== null && _b !== void 0 ? _b : BigNumber.from(0)).add(parseUnits(userRequest.amountRequested)));
|
|
18387
|
+
totals.set(userRequest.tranche.id, perConfig);
|
|
18388
|
+
}
|
|
18389
|
+
// A slot whose total is zero is left out — `'0.0'` is a truthy
|
|
18390
|
+
// string, and a caller testing the slot would read it as an amount.
|
|
18391
|
+
// A tranche with no non-zero slot is left out entirely.
|
|
18392
|
+
const currentEpochFtdAmounts = new Map();
|
|
18393
|
+
for (const [trancheId, perConfig] of totals) {
|
|
18373
18394
|
const ftdAmounts = [];
|
|
18374
|
-
|
|
18375
|
-
|
|
18395
|
+
for (const [ftdID, total] of perConfig) {
|
|
18396
|
+
if (total.gt(0))
|
|
18397
|
+
ftdAmounts[ftdID] = formatUnits(total);
|
|
18398
|
+
}
|
|
18399
|
+
if (ftdAmounts.length) {
|
|
18400
|
+
currentEpochFtdAmounts.set(trancheId, ftdAmounts);
|
|
18401
|
+
}
|
|
18376
18402
|
}
|
|
18377
18403
|
return currentEpochFtdAmounts;
|
|
18378
18404
|
});
|
|
@@ -18393,6 +18419,31 @@ class UserLending {
|
|
|
18393
18419
|
return amountMap;
|
|
18394
18420
|
});
|
|
18395
18421
|
}
|
|
18422
|
+
/**
|
|
18423
|
+
* A request's outstanding amount in ASSETS (the deployment's stable token).
|
|
18424
|
+
*
|
|
18425
|
+
* The subgraph's `UserRequest.amountRequested` is denominated by request
|
|
18426
|
+
* type. A DEPOSIT request holds assets: the deposit NFT's `assetAmount`,
|
|
18427
|
+
* top-ups added, accepted and rejected amounts taken off. A WITHDRAWAL
|
|
18428
|
+
* request holds tranche SHARES: the withdrawal NFT's `sharesAmount`,
|
|
18429
|
+
* accepted shares taken off. So only a withdrawal is converted, at the
|
|
18430
|
+
* tranche's current share price; a deposit passes through unchanged —
|
|
18431
|
+
* pricing it as shares overstated it by the share price.
|
|
18432
|
+
*/
|
|
18433
|
+
requestedAmountInAssets(userRequest) {
|
|
18434
|
+
switch (userRequest.type) {
|
|
18435
|
+
case 'DepositRequest':
|
|
18436
|
+
return userRequest.amountRequested;
|
|
18437
|
+
case 'WithdrawalRequest':
|
|
18438
|
+
return this.convertSharesToAssets(userRequest.amountRequested, userRequest.tranche.balance, userRequest.tranche.shares);
|
|
18439
|
+
default: {
|
|
18440
|
+
// Fail closed: a type this SDK does not know has no known
|
|
18441
|
+
// unit, and pricing it either way would print a wrong figure.
|
|
18442
|
+
const unknownType = userRequest.type;
|
|
18443
|
+
throw new Error(`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${unknownType}`);
|
|
18444
|
+
}
|
|
18445
|
+
}
|
|
18446
|
+
}
|
|
18396
18447
|
convertSharesToAssets(sharesAmount, totalAssets, totalSupply) {
|
|
18397
18448
|
if (parseUnits(totalSupply).isZero())
|
|
18398
18449
|
return '0';
|
|
@@ -29461,17 +29512,24 @@ function cancelDeadline(request, nextEpochStart, nowSeconds) {
|
|
|
29461
29512
|
const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
|
|
29462
29513
|
const isForced = (events) => events.some((e) => e.requestType === 'Forced');
|
|
29463
29514
|
const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
|
|
29464
|
-
/**
|
|
29465
|
-
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
29466
|
-
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
29467
|
-
* restored). The original amount survives on the `Initiated` event's
|
|
29468
|
-
* `assetAmount`. Recover from there so cancelled rows still carry the amount
|
|
29469
|
-
* the lender originally asked for.
|
|
29470
|
-
*/
|
|
29515
|
+
/** `assetAmount` of the `Initiated` event, or 0 when there is none. */
|
|
29471
29516
|
const initiatedAmountOf = (events) => {
|
|
29472
29517
|
const initiated = initiatedEvent(events);
|
|
29473
29518
|
return initiated ? Number(initiated.assetAmount || '0') : 0;
|
|
29474
29519
|
};
|
|
29520
|
+
/**
|
|
29521
|
+
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
29522
|
+
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
29523
|
+
* restored). The amount cancelled survives on the `Cancelled` event's
|
|
29524
|
+
* `assetAmount` — everything still outstanding at the cancel, top-ups
|
|
29525
|
+
* included. Only when that event carries no figure does this fall back to the
|
|
29526
|
+
* `Initiated` event, which holds the FIRST submission alone and so misses
|
|
29527
|
+
* every top-up.
|
|
29528
|
+
*/
|
|
29529
|
+
const cancelledAmountOf = (events) => {
|
|
29530
|
+
const cancelledEvent = events.find((e) => e.requestType === 'Cancelled');
|
|
29531
|
+
return num(cancelledEvent === null || cancelledEvent === void 0 ? void 0 : cancelledEvent.assetAmount) || initiatedAmountOf(events);
|
|
29532
|
+
};
|
|
29475
29533
|
/**
|
|
29476
29534
|
* A deposit is REALLOCATED when the timeline carries a `Reallocated` event, or
|
|
29477
29535
|
* an `Accepted` event into a tranche other than the one requested.
|
|
@@ -29496,17 +29554,15 @@ const numOrNull = (str) => {
|
|
|
29496
29554
|
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
29497
29555
|
* no copy.
|
|
29498
29556
|
*
|
|
29499
|
-
* BRANCH ORDER (this IS the behaviour
|
|
29500
|
-
* `deriveTransactionView` check for check):
|
|
29557
|
+
* BRANCH ORDER (this IS the behaviour):
|
|
29501
29558
|
*
|
|
29502
29559
|
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
29503
29560
|
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
29504
29561
|
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
29505
|
-
* 4. a withdrawal partly filled
|
|
29506
|
-
* remainder (cycle
|
|
29562
|
+
* 4. a withdrawal partly filled (paid > 0) with
|
|
29563
|
+
* a LIVE remainder (queued > 0, cycle open) → `partial`, outflow, −accepted
|
|
29507
29564
|
* 5. resolved (`status === 'Processed'`):
|
|
29508
|
-
* withdrawal
|
|
29509
|
-
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
29565
|
+
* withdrawal (nothing left queued) → `complete`, outflow, −accepted
|
|
29510
29566
|
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
29511
29567
|
* deposit, part rejected → `partial`, inflow, +accepted
|
|
29512
29568
|
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
@@ -29534,10 +29590,10 @@ function deriveRequestState(request) {
|
|
|
29534
29590
|
const isWithdrawal = request.requestType === 'Withdrawal';
|
|
29535
29591
|
const cancelled = isCancelled(request.events);
|
|
29536
29592
|
// Cancelled requests have `requestedAmount` zeroed on the request itself;
|
|
29537
|
-
//
|
|
29538
|
-
//
|
|
29593
|
+
// take the amount from the Cancelled event so the row still carries
|
|
29594
|
+
// "1500 cancelled" (1000 + a 500 top-up) instead of "0 cancelled".
|
|
29539
29595
|
const requested = cancelled
|
|
29540
|
-
? num(request.requestedAmount) ||
|
|
29596
|
+
? num(request.requestedAmount) || cancelledAmountOf(request.events)
|
|
29541
29597
|
: num(request.requestedAmount);
|
|
29542
29598
|
const accepted = num(request.acceptedAmount);
|
|
29543
29599
|
const rejected = num(request.rejectedAmount);
|
|
@@ -29588,18 +29644,23 @@ function deriveRequestState(request) {
|
|
|
29588
29644
|
}
|
|
29589
29645
|
// 4. Withdrawal partly filled with a LIVE remainder: the request returns
|
|
29590
29646
|
// to the queue with a live Cancel. Checked BEFORE the resolved branch and
|
|
29591
|
-
// via `isCycleClosed`, never via the sticky raw status.
|
|
29647
|
+
// via `isCycleClosed`, never via the sticky raw status. `requested` is
|
|
29648
|
+
// what is still OUTSTANDING, so "partly filled" is "something paid AND
|
|
29649
|
+
// something still queued" — never a comparison of the two, which would
|
|
29650
|
+
// turn on whether more was paid than is left.
|
|
29592
29651
|
if (isWithdrawal &&
|
|
29593
29652
|
accepted > 0 &&
|
|
29594
|
-
|
|
29653
|
+
requested > 0 &&
|
|
29595
29654
|
!isCycleClosed(request)) {
|
|
29596
29655
|
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'partial' });
|
|
29597
29656
|
}
|
|
29598
29657
|
// 5. Resolved — a rejection IS a resolution.
|
|
29599
29658
|
if (request.status === UserRequestStatus.PROCESSED) {
|
|
29659
|
+
// A withdrawal is resolved only once nothing is left queued — a
|
|
29660
|
+
// partly filled one returns to the queue (branch 4), and one cancelled
|
|
29661
|
+
// with a remainder is branch 1 — so a resolved withdrawal is complete.
|
|
29600
29662
|
if (isWithdrawal) {
|
|
29601
|
-
|
|
29602
|
-
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: partly ? 'partial' : 'complete' });
|
|
29663
|
+
return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'complete' });
|
|
29603
29664
|
}
|
|
29604
29665
|
// Deposit: full reject vs partial vs full accept.
|
|
29605
29666
|
if (accepted === 0) {
|
|
@@ -65,10 +65,13 @@ export interface RequestState {
|
|
|
65
65
|
statusCode: RequestStatusCode;
|
|
66
66
|
kind: RequestKind;
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `
|
|
68
|
+
* `UserRequest.requestedAmount`, in assets: the amount still OUTSTANDING,
|
|
69
|
+
* which the subgraph reduces as parts are accepted or rejected (0 once a
|
|
70
|
+
* request is fully resolved). A cancelled request has it zeroed by the
|
|
71
|
+
* subgraph, so for a cancellation it is the `Cancelled` event's amount —
|
|
72
|
+
* what was outstanding when cancelled, top-ups included — falling back to
|
|
73
|
+
* the `Initiated` event (see `initiatedAmount`) only when that event
|
|
74
|
+
* carries none.
|
|
72
75
|
*/
|
|
73
76
|
requestedAmount: number;
|
|
74
77
|
/**
|
|
@@ -207,17 +210,15 @@ export declare function cancelDeadline(request: Pick<RequestState, 'canCancel' |
|
|
|
207
210
|
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
208
211
|
* no copy.
|
|
209
212
|
*
|
|
210
|
-
* BRANCH ORDER (this IS the behaviour
|
|
211
|
-
* `deriveTransactionView` check for check):
|
|
213
|
+
* BRANCH ORDER (this IS the behaviour):
|
|
212
214
|
*
|
|
213
215
|
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
214
216
|
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
215
217
|
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
216
|
-
* 4. a withdrawal partly filled
|
|
217
|
-
* remainder (cycle
|
|
218
|
+
* 4. a withdrawal partly filled (paid > 0) with
|
|
219
|
+
* a LIVE remainder (queued > 0, cycle open) → `partial`, outflow, −accepted
|
|
218
220
|
* 5. resolved (`status === 'Processed'`):
|
|
219
|
-
* withdrawal
|
|
220
|
-
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
221
|
+
* withdrawal (nothing left queued) → `complete`, outflow, −accepted
|
|
221
222
|
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
222
223
|
* deposit, part rejected → `partial`, inflow, +accepted
|
|
223
224
|
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
@@ -69,8 +69,6 @@ export interface CurrentEpochFtdAmountSubgraph {
|
|
|
69
69
|
amountRequested: string;
|
|
70
70
|
tranche: {
|
|
71
71
|
id: string;
|
|
72
|
-
balance: string;
|
|
73
|
-
shares: string;
|
|
74
72
|
};
|
|
75
73
|
}[];
|
|
76
74
|
}
|
|
@@ -135,13 +133,9 @@ export interface CurrentFtdBalanceSubgraph {
|
|
|
135
133
|
configId: string;
|
|
136
134
|
lendingPoolTranche: {
|
|
137
135
|
id: string;
|
|
138
|
-
|
|
139
|
-
};
|
|
140
|
-
lendingPool: {
|
|
141
|
-
balance: string;
|
|
142
|
-
tranches: {
|
|
136
|
+
balance: string;
|
|
143
137
|
shares: string;
|
|
144
|
-
}
|
|
138
|
+
};
|
|
145
139
|
};
|
|
146
140
|
}[];
|
|
147
141
|
}
|
|
@@ -36,8 +36,18 @@ export interface UserRequest {
|
|
|
36
36
|
requestType: 'Deposit' | 'Withdrawal';
|
|
37
37
|
trancheId: string;
|
|
38
38
|
trancheName: string;
|
|
39
|
+
/**
|
|
40
|
+
* The amount still OUTSTANDING on the request, in assets (the
|
|
41
|
+
* deployment's stable token, display units) — not the amount first asked
|
|
42
|
+
* for: the subgraph takes accepted and rejected amounts off it, and a
|
|
43
|
+
* cancellation zeroes it. A deposit's figure is the asset amount as the
|
|
44
|
+
* subgraph holds it (top-ups included); a withdrawal's is its remaining
|
|
45
|
+
* tranche shares at the tranche's current share price.
|
|
46
|
+
*/
|
|
39
47
|
requestedAmount: string;
|
|
48
|
+
/** Assets accepted so far — deposited, or paid out for a withdrawal. */
|
|
40
49
|
acceptedAmount: string;
|
|
50
|
+
/** Assets rejected and returned — deposits only; 0 for a withdrawal. */
|
|
41
51
|
rejectedAmount: string;
|
|
42
52
|
timestamp: EpochTimeStamp;
|
|
43
53
|
status: UserRequestStatus;
|
|
@@ -3,6 +3,8 @@ import { BigNumber, BigNumberish, BytesLike, ContractTransaction, Signer } from
|
|
|
3
3
|
import { IKasuAllowListAbi__factory } from '../../contracts';
|
|
4
4
|
import { SdkConfig } from '../../sdk-config';
|
|
5
5
|
import { PortfolioUserTrancheBalance, UserApyBonus, UserPoolBalance, UserRequest, UserTrancheBalance } from './types';
|
|
6
|
+
/** Thrown when the subgraph reports a request type with no known unit. */
|
|
7
|
+
export declare const UNKNOWN_REQUEST_TYPE_MESSAGE = "UserLending: unknown request type, its amount unit is unknown";
|
|
6
8
|
export declare class UserLending {
|
|
7
9
|
private _kasuConfig;
|
|
8
10
|
private readonly _graph;
|
|
@@ -60,6 +62,18 @@ export declare class UserLending {
|
|
|
60
62
|
getCurrentFtdBalance(poolId: string, userId: string): Promise<Map<string, number[]>>;
|
|
61
63
|
getCurrentEpochFtdAmount(poolIds: string[], userId: string, currentEpoch: string): Promise<Map<string, string[]>>;
|
|
62
64
|
getCurrentEpochDepositedAmount(lendingPoolIds: string[], userId: string): Promise<Map<string, string>>;
|
|
65
|
+
/**
|
|
66
|
+
* A request's outstanding amount in ASSETS (the deployment's stable token).
|
|
67
|
+
*
|
|
68
|
+
* The subgraph's `UserRequest.amountRequested` is denominated by request
|
|
69
|
+
* type. A DEPOSIT request holds assets: the deposit NFT's `assetAmount`,
|
|
70
|
+
* top-ups added, accepted and rejected amounts taken off. A WITHDRAWAL
|
|
71
|
+
* request holds tranche SHARES: the withdrawal NFT's `sharesAmount`,
|
|
72
|
+
* accepted shares taken off. So only a withdrawal is converted, at the
|
|
73
|
+
* tranche's current share price; a deposit passes through unchanged —
|
|
74
|
+
* pricing it as shares overstated it by the share price.
|
|
75
|
+
*/
|
|
76
|
+
private requestedAmountInAssets;
|
|
63
77
|
convertSharesToAssets(sharesAmount: string, totalAssets: string, totalSupply: string): string;
|
|
64
78
|
isClearingPending(poolId: string): Promise<boolean>;
|
|
65
79
|
isCancelable(status: string, lendingPoolId: string): Promise<boolean>;
|
package/package.json
CHANGED
package/src/domain/requests.ts
CHANGED
|
@@ -82,10 +82,13 @@ export interface RequestState {
|
|
|
82
82
|
statusCode: RequestStatusCode;
|
|
83
83
|
kind: RequestKind;
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
85
|
+
* `UserRequest.requestedAmount`, in assets: the amount still OUTSTANDING,
|
|
86
|
+
* which the subgraph reduces as parts are accepted or rejected (0 once a
|
|
87
|
+
* request is fully resolved). A cancelled request has it zeroed by the
|
|
88
|
+
* subgraph, so for a cancellation it is the `Cancelled` event's amount —
|
|
89
|
+
* what was outstanding when cancelled, top-ups included — falling back to
|
|
90
|
+
* the `Initiated` event (see `initiatedAmount`) only when that event
|
|
91
|
+
* carries none.
|
|
89
92
|
*/
|
|
90
93
|
requestedAmount: number;
|
|
91
94
|
/**
|
|
@@ -287,16 +290,24 @@ const initiatedEvent = (
|
|
|
287
290
|
): UserRequestEvent | undefined =>
|
|
288
291
|
events.find((e) => e.requestType === 'Initiated');
|
|
289
292
|
|
|
293
|
+
/** `assetAmount` of the `Initiated` event, or 0 when there is none. */
|
|
294
|
+
const initiatedAmountOf = (events: UserRequestEvent[]): number => {
|
|
295
|
+
const initiated = initiatedEvent(events);
|
|
296
|
+
return initiated ? Number(initiated.assetAmount || '0') : 0;
|
|
297
|
+
};
|
|
298
|
+
|
|
290
299
|
/**
|
|
291
300
|
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
292
301
|
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
293
|
-
* restored). The
|
|
294
|
-
* `assetAmount
|
|
295
|
-
*
|
|
302
|
+
* restored). The amount cancelled survives on the `Cancelled` event's
|
|
303
|
+
* `assetAmount` — everything still outstanding at the cancel, top-ups
|
|
304
|
+
* included. Only when that event carries no figure does this fall back to the
|
|
305
|
+
* `Initiated` event, which holds the FIRST submission alone and so misses
|
|
306
|
+
* every top-up.
|
|
296
307
|
*/
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
return
|
|
308
|
+
const cancelledAmountOf = (events: UserRequestEvent[]): number => {
|
|
309
|
+
const cancelledEvent = events.find((e) => e.requestType === 'Cancelled');
|
|
310
|
+
return num(cancelledEvent?.assetAmount) || initiatedAmountOf(events);
|
|
300
311
|
};
|
|
301
312
|
|
|
302
313
|
/**
|
|
@@ -332,17 +343,15 @@ const numOrNull = (str: string | null | undefined): number | null => {
|
|
|
332
343
|
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
333
344
|
* no copy.
|
|
334
345
|
*
|
|
335
|
-
* BRANCH ORDER (this IS the behaviour
|
|
336
|
-
* `deriveTransactionView` check for check):
|
|
346
|
+
* BRANCH ORDER (this IS the behaviour):
|
|
337
347
|
*
|
|
338
348
|
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
339
349
|
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
340
350
|
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
341
|
-
* 4. a withdrawal partly filled
|
|
342
|
-
* remainder (cycle
|
|
351
|
+
* 4. a withdrawal partly filled (paid > 0) with
|
|
352
|
+
* a LIVE remainder (queued > 0, cycle open) → `partial`, outflow, −accepted
|
|
343
353
|
* 5. resolved (`status === 'Processed'`):
|
|
344
|
-
* withdrawal
|
|
345
|
-
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
354
|
+
* withdrawal (nothing left queued) → `complete`, outflow, −accepted
|
|
346
355
|
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
347
356
|
* deposit, part rejected → `partial`, inflow, +accepted
|
|
348
357
|
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
@@ -369,10 +378,10 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
369
378
|
const isWithdrawal = request.requestType === 'Withdrawal';
|
|
370
379
|
const cancelled = isCancelled(request.events);
|
|
371
380
|
// Cancelled requests have `requestedAmount` zeroed on the request itself;
|
|
372
|
-
//
|
|
373
|
-
//
|
|
381
|
+
// take the amount from the Cancelled event so the row still carries
|
|
382
|
+
// "1500 cancelled" (1000 + a 500 top-up) instead of "0 cancelled".
|
|
374
383
|
const requested = cancelled
|
|
375
|
-
? num(request.requestedAmount) ||
|
|
384
|
+
? num(request.requestedAmount) || cancelledAmountOf(request.events)
|
|
376
385
|
: num(request.requestedAmount);
|
|
377
386
|
const accepted = num(request.acceptedAmount);
|
|
378
387
|
const rejected = num(request.rejectedAmount);
|
|
@@ -441,11 +450,14 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
441
450
|
|
|
442
451
|
// 4. Withdrawal partly filled with a LIVE remainder: the request returns
|
|
443
452
|
// to the queue with a live Cancel. Checked BEFORE the resolved branch and
|
|
444
|
-
// via `isCycleClosed`, never via the sticky raw status.
|
|
453
|
+
// via `isCycleClosed`, never via the sticky raw status. `requested` is
|
|
454
|
+
// what is still OUTSTANDING, so "partly filled" is "something paid AND
|
|
455
|
+
// something still queued" — never a comparison of the two, which would
|
|
456
|
+
// turn on whether more was paid than is left.
|
|
445
457
|
if (
|
|
446
458
|
isWithdrawal &&
|
|
447
459
|
accepted > 0 &&
|
|
448
|
-
|
|
460
|
+
requested > 0 &&
|
|
449
461
|
!isCycleClosed(request)
|
|
450
462
|
) {
|
|
451
463
|
return {
|
|
@@ -458,13 +470,15 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
458
470
|
|
|
459
471
|
// 5. Resolved — a rejection IS a resolution.
|
|
460
472
|
if (request.status === UserRequestStatus.PROCESSED) {
|
|
473
|
+
// A withdrawal is resolved only once nothing is left queued — a
|
|
474
|
+
// partly filled one returns to the queue (branch 4), and one cancelled
|
|
475
|
+
// with a remainder is branch 1 — so a resolved withdrawal is complete.
|
|
461
476
|
if (isWithdrawal) {
|
|
462
|
-
const partly = accepted < requested;
|
|
463
477
|
return {
|
|
464
478
|
...base,
|
|
465
479
|
kind: 'outflow',
|
|
466
480
|
amount: -accepted,
|
|
467
|
-
statusCode:
|
|
481
|
+
statusCode: 'complete',
|
|
468
482
|
};
|
|
469
483
|
}
|
|
470
484
|
|
|
@@ -160,14 +160,13 @@ export const currentEpochFtdAmountQuery = gql`
|
|
|
160
160
|
user: $userId
|
|
161
161
|
lendingPool_in: $poolIds
|
|
162
162
|
epochId: $currentEpoch
|
|
163
|
+
type: DepositRequest
|
|
163
164
|
}
|
|
164
165
|
) {
|
|
165
166
|
fixedTermConfigId
|
|
166
167
|
amountRequested
|
|
167
168
|
tranche {
|
|
168
169
|
id
|
|
169
|
-
balance
|
|
170
|
-
shares
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
}
|
|
@@ -183,11 +182,7 @@ export const currentFtdBalanceQuery = gql`
|
|
|
183
182
|
configId
|
|
184
183
|
lendingPoolTranche {
|
|
185
184
|
id
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
lendingPool {
|
|
189
|
-
balance
|
|
190
|
-
tranches {
|
|
185
|
+
balance
|
|
191
186
|
shares
|
|
192
187
|
}
|
|
193
188
|
}
|
|
@@ -77,8 +77,6 @@ export interface CurrentEpochFtdAmountSubgraph {
|
|
|
77
77
|
amountRequested: string;
|
|
78
78
|
tranche: {
|
|
79
79
|
id: string;
|
|
80
|
-
balance: string;
|
|
81
|
-
shares: string;
|
|
82
80
|
};
|
|
83
81
|
}[];
|
|
84
82
|
}
|
|
@@ -161,13 +159,9 @@ export interface CurrentFtdBalanceSubgraph {
|
|
|
161
159
|
configId: string;
|
|
162
160
|
lendingPoolTranche: {
|
|
163
161
|
id: string;
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
lendingPool: {
|
|
167
|
-
balance: string;
|
|
168
|
-
tranches: {
|
|
162
|
+
balance: string;
|
|
169
163
|
shares: string;
|
|
170
|
-
}
|
|
164
|
+
};
|
|
171
165
|
};
|
|
172
166
|
}[];
|
|
173
167
|
}
|
|
@@ -39,8 +39,18 @@ export interface UserRequest {
|
|
|
39
39
|
requestType: 'Deposit' | 'Withdrawal';
|
|
40
40
|
trancheId: string;
|
|
41
41
|
trancheName: string;
|
|
42
|
+
/**
|
|
43
|
+
* The amount still OUTSTANDING on the request, in assets (the
|
|
44
|
+
* deployment's stable token, display units) — not the amount first asked
|
|
45
|
+
* for: the subgraph takes accepted and rejected amounts off it, and a
|
|
46
|
+
* cancellation zeroes it. A deposit's figure is the asset amount as the
|
|
47
|
+
* subgraph holds it (top-ups included); a withdrawal's is its remaining
|
|
48
|
+
* tranche shares at the tranche's current share price.
|
|
49
|
+
*/
|
|
42
50
|
requestedAmount: string;
|
|
51
|
+
/** Assets accepted so far — deposited, or paid out for a withdrawal. */
|
|
43
52
|
acceptedAmount: string;
|
|
53
|
+
/** Assets rejected and returned — deposits only; 0 for a withdrawal. */
|
|
44
54
|
rejectedAmount: string;
|
|
45
55
|
timestamp: EpochTimeStamp;
|
|
46
56
|
status: UserRequestStatus;
|
|
@@ -8,13 +8,7 @@ import {
|
|
|
8
8
|
ethers,
|
|
9
9
|
Signer,
|
|
10
10
|
} from 'ethers';
|
|
11
|
-
import {
|
|
12
|
-
defaultAbiCoder,
|
|
13
|
-
formatEther,
|
|
14
|
-
formatUnits,
|
|
15
|
-
parseEther,
|
|
16
|
-
parseUnits,
|
|
17
|
-
} from 'ethers/lib/utils';
|
|
11
|
+
import { defaultAbiCoder, formatUnits, parseUnits } from 'ethers/lib/utils';
|
|
18
12
|
import { GraphQLClient } from 'graphql-request';
|
|
19
13
|
|
|
20
14
|
import {
|
|
@@ -70,6 +64,20 @@ import {
|
|
|
70
64
|
UserTrancheBalance,
|
|
71
65
|
} from './types';
|
|
72
66
|
|
|
67
|
+
/** Thrown when the subgraph reports a request type with no known unit. */
|
|
68
|
+
export const UNKNOWN_REQUEST_TYPE_MESSAGE =
|
|
69
|
+
'UserLending: unknown request type, its amount unit is unknown';
|
|
70
|
+
|
|
71
|
+
const KNOWN_REQUEST_TYPES: readonly string[] = [
|
|
72
|
+
'DepositRequest',
|
|
73
|
+
'WithdrawalRequest',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const isKnownRequestType = (
|
|
77
|
+
type: unknown,
|
|
78
|
+
): type is UserRequestsSubgraph['userRequests'][number]['type'] =>
|
|
79
|
+
typeof type === 'string' && KNOWN_REQUEST_TYPES.includes(type);
|
|
80
|
+
|
|
73
81
|
export class UserLending {
|
|
74
82
|
private readonly _graph: GraphQLClient;
|
|
75
83
|
|
|
@@ -406,6 +414,17 @@ export class UserLending {
|
|
|
406
414
|
const retn: UserRequest[] = [];
|
|
407
415
|
|
|
408
416
|
for (const userRequest of subgraphResult.userRequests) {
|
|
417
|
+
// A request type this SDK does not know has no known amount unit
|
|
418
|
+
// (the schema also declares types no live deployment emits yet).
|
|
419
|
+
// Skip that ONE row before anything is priced or read on chain,
|
|
420
|
+
// rather than failing — and blanking — the lender's whole list.
|
|
421
|
+
if (!isKnownRequestType(userRequest.type)) {
|
|
422
|
+
console.error(
|
|
423
|
+
`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${String(userRequest.type)} (request ${userRequest.id}); row skipped`,
|
|
424
|
+
);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
409
428
|
const directusPool = directusResult.find(
|
|
410
429
|
({ id }) => id === userRequest.lendingPool.id,
|
|
411
430
|
);
|
|
@@ -552,13 +571,7 @@ export class UserLending {
|
|
|
552
571
|
: 'Withdrawal',
|
|
553
572
|
trancheName: trancheName,
|
|
554
573
|
trancheId: userRequest.tranche.id,
|
|
555
|
-
requestedAmount:
|
|
556
|
-
? userRequest.amountRequested
|
|
557
|
-
: this.convertSharesToAssets(
|
|
558
|
-
userRequest.amountRequested,
|
|
559
|
-
userRequest.tranche.balance,
|
|
560
|
-
userRequest.tranche.shares,
|
|
561
|
-
).toString(),
|
|
574
|
+
requestedAmount: this.requestedAmountInAssets(userRequest),
|
|
562
575
|
acceptedAmount: userRequest.amountAccepted,
|
|
563
576
|
rejectedAmount: userRequest.amountRejected,
|
|
564
577
|
status: userRequest.status,
|
|
@@ -606,15 +619,16 @@ export class UserLending {
|
|
|
606
619
|
const trancheID =
|
|
607
620
|
userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche.id.toLowerCase();
|
|
608
621
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
622
|
+
// The lock holds shares of ONE tranche, so they are worth that
|
|
623
|
+
// tranche's share price — not the pool's average across tranches,
|
|
624
|
+
// which differs whenever the tranches have earned differently.
|
|
625
|
+
const lockTranche =
|
|
626
|
+
userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche;
|
|
613
627
|
|
|
614
628
|
const amount = this.convertSharesToAssets(
|
|
615
629
|
userFTD.trancheShares,
|
|
616
|
-
|
|
617
|
-
|
|
630
|
+
lockTranche.balance,
|
|
631
|
+
lockTranche.shares,
|
|
618
632
|
);
|
|
619
633
|
|
|
620
634
|
const item = trancheMap.get(trancheID) ?? [];
|
|
@@ -643,33 +657,43 @@ export class UserLending {
|
|
|
643
657
|
currentEpoch: parseFloat(currentEpoch),
|
|
644
658
|
});
|
|
645
659
|
|
|
646
|
-
|
|
660
|
+
// Deposit requests only (the query filters on type): a deposit's
|
|
661
|
+
// `amountRequested` is already an ASSET amount, so it is summed as it
|
|
662
|
+
// is — never put through a share price. Summed, not assigned: a
|
|
663
|
+
// cancelled request and its replacement share a tranche and a config
|
|
664
|
+
// in one epoch, and the cancelled one's 0 must not overwrite the live
|
|
665
|
+
// amount.
|
|
666
|
+
const totals = new Map<string, Map<number, BigNumber>>();
|
|
647
667
|
|
|
648
668
|
for (const userRequest of currentEpochFtdAmountRes.userRequests) {
|
|
649
669
|
const ftdID = parseFloat(userRequest.fixedTermConfigId);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
670
|
+
if (!Number.isInteger(ftdID) || ftdID < 0) continue;
|
|
671
|
+
|
|
672
|
+
const perConfig =
|
|
673
|
+
totals.get(userRequest.tranche.id) ??
|
|
674
|
+
new Map<number, BigNumber>();
|
|
675
|
+
perConfig.set(
|
|
676
|
+
ftdID,
|
|
677
|
+
(perConfig.get(ftdID) ?? BigNumber.from(0)).add(
|
|
678
|
+
parseUnits(userRequest.amountRequested),
|
|
679
|
+
),
|
|
657
680
|
);
|
|
681
|
+
totals.set(userRequest.tranche.id, perConfig);
|
|
682
|
+
}
|
|
658
683
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
currentEpochFtdAmounts.set(userRequest.tranche.id, clonedItem);
|
|
684
|
+
// A slot whose total is zero is left out — `'0.0'` is a truthy
|
|
685
|
+
// string, and a caller testing the slot would read it as an amount.
|
|
686
|
+
// A tranche with no non-zero slot is left out entirely.
|
|
687
|
+
const currentEpochFtdAmounts = new Map<string, string[]>();
|
|
665
688
|
|
|
666
|
-
|
|
667
|
-
}
|
|
689
|
+
for (const [trancheId, perConfig] of totals) {
|
|
668
690
|
const ftdAmounts: string[] = [];
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
691
|
+
for (const [ftdID, total] of perConfig) {
|
|
692
|
+
if (total.gt(0)) ftdAmounts[ftdID] = formatUnits(total);
|
|
693
|
+
}
|
|
694
|
+
if (ftdAmounts.length) {
|
|
695
|
+
currentEpochFtdAmounts.set(trancheId, ftdAmounts);
|
|
696
|
+
}
|
|
673
697
|
}
|
|
674
698
|
|
|
675
699
|
return currentEpochFtdAmounts;
|
|
@@ -708,6 +732,40 @@ export class UserLending {
|
|
|
708
732
|
return amountMap;
|
|
709
733
|
}
|
|
710
734
|
|
|
735
|
+
/**
|
|
736
|
+
* A request's outstanding amount in ASSETS (the deployment's stable token).
|
|
737
|
+
*
|
|
738
|
+
* The subgraph's `UserRequest.amountRequested` is denominated by request
|
|
739
|
+
* type. A DEPOSIT request holds assets: the deposit NFT's `assetAmount`,
|
|
740
|
+
* top-ups added, accepted and rejected amounts taken off. A WITHDRAWAL
|
|
741
|
+
* request holds tranche SHARES: the withdrawal NFT's `sharesAmount`,
|
|
742
|
+
* accepted shares taken off. So only a withdrawal is converted, at the
|
|
743
|
+
* tranche's current share price; a deposit passes through unchanged —
|
|
744
|
+
* pricing it as shares overstated it by the share price.
|
|
745
|
+
*/
|
|
746
|
+
private requestedAmountInAssets(
|
|
747
|
+
userRequest: UserRequestsSubgraph['userRequests'][number],
|
|
748
|
+
): string {
|
|
749
|
+
switch (userRequest.type) {
|
|
750
|
+
case 'DepositRequest':
|
|
751
|
+
return userRequest.amountRequested;
|
|
752
|
+
case 'WithdrawalRequest':
|
|
753
|
+
return this.convertSharesToAssets(
|
|
754
|
+
userRequest.amountRequested,
|
|
755
|
+
userRequest.tranche.balance,
|
|
756
|
+
userRequest.tranche.shares,
|
|
757
|
+
);
|
|
758
|
+
default: {
|
|
759
|
+
// Fail closed: a type this SDK does not know has no known
|
|
760
|
+
// unit, and pricing it either way would print a wrong figure.
|
|
761
|
+
const unknownType: string = userRequest.type;
|
|
762
|
+
throw new Error(
|
|
763
|
+
`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${unknownType}`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
711
769
|
convertSharesToAssets(
|
|
712
770
|
sharesAmount: string,
|
|
713
771
|
totalAssets: string,
|