@kasufinance/kasu-sdk 2.10.1 → 2.10.3
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 +155 -23
- package/dist/bundle.esm.js +154 -24
- package/dist/domain/batch-proof.js +19 -12
- package/dist/domain/batch-proof.js.map +1 -1
- package/dist/domain/index.d.ts +1 -1
- package/dist/domain/index.js +1 -1
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/sponsored-calls.d.ts +34 -6
- package/dist/domain/sponsored-calls.js +65 -7
- package/dist/domain/sponsored-calls.js.map +1 -1
- package/dist/facade/strategies.js +12 -0
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +10 -0
- package/dist/flows/batch-submission.d.ts +21 -4
- package/dist/flows/batch-submission.js +59 -5
- package/dist/flows/batch-submission.js.map +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/batch-proof.ts +18 -12
- package/src/domain/index.ts +1 -0
- package/src/domain/sponsored-calls.ts +79 -13
- package/src/facade/strategies.ts +13 -0
- package/src/facade/types.ts +15 -1
- package/src/flows/batch-submission.ts +63 -6
- package/src/flows/index.ts +1 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { BigNumber, BigNumberish, constants, utils } from 'ethers';
|
|
2
2
|
|
|
3
|
+
import { IERC20MetadataAbi__factory } from '../contracts/factories/IERC20MetadataAbi__factory';
|
|
3
4
|
import { ILendingPoolManagerAbi__factory } from '../contracts/factories/ILendingPoolManagerAbi__factory';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* The single-call
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The single-call requests a sponsored relay may carry — the four
|
|
8
|
+
* `LendingPoolManager` requests, plus the stable asset's own `transfer` (P2P
|
|
9
|
+
* Send) — encoded by the SDK so no application hand-builds calldata or
|
|
10
|
+
* hand-maps the relay's `kind` for them.
|
|
9
11
|
*
|
|
10
12
|
* Every function here is PURE: addresses and ids in, `{ kind, call }` out.
|
|
11
13
|
* Reading the chain (the shares a withdrawal is for) is the facade's job —
|
|
@@ -34,6 +36,11 @@ export const SPONSORED_CALL_KINDS = [
|
|
|
34
36
|
'cancel_withdrawal',
|
|
35
37
|
/** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
|
|
36
38
|
'withdraw_at_expiry',
|
|
39
|
+
/**
|
|
40
|
+
* `stableAsset.transfer(to, amount)` — P2P Send, to the chain's stable
|
|
41
|
+
* asset rather than the manager. See {@link encodeStableTransfer}.
|
|
42
|
+
*/
|
|
43
|
+
'transfer',
|
|
37
44
|
] as const;
|
|
38
45
|
|
|
39
46
|
export type SponsoredCallKind = (typeof SPONSORED_CALL_KINDS)[number];
|
|
@@ -56,18 +63,26 @@ export interface SponsoredCall {
|
|
|
56
63
|
const LENDING_POOL_MANAGER: utils.Interface =
|
|
57
64
|
ILendingPoolManagerAbi__factory.createInterface();
|
|
58
65
|
|
|
59
|
-
/** The `
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/** The generated ERC-20 interface — the `transfer` kind's one function. */
|
|
67
|
+
const ERC20: utils.Interface = IERC20MetadataAbi__factory.createInterface();
|
|
68
|
+
|
|
69
|
+
/** The kinds whose call goes to the `LendingPoolManager`. */
|
|
70
|
+
type LendingPoolManagerKind = Exclude<SponsoredCallKind, 'transfer'>;
|
|
71
|
+
|
|
72
|
+
/** The `LendingPoolManager` function behind each manager kind. */
|
|
73
|
+
const FUNCTION_OF: Readonly<Record<LendingPoolManagerKind, string>> =
|
|
74
|
+
Object.freeze({
|
|
75
|
+
withdrawal: 'requestWithdrawal',
|
|
76
|
+
cancel_deposit: 'cancelDepositRequest',
|
|
77
|
+
cancel_withdrawal: 'cancelWithdrawalRequest',
|
|
78
|
+
withdraw_at_expiry: 'requestFixedTermDepositWithdrawal',
|
|
79
|
+
});
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
82
|
* The 4-byte selector of each kind's function, DERIVED from the generated
|
|
69
|
-
* `ILendingPoolManager`
|
|
70
|
-
* it and the cross-check spec
|
|
83
|
+
* ABIs (`ILendingPoolManager`, and `IERC20Metadata` for `transfer`) — never
|
|
84
|
+
* typed in by hand, so an ABI change moves it and the cross-check spec
|
|
85
|
+
* against the relay's own pins goes red.
|
|
71
86
|
*/
|
|
72
87
|
export const SPONSORED_CALL_SELECTORS: Readonly<
|
|
73
88
|
Record<SponsoredCallKind, string>
|
|
@@ -80,6 +95,7 @@ export const SPONSORED_CALL_SELECTORS: Readonly<
|
|
|
80
95
|
withdraw_at_expiry: LENDING_POOL_MANAGER.getSighash(
|
|
81
96
|
FUNCTION_OF.withdraw_at_expiry,
|
|
82
97
|
),
|
|
98
|
+
transfer: ERC20.getSighash('transfer'),
|
|
83
99
|
});
|
|
84
100
|
|
|
85
101
|
/**
|
|
@@ -122,7 +138,7 @@ function toUint256(value: BigNumberish, code: string): BigNumber {
|
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
function encode(
|
|
125
|
-
kind:
|
|
141
|
+
kind: LendingPoolManagerKind,
|
|
126
142
|
lendingPoolManager: string,
|
|
127
143
|
args: readonly unknown[],
|
|
128
144
|
): SponsoredCall {
|
|
@@ -211,3 +227,53 @@ export function encodeWithdrawAtExpiryCall(
|
|
|
211
227
|
toUint256(fixedTermDepositId, 'lock-id'),
|
|
212
228
|
]);
|
|
213
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* `stableAsset.transfer(to, amount)` — `transfer`: P2P Send of the chain's
|
|
233
|
+
* stable asset from the sender's own account (`msg.sender`) to `to`.
|
|
234
|
+
*
|
|
235
|
+
* `amount` is in the token's BASE units (6 decimals for USDC), and must be
|
|
236
|
+
* greater than zero. `to` must be a well-formed, non-zero address (a
|
|
237
|
+
* mixed-case one with a valid EIP-55 checksum). That is
|
|
238
|
+
* ALL this encoder judges, because it is all a pure function can know.
|
|
239
|
+
*
|
|
240
|
+
* The recipient POLICY is the relay's job and is deliberately not restated
|
|
241
|
+
* here: it refuses the sender's own account, the stable asset, the
|
|
242
|
+
* `LendingPoolManager`, the precompile range, any address with contract code
|
|
243
|
+
* (read on chain at submission), a sender our identity store does not hold
|
|
244
|
+
* as verified, and anything over the sender's daily count and amount caps.
|
|
245
|
+
* A call this encoder accepts can therefore still be refused — the app maps
|
|
246
|
+
* the relay's refusal codes to its own words.
|
|
247
|
+
*
|
|
248
|
+
* Pass `stableAsset` from the chain config (`CHAIN_CONFIGS[chain].stableAsset
|
|
249
|
+
* .address`) — the relay compares `to` against its own configured token and
|
|
250
|
+
* refuses any other contract.
|
|
251
|
+
*/
|
|
252
|
+
export function encodeStableTransfer(input: {
|
|
253
|
+
stableAsset: string;
|
|
254
|
+
to: string;
|
|
255
|
+
amount: BigNumberish;
|
|
256
|
+
}): SponsoredCall {
|
|
257
|
+
assertAddress(input.stableAsset, 'stable-asset');
|
|
258
|
+
assertAddress(input.to, 'recipient');
|
|
259
|
+
// A mixed-case recipient carries an EIP-55 checksum, and one that does not
|
|
260
|
+
// match is a mistyped address: refuse it as `recipient` here rather than
|
|
261
|
+
// let the ABI coder throw its own error at the encode below.
|
|
262
|
+
try {
|
|
263
|
+
utils.getAddress(input.to);
|
|
264
|
+
} catch {
|
|
265
|
+
throw new SponsoredCallInputError('recipient');
|
|
266
|
+
}
|
|
267
|
+
if (BigNumber.from(input.to).isZero()) {
|
|
268
|
+
throw new SponsoredCallInputError('zero-recipient');
|
|
269
|
+
}
|
|
270
|
+
const amount = toUint256(input.amount, 'amount');
|
|
271
|
+
if (amount.isZero()) throw new SponsoredCallInputError('zero-amount');
|
|
272
|
+
return {
|
|
273
|
+
kind: 'transfer',
|
|
274
|
+
call: {
|
|
275
|
+
to: input.stableAsset,
|
|
276
|
+
data: ERC20.encodeFunctionData('transfer', [input.to, amount]),
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
package/src/facade/strategies.ts
CHANGED
|
@@ -215,7 +215,9 @@ export class StrategiesFacade {
|
|
|
215
215
|
return {
|
|
216
216
|
id: pool.id,
|
|
217
217
|
name: pool.poolName,
|
|
218
|
+
subheading: nonEmptyOrUndefined(pool.subheading),
|
|
218
219
|
description: pool.description,
|
|
220
|
+
liteDescription: nonEmptyOrUndefined(pool.liteDescription),
|
|
219
221
|
isActive: pool.isActive,
|
|
220
222
|
apy: pool.apy,
|
|
221
223
|
tvl: pool.totalValueLocked,
|
|
@@ -231,6 +233,17 @@ export class StrategiesFacade {
|
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
/**
|
|
237
|
+
* A CMS text field as a string, or `undefined` when it is absent, not a
|
|
238
|
+
* string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
|
|
239
|
+
* subheading must read as "none", not render an empty line.
|
|
240
|
+
*/
|
|
241
|
+
function nonEmptyOrUndefined(value: unknown): string | undefined {
|
|
242
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
243
|
+
? value
|
|
244
|
+
: undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
234
247
|
/**
|
|
235
248
|
* A CMS "numeric" field as a finite number, or `null`. An empty or blank
|
|
236
249
|
* string is UNAUTHORED, not zero.
|
package/src/facade/types.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { BigNumberish, BytesLike } from 'ethers';
|
|
2
2
|
|
|
3
3
|
import { ContractAddresses, SdkConfigOptions } from '../sdk-config';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
LendingTotals,
|
|
6
|
+
PoolOverview,
|
|
7
|
+
TrancheData,
|
|
8
|
+
} from '../services/DataService/types';
|
|
5
9
|
import {
|
|
6
10
|
PortfolioLendingPool,
|
|
7
11
|
PortfolioSummary,
|
|
@@ -106,7 +110,17 @@ export interface KasuOptions {
|
|
|
106
110
|
export interface Strategy {
|
|
107
111
|
id: string;
|
|
108
112
|
name: string;
|
|
113
|
+
/**
|
|
114
|
+
* The CMS subheading (the borrower type shown under the name). Absent when
|
|
115
|
+
* the record has none, or an empty one.
|
|
116
|
+
*/
|
|
117
|
+
subheading?: string;
|
|
109
118
|
description: string;
|
|
119
|
+
/**
|
|
120
|
+
* The CMS short description (`liteDescription`) for compact surfaces.
|
|
121
|
+
* Absent when the record has none, or an empty one.
|
|
122
|
+
*/
|
|
123
|
+
liteDescription?: string;
|
|
110
124
|
isActive: boolean;
|
|
111
125
|
/** Weighted-average APY across tranches (decimal, e.g. 0.08 = 8 %). */
|
|
112
126
|
apy: number;
|
|
@@ -349,7 +349,12 @@ export async function checkBatchOutcome<R extends ReceiptLike = ReceiptLike>(
|
|
|
349
349
|
return 'unknown';
|
|
350
350
|
}
|
|
351
351
|
if (read.kind === 'reverted') return 'not-deposited';
|
|
352
|
-
|
|
352
|
+
// An empty hash is not an answer — `waitForMined` reads it the same way.
|
|
353
|
+
if (
|
|
354
|
+
read.kind !== 'mined' ||
|
|
355
|
+
typeof read.txHash !== 'string' ||
|
|
356
|
+
read.txHash === ''
|
|
357
|
+
) {
|
|
353
358
|
return 'unknown';
|
|
354
359
|
}
|
|
355
360
|
try {
|
|
@@ -367,9 +372,60 @@ export async function checkBatchOutcome<R extends ReceiptLike = ReceiptLike>(
|
|
|
367
372
|
}
|
|
368
373
|
|
|
369
374
|
// ---------------------------------------------------------------------------
|
|
370
|
-
// One sponsored call — withdraw, cancel, withdraw at expiry
|
|
375
|
+
// One sponsored call — withdraw, cancel, withdraw at expiry, transfer
|
|
371
376
|
// ---------------------------------------------------------------------------
|
|
372
377
|
|
|
378
|
+
/**
|
|
379
|
+
* `checkBatchOutcome` for ONE sponsored call — ONE status read, no polling —
|
|
380
|
+
* for an app's guard against sending the same request twice (a P2P transfer
|
|
381
|
+
* whose earlier submission ended PENDING, say):
|
|
382
|
+
*
|
|
383
|
+
* - `executed` — mined, and the receipt proves the sender's operation
|
|
384
|
+
* succeeded (`proveUserOperationInReceipt`);
|
|
385
|
+
* - `not-executed` — definitively reverted (vendor status or the operation's
|
|
386
|
+
* own flag);
|
|
387
|
+
* - `unknown` — anything else, including a read or receipt that failed, or a
|
|
388
|
+
* receipt that does not show the sender's operation.
|
|
389
|
+
*
|
|
390
|
+
* Only `not-executed` may let a new attempt proceed unprompted; `executed`
|
|
391
|
+
* means the earlier one landed and the user should be told.
|
|
392
|
+
*/
|
|
393
|
+
export async function checkSponsoredCallOutcome<
|
|
394
|
+
R extends ReceiptLike = ReceiptLike,
|
|
395
|
+
>(
|
|
396
|
+
transactionId: string,
|
|
397
|
+
expect: Omit<UserOperationProofExpectation, 'userOperationHash'>,
|
|
398
|
+
ports: Pick<BatchSubmissionPorts<R>, 'getStatus' | 'getReceipt'>,
|
|
399
|
+
): Promise<'executed' | 'not-executed' | 'unknown'> {
|
|
400
|
+
let read: BatchStatusRead;
|
|
401
|
+
try {
|
|
402
|
+
read = await ports.getStatus(transactionId);
|
|
403
|
+
} catch {
|
|
404
|
+
return 'unknown';
|
|
405
|
+
}
|
|
406
|
+
if (read.kind === 'reverted') return 'not-executed';
|
|
407
|
+
// An empty hash is not an answer — `waitForMined` reads it the same way.
|
|
408
|
+
if (
|
|
409
|
+
read.kind !== 'mined' ||
|
|
410
|
+
typeof read.txHash !== 'string' ||
|
|
411
|
+
read.txHash === ''
|
|
412
|
+
) {
|
|
413
|
+
return 'unknown';
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const receipt = await ports.getReceipt(read.txHash);
|
|
417
|
+
const proof = proveUserOperationInReceipt(receipt, {
|
|
418
|
+
...expect,
|
|
419
|
+
userOperationHash: read.userOperationHash ?? null,
|
|
420
|
+
});
|
|
421
|
+
if (proof.kind === 'executed') return 'executed';
|
|
422
|
+
if (proof.kind === 'reverted') return 'not-executed';
|
|
423
|
+
return 'unknown';
|
|
424
|
+
} catch {
|
|
425
|
+
return 'unknown';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
373
429
|
/**
|
|
374
430
|
* `BatchSubmissionPorts` for ONE sponsored call: the same status, receipt and
|
|
375
431
|
* clock ports — a relay client written for the deposit batch serves both —
|
|
@@ -409,9 +465,9 @@ export interface SponsoredCallSubmission<R extends ReceiptLike = ReceiptLike>
|
|
|
409
465
|
}
|
|
410
466
|
|
|
411
467
|
/**
|
|
412
|
-
* `submitBatch` for ONE sponsored
|
|
413
|
-
* `
|
|
414
|
-
*
|
|
468
|
+
* `submitBatch` for ONE sponsored call — the body of a `WithdrawFlow`
|
|
469
|
+
* `submitCall` port, of an app's cancel or withdraw-at-expiry action, and of
|
|
470
|
+
* a P2P transfer (`encodeStableTransfer`). Same post, same polling policy, same outcome
|
|
415
471
|
* table; the proof is `proveUserOperationInReceipt` (the sender's operation
|
|
416
472
|
* succeeded) instead of the deposit proof, and an unproven receipt is
|
|
417
473
|
* `call_unproven:<reason>`.
|
|
@@ -427,7 +483,8 @@ export interface SponsoredCallSubmission<R extends ReceiptLike = ReceiptLike>
|
|
|
427
483
|
* The call is checked against its kind before anything is posted — a known
|
|
428
484
|
* kind, exactly `{ to, data }`, and `data` opening with that kind's selector —
|
|
429
485
|
* and a mismatch is `BatchSubmissionError('call_mismatch', 'failed')`: nothing
|
|
430
|
-
* was sent. Build calls with `kasu.calls
|
|
486
|
+
* was sent. Build calls with `kasu.calls` (or `encodeStableTransfer`), and
|
|
487
|
+
* this never fires.
|
|
431
488
|
*
|
|
432
489
|
* As on the deposit path, nothing after the post ever produces `failed`.
|
|
433
490
|
*/
|