@kasufinance/kasu-sdk 2.7.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +162 -7
  2. package/dist/bundle.cjs.js +2292 -449
  3. package/dist/bundle.esm.js +2253 -451
  4. package/dist/domain/batch-proof.d.ts +210 -0
  5. package/dist/domain/batch-proof.js +384 -0
  6. package/dist/domain/batch-proof.js.map +1 -0
  7. package/dist/domain/explorer-urls.d.ts +33 -0
  8. package/dist/domain/explorer-urls.js +38 -0
  9. package/dist/domain/explorer-urls.js.map +1 -0
  10. package/dist/domain/fixed-term.d.ts +74 -0
  11. package/dist/domain/fixed-term.js +86 -0
  12. package/dist/domain/fixed-term.js.map +1 -0
  13. package/dist/domain/index.d.ts +12 -2
  14. package/dist/domain/index.js +7 -2
  15. package/dist/domain/index.js.map +1 -1
  16. package/dist/domain/partners.d.ts +29 -0
  17. package/dist/domain/partners.js +20 -0
  18. package/dist/domain/partners.js.map +1 -1
  19. package/dist/domain/requests.d.ts +24 -0
  20. package/dist/domain/requests.js +33 -0
  21. package/dist/domain/requests.js.map +1 -1
  22. package/dist/domain/sponsored-calls.d.ts +74 -0
  23. package/dist/domain/sponsored-calls.js +156 -0
  24. package/dist/domain/sponsored-calls.js.map +1 -0
  25. package/dist/domain/yields.d.ts +121 -0
  26. package/dist/domain/yields.js +151 -0
  27. package/dist/domain/yields.js.map +1 -0
  28. package/dist/facade/calls.d.ts +59 -0
  29. package/dist/facade/calls.js +89 -0
  30. package/dist/facade/calls.js.map +1 -0
  31. package/dist/facade/chain-configs.js +5 -0
  32. package/dist/facade/chain-configs.js.map +1 -1
  33. package/dist/facade/flows.d.ts +40 -3
  34. package/dist/facade/flows.js +36 -3
  35. package/dist/facade/flows.js.map +1 -1
  36. package/dist/facade/index.d.ts +3 -1
  37. package/dist/facade/index.js +1 -0
  38. package/dist/facade/index.js.map +1 -1
  39. package/dist/facade/kasu.d.ts +8 -2
  40. package/dist/facade/kasu.js +17 -4
  41. package/dist/facade/kasu.js.map +1 -1
  42. package/dist/facade/strategies.d.ts +43 -2
  43. package/dist/facade/strategies.js +80 -4
  44. package/dist/facade/strategies.js.map +1 -1
  45. package/dist/facade/types.d.ts +18 -0
  46. package/dist/flows/batch-errors.d.ts +74 -0
  47. package/dist/flows/batch-errors.js +83 -0
  48. package/dist/flows/batch-errors.js.map +1 -0
  49. package/dist/flows/batch-submission.d.ts +223 -0
  50. package/dist/flows/batch-submission.js +321 -0
  51. package/dist/flows/batch-submission.js.map +1 -0
  52. package/dist/flows/deposit-flow.d.ts +128 -4
  53. package/dist/flows/deposit-flow.js +233 -4
  54. package/dist/flows/deposit-flow.js.map +1 -1
  55. package/dist/flows/index.d.ts +6 -2
  56. package/dist/flows/index.js +3 -1
  57. package/dist/flows/index.js.map +1 -1
  58. package/dist/flows/withdraw-flow.d.ts +95 -4
  59. package/dist/flows/withdraw-flow.js +107 -2
  60. package/dist/flows/withdraw-flow.js.map +1 -1
  61. package/dist/services/DataService/queries.js +1 -0
  62. package/dist/services/DataService/queries.js.map +1 -1
  63. package/dist/services/UserLending/user-lending.d.ts +13 -0
  64. package/dist/services/UserLending/user-lending.js +25 -4
  65. package/dist/services/UserLending/user-lending.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/domain/batch-proof.ts +576 -0
  68. package/src/domain/explorer-urls.ts +74 -0
  69. package/src/domain/fixed-term.ts +115 -0
  70. package/src/domain/index.ts +56 -0
  71. package/src/domain/partners.ts +35 -0
  72. package/src/domain/requests.ts +37 -0
  73. package/src/domain/sponsored-calls.ts +213 -0
  74. package/src/domain/yields.ts +189 -0
  75. package/src/facade/calls.ts +135 -0
  76. package/src/facade/chain-configs.ts +5 -0
  77. package/src/facade/flows.ts +35 -2
  78. package/src/facade/index.ts +3 -0
  79. package/src/facade/kasu.ts +32 -2
  80. package/src/facade/strategies.ts +98 -7
  81. package/src/facade/types.ts +19 -0
  82. package/src/flows/batch-errors.ts +118 -0
  83. package/src/flows/batch-submission.ts +511 -0
  84. package/src/flows/deposit-flow.ts +379 -7
  85. package/src/flows/index.ts +34 -1
  86. package/src/flows/withdraw-flow.ts +219 -10
  87. package/src/services/DataService/queries.ts +1 -0
  88. package/src/services/UserLending/user-lending.ts +38 -10
