@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.
@@ -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: utils$2.parseUnits(userRequest.tranche.shares).isZero()
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
- const totalSupply = userFTD.lendingPool.tranches.reduce((total, cur) => total.add(utils$2.parseEther(cur.shares)), ethers.BigNumber.from(0));
18344
- const amount = this.convertSharesToAssets(userFTD.trancheShares, userFTD.lendingPool.balance, utils$2.formatEther(totalSupply));
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
- const currentEpochFtdAmounts = new Map();
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
- const item = currentEpochFtdAmounts.get(userRequest.tranche.id);
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
- ftdAmounts[ftdID] = amountRequested;
18377
- currentEpochFtdAmounts.set(userRequest.tranche.id, ftdAmounts);
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; it reproduces kasu-ui's
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 with a LIVE
29508
- * remainder (cycle still open) → `partial`, outflow, −accepted
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, partly filled → `partial`, outflow, −accepted
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
- // pull the original value from the Initiated event so the row still
29540
- // carries "100 cancelled" instead of "0 cancelled".
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) || initiatedAmountOf(request.events)
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
- accepted < requested &&
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
- const partly = accepted < requested;
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) {
@@ -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: parseUnits(userRequest.tranche.shares).isZero()
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
- const totalSupply = userFTD.lendingPool.tranches.reduce((total, cur) => total.add(parseEther(cur.shares)), BigNumber.from(0));
18342
- const amount = this.convertSharesToAssets(userFTD.trancheShares, userFTD.lendingPool.balance, formatEther(totalSupply));
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
- const currentEpochFtdAmounts = new Map();
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
- const item = currentEpochFtdAmounts.get(userRequest.tranche.id);
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
- ftdAmounts[ftdID] = amountRequested;
18375
- currentEpochFtdAmounts.set(userRequest.tranche.id, ftdAmounts);
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; it reproduces kasu-ui's
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 with a LIVE
29506
- * remainder (cycle still open) → `partial`, outflow, −accepted
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, partly filled → `partial`, outflow, −accepted
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
- // pull the original value from the Initiated event so the row still
29538
- // carries "100 cancelled" instead of "0 cancelled".
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) || initiatedAmountOf(request.events)
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
- accepted < requested &&
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
- const partly = accepted < requested;
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
- * The amount originally asked for, always positive. A cancelled request
69
- * has its on-request `requestedAmount` zeroed by the subgraph, so the
70
- * original is recovered from the `Initiated` event (see
71
- * `initiatedAmount`).
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; it reproduces kasu-ui's
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 with a LIVE
217
- * remainder (cycle still open) → `partial`, outflow, −accepted
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, partly filled → `partial`, outflow, −accepted
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kasufinance/kasu-sdk",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "TypeScript SDK for the Kasu Finance protocol — pool data, portfolio analytics, and KYC-gated deposits and withdrawals on Base and XDC",
5
5
  "keywords": [
6
6
  "kasu",
@@ -82,10 +82,13 @@ export interface RequestState {
82
82
  statusCode: RequestStatusCode;
83
83
  kind: RequestKind;
84
84
  /**
85
- * The amount originally asked for, always positive. A cancelled request
86
- * has its on-request `requestedAmount` zeroed by the subgraph, so the
87
- * original is recovered from the `Initiated` event (see
88
- * `initiatedAmount`).
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 original amount survives on the `Initiated` event's
294
- * `assetAmount`. Recover from there so cancelled rows still carry the amount
295
- * the lender originally asked for.
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 initiatedAmountOf = (events: UserRequestEvent[]): number => {
298
- const initiated = initiatedEvent(events);
299
- return initiated ? Number(initiated.assetAmount || '0') : 0;
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; it reproduces kasu-ui's
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 with a LIVE
342
- * remainder (cycle still open) → `partial`, outflow, −accepted
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, partly filled → `partial`, outflow, −accepted
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
- // pull the original value from the Initiated event so the row still
373
- // carries "100 cancelled" instead of "0 cancelled".
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) || initiatedAmountOf(request.events)
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
- accepted < requested &&
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: partly ? 'partial' : 'complete',
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: parseUnits(userRequest.tranche.shares).isZero()
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
- const totalSupply = userFTD.lendingPool.tranches.reduce(
610
- (total, cur) => total.add(parseEther(cur.shares)),
611
- BigNumber.from(0),
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
- userFTD.lendingPool.balance,
617
- formatEther(totalSupply),
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
- const currentEpochFtdAmounts = new Map<string, string[]>();
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
- const item = currentEpochFtdAmounts.get(userRequest.tranche.id);
652
-
653
- const amountRequested = this.convertSharesToAssets(
654
- userRequest.amountRequested,
655
- userRequest.tranche.balance,
656
- userRequest.tranche.shares,
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
- if (item) {
660
- const clonedItem = [...item];
661
-
662
- clonedItem[ftdID] = amountRequested;
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
- continue;
667
- }
689
+ for (const [trancheId, perConfig] of totals) {
668
690
  const ftdAmounts: string[] = [];
669
-
670
- ftdAmounts[ftdID] = amountRequested;
671
-
672
- currentEpochFtdAmounts.set(userRequest.tranche.id, ftdAmounts);
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,