@@ -0,0 +1,115 @@
1
+ import { TrancheData } from '../services/DataService/types';
2
+
3
+ /**
4
+ * Fixed-term deposits. A fixed-term config locks a deposit for N epochs in
5
+ * exchange for a fixed rate. The configs ride on `TrancheData.fixedTermConfig`;
6
+ * `'0'` is the variable-rate sentinel (no lock) that `requestDepositWithKyc`
7
+ * takes as `fixedTermConfigId`.
8
+ *
9
+ * Numbers only. Turning a lock into "12 months" or "3 weeks" is copy, and
10
+ * stays in each application — what lives here is the arithmetic both of those
11
+ * strings have to agree on, so a selector and a details table can never state
12
+ * one term two ways.
13
+ */
14
+
15
+ /** One fixed-term config, exactly as `TrancheData` carries it. */
16
+ export type FixedTermConfig = TrancheData['fixedTermConfig'][number];
17
+
18
+ /**
19
+ * One epoch's nominal length in days. A lock is counted in epochs, and the
20
+ * loan-agreement text states its term from the same 7-day epoch.
21
+ */
22
+ export const DAYS_PER_EPOCH = 7;
23
+
24
+ /**
25
+ * The average calendar month, 365.25 / 12. A lock is counted in epochs, so a
26
+ * "12-month" term is 52 epochs = 364 days, and only the average-month divisor
27
+ * lands that on 12 (364 / 30.4375 = 11.96 → 12).
28
+ */
29
+ export const DAYS_PER_MONTH = 30.4375;
30
+
31
+ /**
32
+ * The shortest lock stated in months. Below it a term reads in weeks or days,
33
+ * so nothing may claim a month count for it — a 3-epoch lock is 21 days, which
34
+ * the average-month divisor would otherwise round UP to one month.
35
+ */
36
+ export const MIN_DAYS_FOR_MONTHS = 30;
37
+
38
+ /**
39
+ * `'0'`, empty or absent is the variable-rate sentinel, not a fixed term.
40
+ * Every surface that tags a rate "variable" or "fixed" must derive the tag
41
+ * through this, so no screen can hardcode a tag that contradicts the rate
42
+ * beside it.
43
+ */
44
+ export function isFixedTermConfigId(
45
+ configId: string | null | undefined,
46
+ ): boolean {
47
+ if (configId == null) return false;
48
+ const trimmed = configId.trim();
49
+ return trimmed !== '' && trimmed !== '0';
50
+ }
51
+
52
+ /**
53
+ * The fixed-term configs this wallet may actually select on a tranche: a
54
+ * config open to `Everyone`, or an `AllowlistedOnly` config whose allowlist
55
+ * names the wallet. Anything else is dropped (hide-ineligible): the contract
56
+ * refuses a deposit into an allowlisted config from a wallet that is not on
57
+ * it, so offering one is offering a transaction that reverts.
58
+ *
59
+ * Address matching is case-insensitive. An allowlist entry whose
60
+ * `isAllowlisted` is `false` is a wallet that was REMOVED — the subgraph keeps
61
+ * the entity and flips the flag — so it does not count. An entry that carries
62
+ * no flag at all counts by its presence, which is how every entry read before
63
+ * 2.9.0 arrived (the query did not select the field).
64
+ *
65
+ * `address` `null` / `undefined` — no wallet yet — sees only the `Everyone`
66
+ * configs. Order is the tranche's own.
67
+ */
68
+ export function availableFixedTermConfigs(
69
+ tranche: Pick<TrancheData, 'fixedTermConfig'> | null | undefined,
70
+ address: string | null | undefined,
71
+ ): FixedTermConfig[] {
72
+ if (!tranche) return [];
73
+ const lower = address?.toLowerCase();
74
+ return tranche.fixedTermConfig.filter(
75
+ (config) =>
76
+ config.fixedTermDepositStatus === 'Everyone' ||
77
+ (lower != null &&
78
+ lower !== '' &&
79
+ config.fixedTermDepositAllowlist.some(
80
+ (entry) =>
81
+ (entry.isAllowlisted as boolean | undefined) !==
82
+ false && entry.userId.toLowerCase() === lower,
83
+ )),
84
+ );
85
+ }
86
+
87
+ /**
88
+ * A lock's length in days: `epochLockDuration × 7`.
89
+ *
90
+ * FAILS CLOSED: an unparseable, non-finite, zero or negative epoch count
91
+ * returns `NaN`, never `0` — a zero-day lock is not a term, and a consumer
92
+ * that rendered it would state one.
93
+ *
94
+ * @param epochLockDuration the config's `epochLockDuration`, a decimal string.
95
+ */
96
+ export function lockDurationDays(epochLockDuration: string): number {
97
+ const epochs = parseFloat(epochLockDuration);
98
+ if (!Number.isFinite(epochs) || epochs <= 0) return NaN;
99
+ return epochs * DAYS_PER_EPOCH;
100
+ }
101
+
102
+ /**
103
+ * A lock in WHOLE months, or `null` when the epoch count is not a positive
104
+ * number or the lock is shorter than `MIN_DAYS_FOR_MONTHS`.
105
+ *
106
+ * Unlike a year-rounding formatter it never rolls up into years: a 104-epoch
107
+ * lock is 24 months. A consumer that states years does that conversion in its
108
+ * own words.
109
+ */
110
+ export function lockDurationMonths(epochLockDuration: string): number | null {
111
+ const days = lockDurationDays(epochLockDuration);
112
+ if (Number.isNaN(days) || days < MIN_DAYS_FOR_MONTHS) return null;
113
+ const months = Math.round(days / DAYS_PER_MONTH);
114
+ return months > 0 ? months : null;
115
+ }
@@ -37,6 +37,25 @@ export {
37
37
  } from './au-minimum';
38
38
  export type { AuMinimumInput } from './au-minimum';
39
39
 
40
+ export {
41
+ DepositBatchMismatchError,
42
+ ENTRY_POINT_V07,
43
+ proveDepositInReceipt,
44
+ proveUserOperationInReceipt,
45
+ validateDepositBatchCalls,
46
+ } from './batch-proof';
47
+ export type {
48
+ DepositBatchCall,
49
+ DepositBatchExpectation,
50
+ DepositProof,
51
+ DepositProofExpectation,
52
+ ReceiptLike,
53
+ ReceiptLog,
54
+ UserOperationProof,
55
+ UserOperationProofExpectation,
56
+ ValidatedDepositBatch,
57
+ } from './batch-proof';
58
+
40
59
  export {
41
60
  ceilToCents,
42
61
  floorToCents,
@@ -49,10 +68,25 @@ export {
49
68
  } from './deposit-bounds';
50
69
  export type { BoundShortcuts, DepositBounds } from './deposit-bounds';
51
70
 
71
+ export { explorerAddressUrl, explorerTxUrl } from './explorer-urls';
72
+ export type { ExplorerConfig } from './explorer-urls';
73
+
74
+ export {
75
+ availableFixedTermConfigs,
76
+ DAYS_PER_EPOCH,
77
+ DAYS_PER_MONTH,
78
+ isFixedTermConfigId,
79
+ lockDurationDays,
80
+ lockDurationMonths,
81
+ MIN_DAYS_FOR_MONTHS,
82
+ } from './fixed-term';
83
+ export type { FixedTermConfig } from './fixed-term';
84
+
52
85
  export {
53
86
  APXIUM,
54
87
  getCreditOriginator,
55
88
  getInstitutionalLender,
89
+ getStrategyRegion,
56
90
  INVOICEMATE,
57
91
  RIXON_CAPITAL,
58
92
  } from './partners';
@@ -88,6 +122,7 @@ export {
88
122
  export type { BestTranche } from './pools';
89
123
 
90
124
  export {
125
+ cancelDeadline,
91
126
  countSubmissions,
92
127
  deriveRequestState,
93
128
  firstSubmissionTimestamp,
@@ -116,6 +151,17 @@ export type {
116
151
  SettlementWindowState,
117
152
  } from './settlement';
118
153
 
154
+ export {
155
+ encodeCancelDepositCall,
156
+ encodeCancelWithdrawalCall,
157
+ encodeRequestWithdrawalCall,
158
+ encodeWithdrawAtExpiryCall,
159
+ SPONSORED_CALL_KINDS,
160
+ SPONSORED_CALL_SELECTORS,
161
+ SponsoredCallInputError,
162
+ } from './sponsored-calls';
163
+ export type { SponsoredCall, SponsoredCallKind } from './sponsored-calls';
164
+
119
165
  export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
120
166
 
121
167
  export {
@@ -141,3 +187,13 @@ export {
141
187
  isUserRejected,
142
188
  } from './wallet-errors';
143
189
  export type { WalletFailure } from './wallet-errors';
190
+
191
+ export {
192
+ accruedInterest,
193
+ dailyInterest,
194
+ projectedAnnualEarnings,
195
+ SECONDS_PER_WEEK,
196
+ weeklyInterest,
197
+ weightedAverageApy,
198
+ } from './yields';
199
+ export type { InterestSlice, PoolAccrual } from './yields';
@@ -7,19 +7,36 @@
7
7
  export interface StrategyPartner {
8
8
  name: string;
9
9
  url: string;
10
+ /**
11
+ * The region of the strategies this partner takes part in, as every Kasu
12
+ * surface names it. For a credit originator it is THE strategy's region —
13
+ * `getStrategyRegion` reads it from here — and it is the word that goes
14
+ * into the wallet-signed loan-agreement message, so it must never be
15
+ * stated from anywhere else.
16
+ *
17
+ * Apxium originates in Australia. InvoiceMate (PayFi) funds cross-border
18
+ * payment corridors no single jurisdiction names, so its region is a
19
+ * SCOPE — `'Global'` — rather than a country: naming one would be a claim
20
+ * about where its End Borrowers sit. Rixon Capital is the senior lender in
21
+ * Apxium's Australian strategies.
22
+ */
23
+ region: string;
10
24
  }
11
25
 
12
26
  export const APXIUM: StrategyPartner = {
13
27
  name: 'Apxium',
14
28
  url: 'https://www.apxium.com/',
29
+ region: 'Australia',
15
30
  };
16
31
  export const INVOICEMATE: StrategyPartner = {
17
32
  name: 'InvoiceMate',
18
33
  url: 'https://invoicemate.net/',
34
+ region: 'Global',
19
35
  };
20
36
  export const RIXON_CAPITAL: StrategyPartner = {
21
37
  name: 'Rixon Capital',
22
38
  url: 'https://rixon.capital/',
39
+ region: 'Australia',
23
40
  };
24
41
 
25
42
  /**
@@ -64,3 +81,21 @@ export function getInstitutionalLender(
64
81
  ): StrategyPartner | null {
65
82
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
66
83
  }
84
+
85
+ /**
86
+ * The strategy's region — `'Australia'` for the Apxium family, `'Global'` for
87
+ * PayFi — derived from its credit originator, exactly as every surface that
88
+ * states a region must derive it.
89
+ *
90
+ * LOAD-BEARING ON THE MONEY PATH: this is the `region` a lender's wallet signs
91
+ * in the loan-agreement message (`buildLoanAgreementSignMessage`). The backend
92
+ * rebuilds that message from whatever region the client sends and treats it
93
+ * as display-only, so nothing but this function keeps the signed word equal to
94
+ * the word on screen. Never state a region from a constant — that is how a
95
+ * PayFi lender came to pick "Global · Junior" and sign "Australia".
96
+ *
97
+ * Replace the inference with a per-pool field (in the SDK) when one ships.
98
+ */
99
+ export function getStrategyRegion(pool: PoolNameSignal): string {
100
+ return getCreditOriginator(pool).region;
101
+ }
@@ -4,6 +4,8 @@ import {
4
4
  UserRequestEvent,
5
5
  } from '../services/UserLending/types';
6
6
 
7
+ import { deriveCycleDates } from './settlement';
8
+
7
9
  /**
8
10
  * The lending-request view model, as CODES.
9
11
  *
@@ -239,6 +241,41 @@ export function isCycleClosed(request: Pick<UserRequest, 'canCancel'>): boolean
239
241
  return !request.canCancel;
240
242
  }
241
243
 
244
+ /**
245
+ * Until when can THIS request still be cancelled? — unix seconds, or `null`
246
+ * when it cannot be cancelled at all, or when no epoch boundary is loaded.
247
+ *
248
+ * The answer is the next cycle close (`deriveCycleDates(...).close`): the
249
+ * clearing window opens there, and the contract refuses a cancel while the
250
+ * pool's clearing is pending (`CannotCancelRequestIfClearingIsPending`).
251
+ * Inside a window whose clearing already ENDED, a still-cancellable request is
252
+ * queued for the next cycle, so its deadline is next week's close — which is
253
+ * exactly what `deriveCycleDates` rolls forward to.
254
+ *
255
+ * `null` unless the request is BOTH:
256
+ *
257
+ * - `canCancel` — the SDK's per-pool signal (see `isCycleClosed`);
258
+ * - live: `statusCode` `pending`, or `partial` (a partly-filled withdrawal
259
+ * whose remainder is back in the queue — a resolved partial has
260
+ * `canCancel: false`). A cancelled, forced, reallocated, rejected or complete
261
+ * request has nothing left to cancel, whatever `canCancel` says.
262
+ *
263
+ * `canCancel` is read ONCE, when the request was fetched. A request held
264
+ * across a cycle boundary (`nextCycleBoundary`) must be refetched before this
265
+ * is shown again — the deadline is a hint, the contract is the gate.
266
+ */
267
+ export function cancelDeadline(
268
+ request: Pick<RequestState, 'canCancel' | 'statusCode'>,
269
+ nextEpochStart: number | undefined,
270
+ nowSeconds: number,
271
+ ): number | null {
272
+ if (!request.canCancel) return null;
273
+ if (request.statusCode !== 'pending' && request.statusCode !== 'partial') {
274
+ return null;
275
+ }
276
+ return deriveCycleDates(nextEpochStart, nowSeconds)?.close ?? null;
277
+ }
278
+
242
279
  const isCancelled = (events: UserRequestEvent[]): boolean =>
243
280
  events.some((e) => e.requestType === 'Cancelled');
244
281
 
@@ -0,0 +1,213 @@
1
+ import { BigNumber, BigNumberish, constants, utils } from 'ethers';
2
+
3
+ import { ILendingPoolManagerAbi__factory } from '../contracts/factories/ILendingPoolManagerAbi__factory';
4
+
5
+ /**
6
+ * The single-call `LendingPoolManager` requests a sponsored relay may carry,
7
+ * encoded by the SDK so no application hand-builds calldata or hand-maps the
8
+ * relay's `kind` for them.
9
+ *
10
+ * Every function here is PURE: addresses and ids in, `{ kind, call }` out.
11
+ * Reading the chain (the shares a withdrawal is for) is the facade's job —
12
+ * `kasu.calls` — and sending is the application's.
13
+ *
14
+ * Each call is `{ to, data }` and nothing else: no `value` (none of these
15
+ * functions is payable), no `from` (the manager acts for `msg.sender`, the
16
+ * lender's own account, which is why none of them takes a user argument).
17
+ * `data` is ethers' ABI encoding, which is canonical by construction — the
18
+ * relay re-encodes what it decodes and refuses anything that differs, so a
19
+ * hand-tweaked blob never gets through, and an SDK-built one always matches.
20
+ */
21
+
22
+ /**
23
+ * The relay's name for each shape. Carried beside the call so the app posts
24
+ * `kind` from the SDK, never from a table of its own — a request declaring the
25
+ * wrong kind is refused (`invalid_calls`), so a hand-mapped table that drifts
26
+ * turns into a lender who cannot withdraw.
27
+ */
28
+ export const SPONSORED_CALL_KINDS = [
29
+ /** `LendingPoolManager.requestWithdrawal(pool, tranche, shares)`. */
30
+ 'withdrawal',
31
+ /** `LendingPoolManager.cancelDepositRequest(pool, dNftID)`. */
32
+ 'cancel_deposit',
33
+ /** `LendingPoolManager.cancelWithdrawalRequest(pool, wNftID)`. */
34
+ 'cancel_withdrawal',
35
+ /** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
36
+ 'withdraw_at_expiry',
37
+ ] as const;
38
+
39
+ export type SponsoredCallKind = (typeof SPONSORED_CALL_KINDS)[number];
40
+
41
+ /** One encoded call and the relay `kind` that names its shape. */
42
+ export interface SponsoredCall {
43
+ kind: SponsoredCallKind;
44
+ /**
45
+ * Exactly `{ to, data }` — structurally the flows' `BatchCall`. Post it as
46
+ * the relay's single call; do not add keys to it.
47
+ */
48
+ call: { to: string; data: string };
49
+ }
50
+
51
+ /**
52
+ * Widened to the plain `Interface`: the typechain one overloads
53
+ * `encodeFunctionData` per literal name, and the name here comes from
54
+ * `FUNCTION_OF`.
55
+ */
56
+ const LENDING_POOL_MANAGER: utils.Interface =
57
+ ILendingPoolManagerAbi__factory.createInterface();
58
+
59
+ /** The `LendingPoolManager` function behind each kind. */
60
+ const FUNCTION_OF: Readonly<Record<SponsoredCallKind, string>> = Object.freeze({
61
+ withdrawal: 'requestWithdrawal',
62
+ cancel_deposit: 'cancelDepositRequest',
63
+ cancel_withdrawal: 'cancelWithdrawalRequest',
64
+ withdraw_at_expiry: 'requestFixedTermDepositWithdrawal',
65
+ });
66
+
67
+ /**
68
+ * The 4-byte selector of each kind's function, DERIVED from the generated
69
+ * `ILendingPoolManager` ABI — never typed in by hand, so an ABI change moves
70
+ * it and the cross-check spec against the relay's own pins goes red.
71
+ */
72
+ export const SPONSORED_CALL_SELECTORS: Readonly<
73
+ Record<SponsoredCallKind, string>
74
+ > = Object.freeze({
75
+ withdrawal: LENDING_POOL_MANAGER.getSighash(FUNCTION_OF.withdrawal),
76
+ cancel_deposit: LENDING_POOL_MANAGER.getSighash(FUNCTION_OF.cancel_deposit),
77
+ cancel_withdrawal: LENDING_POOL_MANAGER.getSighash(
78
+ FUNCTION_OF.cancel_withdrawal,
79
+ ),
80
+ withdraw_at_expiry: LENDING_POOL_MANAGER.getSighash(
81
+ FUNCTION_OF.withdraw_at_expiry,
82
+ ),
83
+ });
84
+
85
+ /**
86
+ * What an encoder throws for an input it will not encode. `code` names the
87
+ * argument; the message names no party, so a wallet-rejection classifier can
88
+ * never read it as something the lender did. Nothing has been sent when this
89
+ * is thrown.
90
+ */
91
+ export class SponsoredCallInputError extends Error {
92
+ readonly code: string;
93
+
94
+ constructor(code: string) {
95
+ super(`Sponsored call input refused (${code})`);
96
+ this.name = 'SponsoredCallInputError';
97
+ this.code = code;
98
+ Object.setPrototypeOf(this, SponsoredCallInputError.prototype);
99
+ }
100
+ }
101
+
102
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
103
+
104
+ function assertAddress(value: unknown, code: string): asserts value is string {
105
+ if (typeof value !== 'string' || !ADDRESS_RE.test(value)) {
106
+ throw new SponsoredCallInputError(code);
107
+ }
108
+ }
109
+
110
+ /** A uint256 — an NFT or lock id. Zero is a valid id as far as encoding goes. */
111
+ function toUint256(value: BigNumberish, code: string): BigNumber {
112
+ let parsed: BigNumber;
113
+ try {
114
+ parsed = BigNumber.from(value);
115
+ } catch {
116
+ throw new SponsoredCallInputError(code);
117
+ }
118
+ if (parsed.isNegative() || parsed.gt(constants.MaxUint256)) {
119
+ throw new SponsoredCallInputError(code);
120
+ }
121
+ return parsed;
122
+ }
123
+
124
+ function encode(
125
+ kind: SponsoredCallKind,
126
+ lendingPoolManager: string,
127
+ args: readonly unknown[],
128
+ ): SponsoredCall {
129
+ return {
130
+ kind,
131
+ call: {
132
+ to: lendingPoolManager,
133
+ data: LENDING_POOL_MANAGER.encodeFunctionData(
134
+ FUNCTION_OF[kind],
135
+ args,
136
+ ),
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * `requestWithdrawal(poolId, trancheId, shares)` — `withdrawal`.
143
+ *
144
+ * `shares` are tranche SHARES, not assets: `kasu.calls.requestWithdrawal`
145
+ * converts an asset amount (or `'max'`) into them exactly as
146
+ * `UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax` do. Zero
147
+ * shares is refused here: the contract reverts on it
148
+ * (`RequestWithdrawSharesAreZero`) and the relay refuses it before that.
149
+ */
150
+ export function encodeRequestWithdrawalCall(
151
+ lendingPoolManager: string,
152
+ poolId: string,
153
+ trancheId: string,
154
+ shares: BigNumberish,
155
+ ): SponsoredCall {
156
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
157
+ assertAddress(poolId, 'pool');
158
+ assertAddress(trancheId, 'tranche');
159
+ const amount = toUint256(shares, 'shares');
160
+ if (amount.isZero()) throw new SponsoredCallInputError('zero-shares');
161
+ return encode('withdrawal', lendingPoolManager, [
162
+ poolId,
163
+ trancheId,
164
+ amount,
165
+ ]);
166
+ }
167
+
168
+ /** `cancelDepositRequest(poolId, dNftId)` — `cancel_deposit`. */
169
+ export function encodeCancelDepositCall(
170
+ lendingPoolManager: string,
171
+ poolId: string,
172
+ dNftId: BigNumberish,
173
+ ): SponsoredCall {
174
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
175
+ assertAddress(poolId, 'pool');
176
+ return encode('cancel_deposit', lendingPoolManager, [
177
+ poolId,
178
+ toUint256(dNftId, 'nft-id'),
179
+ ]);
180
+ }
181
+
182
+ /** `cancelWithdrawalRequest(poolId, wNftId)` — `cancel_withdrawal`. */
183
+ export function encodeCancelWithdrawalCall(
184
+ lendingPoolManager: string,
185
+ poolId: string,
186
+ wNftId: BigNumberish,
187
+ ): SponsoredCall {
188
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
189
+ assertAddress(poolId, 'pool');
190
+ return encode('cancel_withdrawal', lendingPoolManager, [
191
+ poolId,
192
+ toUint256(wNftId, 'nft-id'),
193
+ ]);
194
+ }
195
+
196
+ /**
197
+ * `requestFixedTermDepositWithdrawal(poolId, fixedTermDepositId)` —
198
+ * `withdraw_at_expiry`. The contract function takes the pool and the LOCK id
199
+ * only (no tranche): the manager forwards to the fixed-term contract on the
200
+ * lender's behalf, which knows the lock's tranche.
201
+ */
202
+ export function encodeWithdrawAtExpiryCall(
203
+ lendingPoolManager: string,
204
+ poolId: string,
205
+ fixedTermDepositId: BigNumberish,
206
+ ): SponsoredCall {
207
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
208
+ assertAddress(poolId, 'pool');
209
+ return encode('withdraw_at_expiry', lendingPoolManager, [
210
+ poolId,
211
+ toUint256(fixedTermDepositId, 'lock-id'),
212
+ ]);
213
+ }