@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.
- package/README.md +162 -7
- package/dist/bundle.cjs.js +2292 -449
- package/dist/bundle.esm.js +2253 -451
- package/dist/domain/batch-proof.d.ts +210 -0
- package/dist/domain/batch-proof.js +384 -0
- package/dist/domain/batch-proof.js.map +1 -0
- package/dist/domain/explorer-urls.d.ts +33 -0
- package/dist/domain/explorer-urls.js +38 -0
- package/dist/domain/explorer-urls.js.map +1 -0
- package/dist/domain/fixed-term.d.ts +74 -0
- package/dist/domain/fixed-term.js +86 -0
- package/dist/domain/fixed-term.js.map +1 -0
- package/dist/domain/index.d.ts +12 -2
- package/dist/domain/index.js +7 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/partners.d.ts +29 -0
- package/dist/domain/partners.js +20 -0
- package/dist/domain/partners.js.map +1 -1
- package/dist/domain/requests.d.ts +24 -0
- package/dist/domain/requests.js +33 -0
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/sponsored-calls.d.ts +74 -0
- package/dist/domain/sponsored-calls.js +156 -0
- package/dist/domain/sponsored-calls.js.map +1 -0
- package/dist/domain/yields.d.ts +121 -0
- package/dist/domain/yields.js +151 -0
- package/dist/domain/yields.js.map +1 -0
- package/dist/facade/calls.d.ts +59 -0
- package/dist/facade/calls.js +89 -0
- package/dist/facade/calls.js.map +1 -0
- package/dist/facade/chain-configs.js +5 -0
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/flows.d.ts +40 -3
- package/dist/facade/flows.js +36 -3
- package/dist/facade/flows.js.map +1 -1
- package/dist/facade/index.d.ts +3 -1
- package/dist/facade/index.js +1 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +8 -2
- package/dist/facade/kasu.js +17 -4
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/strategies.d.ts +43 -2
- package/dist/facade/strategies.js +80 -4
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +18 -0
- package/dist/flows/batch-errors.d.ts +74 -0
- package/dist/flows/batch-errors.js +83 -0
- package/dist/flows/batch-errors.js.map +1 -0
- package/dist/flows/batch-submission.d.ts +223 -0
- package/dist/flows/batch-submission.js +321 -0
- package/dist/flows/batch-submission.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +128 -4
- package/dist/flows/deposit-flow.js +233 -4
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/flows/index.d.ts +6 -2
- package/dist/flows/index.js +3 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/withdraw-flow.d.ts +95 -4
- package/dist/flows/withdraw-flow.js +107 -2
- package/dist/flows/withdraw-flow.js.map +1 -1
- package/dist/services/DataService/queries.js +1 -0
- package/dist/services/DataService/queries.js.map +1 -1
- package/dist/services/UserLending/user-lending.d.ts +13 -0
- package/dist/services/UserLending/user-lending.js +25 -4
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/batch-proof.ts +576 -0
- package/src/domain/explorer-urls.ts +74 -0
- package/src/domain/fixed-term.ts +115 -0
- package/src/domain/index.ts +56 -0
- package/src/domain/partners.ts +35 -0
- package/src/domain/requests.ts +37 -0
- package/src/domain/sponsored-calls.ts +213 -0
- package/src/domain/yields.ts +189 -0
- package/src/facade/calls.ts +135 -0
- package/src/facade/chain-configs.ts +5 -0
- package/src/facade/flows.ts +35 -2
- package/src/facade/index.ts +3 -0
- package/src/facade/kasu.ts +32 -2
- package/src/facade/strategies.ts +98 -7
- package/src/facade/types.ts +19 -0
- package/src/flows/batch-errors.ts +118 -0
- package/src/flows/batch-submission.ts +511 -0
- package/src/flows/deposit-flow.ts +379 -7
- package/src/flows/index.ts +34 -1
- package/src/flows/withdraw-flow.ts +219 -10
- package/src/services/DataService/queries.ts +1 -0
- package/src/services/UserLending/user-lending.ts +38 -10
package/dist/bundle.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatUnits, parseUnits, formatEther, defaultAbiCoder, parseEther } from 'ethers/lib/utils';
|
|
2
|
-
import { utils as utils$2, Contract, BigNumber, ethers, Signer, providers } from 'ethers';
|
|
2
|
+
import { utils as utils$2, Contract, BigNumber, ethers, constants, Signer, providers } from 'ethers';
|
|
3
3
|
|
|
4
4
|
const defaultJsonSerializer = JSON;
|
|
5
5
|
|
|
@@ -2031,7 +2031,7 @@ function isSource(source) {
|
|
|
2031
2031
|
* Given a GraphQL source, parses it into a Document.
|
|
2032
2032
|
* Throws GraphQLError if a syntax error is encountered.
|
|
2033
2033
|
*/
|
|
2034
|
-
function parse(source, options) {
|
|
2034
|
+
function parse$1(source, options) {
|
|
2035
2035
|
const parser = new Parser(source, options);
|
|
2036
2036
|
const document = parser.parseDocument();
|
|
2037
2037
|
Object.defineProperty(document, 'tokenCount', {
|
|
@@ -4294,7 +4294,7 @@ const resolveRequestDocument = (document) => {
|
|
|
4294
4294
|
if (typeof document === `string`) {
|
|
4295
4295
|
let operationName = undefined;
|
|
4296
4296
|
try {
|
|
4297
|
-
const parsedDocument = parse(document);
|
|
4297
|
+
const parsedDocument = parse$1(document);
|
|
4298
4298
|
operationName = extractOperationName(parsedDocument);
|
|
4299
4299
|
}
|
|
4300
4300
|
catch (err) {
|
|
@@ -15809,6 +15809,7 @@ const getPoolOverviewQuery = (ids) => gql `
|
|
|
15809
15809
|
fixedTermDepositStatus
|
|
15810
15810
|
fixedTermDepositAllowlist{
|
|
15811
15811
|
id
|
|
15812
|
+
isAllowlisted
|
|
15812
15813
|
user{
|
|
15813
15814
|
id
|
|
15814
15815
|
}
|
|
@@ -17238,10 +17239,32 @@ class UserLending {
|
|
|
17238
17239
|
return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, amount);
|
|
17239
17240
|
});
|
|
17240
17241
|
}
|
|
17241
|
-
|
|
17242
|
+
/**
|
|
17243
|
+
* The tranche shares a withdrawal of `assetAmount` requests — the
|
|
17244
|
+
* tranche's own `convertToShares`, read now. `requestWithdrawalInAsset`
|
|
17245
|
+
* and the sponsored `kasu.calls.requestWithdrawal` both use this, so the
|
|
17246
|
+
* two paths cannot convert differently.
|
|
17247
|
+
*/
|
|
17248
|
+
getWithdrawalSharesForAssets(tranche, assetAmount) {
|
|
17249
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17250
|
+
const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
|
|
17251
|
+
return yield trancheContract.convertToShares(assetAmount);
|
|
17252
|
+
});
|
|
17253
|
+
}
|
|
17254
|
+
/**
|
|
17255
|
+
* Every tranche share `user` holds — what a max withdrawal requests
|
|
17256
|
+
* (`balanceOf(user)` on the tranche, read now). Shared by
|
|
17257
|
+
* `requestWithdrawalMax` and the sponsored `kasu.calls.requestWithdrawal`.
|
|
17258
|
+
*/
|
|
17259
|
+
getWithdrawalSharesMax(tranche, user) {
|
|
17242
17260
|
return __awaiter(this, void 0, void 0, function* () {
|
|
17243
17261
|
const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
|
|
17244
|
-
|
|
17262
|
+
return yield trancheContract['balanceOf(address)'](user);
|
|
17263
|
+
});
|
|
17264
|
+
}
|
|
17265
|
+
requestWithdrawalInAsset(lendingPool, tranche, assetAmount) {
|
|
17266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17267
|
+
const shareAmount = yield this.getWithdrawalSharesForAssets(tranche, assetAmount);
|
|
17245
17268
|
return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, shareAmount);
|
|
17246
17269
|
});
|
|
17247
17270
|
}
|
|
@@ -17253,8 +17276,7 @@ class UserLending {
|
|
|
17253
17276
|
}
|
|
17254
17277
|
requestWithdrawalMax(lendingPool, tranche, user) {
|
|
17255
17278
|
return __awaiter(this, void 0, void 0, function* () {
|
|
17256
|
-
const
|
|
17257
|
-
const userShares = yield trancheContract['balanceOf(address)'](user);
|
|
17279
|
+
const userShares = yield this.getWithdrawalSharesMax(tranche, user);
|
|
17258
17280
|
return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, userShares);
|
|
17259
17281
|
});
|
|
17260
17282
|
}
|
|
@@ -20460,7 +20482,7 @@ function toFormData$1(obj, formData, options) {
|
|
|
20460
20482
|
*
|
|
20461
20483
|
* @returns {string} The encoded string.
|
|
20462
20484
|
*/
|
|
20463
|
-
function encode$
|
|
20485
|
+
function encode$2(str) {
|
|
20464
20486
|
const charMap = {
|
|
20465
20487
|
'!': '%21',
|
|
20466
20488
|
"'": '%27',
|
|
@@ -20496,8 +20518,8 @@ prototype.append = function append(name, value) {
|
|
|
20496
20518
|
|
|
20497
20519
|
prototype.toString = function toString(encoder) {
|
|
20498
20520
|
const _encode = encoder
|
|
20499
|
-
? (value) => encoder.call(this, value, encode$
|
|
20500
|
-
: encode$
|
|
20521
|
+
? (value) => encoder.call(this, value, encode$2)
|
|
20522
|
+
: encode$2;
|
|
20501
20523
|
|
|
20502
20524
|
return this._pairs
|
|
20503
20525
|
.map(function each(pair) {
|
|
@@ -20514,7 +20536,7 @@ prototype.toString = function toString(encoder) {
|
|
|
20514
20536
|
*
|
|
20515
20537
|
* @returns {string} The encoded value.
|
|
20516
20538
|
*/
|
|
20517
|
-
function encode(val) {
|
|
20539
|
+
function encode$1(val) {
|
|
20518
20540
|
return encodeURIComponent(val)
|
|
20519
20541
|
.replace(/%3A/gi, ':')
|
|
20520
20542
|
.replace(/%24/g, '$')
|
|
@@ -20546,7 +20568,7 @@ function buildURL(url, params, options) {
|
|
|
20546
20568
|
// Read serializer options pollution-safely: own properties and methods on a
|
|
20547
20569
|
// class/template prototype are honored, but values injected onto a polluted
|
|
20548
20570
|
// Object.prototype are ignored.
|
|
20549
|
-
const _encode = utils$1.getSafeProp(_options, 'encode') || encode;
|
|
20571
|
+
const _encode = utils$1.getSafeProp(_options, 'encode') || encode$1;
|
|
20550
20572
|
const serializeFn = utils$1.getSafeProp(_options, 'serialize');
|
|
20551
20573
|
|
|
20552
20574
|
let serializedParams;
|
|
@@ -21186,7 +21208,7 @@ let CanceledError$1 = class CanceledError extends AxiosError$1 {
|
|
|
21186
21208
|
*
|
|
21187
21209
|
* @returns {object} The response.
|
|
21188
21210
|
*/
|
|
21189
|
-
function settle(resolve, reject, response) {
|
|
21211
|
+
function settle$1(resolve, reject, response) {
|
|
21190
21212
|
const validateStatus = response.config.validateStatus;
|
|
21191
21213
|
if (!response.status || !validateStatus || validateStatus(response.status)) {
|
|
21192
21214
|
resolve(response);
|
|
@@ -21949,7 +21971,7 @@ var xhrAdapter = isXHRAdapterSupported &&
|
|
|
21949
21971
|
request,
|
|
21950
21972
|
};
|
|
21951
21973
|
|
|
21952
|
-
settle(
|
|
21974
|
+
settle$1(
|
|
21953
21975
|
function _resolve(value) {
|
|
21954
21976
|
resolve(value);
|
|
21955
21977
|
done();
|
|
@@ -23007,7 +23029,7 @@ const factory = (env) => {
|
|
|
23007
23029
|
!isStreamResponse && unsubscribe && unsubscribe();
|
|
23008
23030
|
|
|
23009
23031
|
return await new Promise((resolve, reject) => {
|
|
23010
|
-
settle(resolve, reject, {
|
|
23032
|
+
settle$1(resolve, reject, {
|
|
23011
23033
|
data: responseData,
|
|
23012
23034
|
headers: AxiosHeaders$1.from(response.headers),
|
|
23013
23035
|
status: response.status,
|
|
@@ -24183,6 +24205,238 @@ class SdkConfig {
|
|
|
24183
24205
|
}
|
|
24184
24206
|
}
|
|
24185
24207
|
|
|
24208
|
+
/**
|
|
24209
|
+
* The single-call `LendingPoolManager` requests a sponsored relay may carry,
|
|
24210
|
+
* encoded by the SDK so no application hand-builds calldata or hand-maps the
|
|
24211
|
+
* relay's `kind` for them.
|
|
24212
|
+
*
|
|
24213
|
+
* Every function here is PURE: addresses and ids in, `{ kind, call }` out.
|
|
24214
|
+
* Reading the chain (the shares a withdrawal is for) is the facade's job —
|
|
24215
|
+
* `kasu.calls` — and sending is the application's.
|
|
24216
|
+
*
|
|
24217
|
+
* Each call is `{ to, data }` and nothing else: no `value` (none of these
|
|
24218
|
+
* functions is payable), no `from` (the manager acts for `msg.sender`, the
|
|
24219
|
+
* lender's own account, which is why none of them takes a user argument).
|
|
24220
|
+
* `data` is ethers' ABI encoding, which is canonical by construction — the
|
|
24221
|
+
* relay re-encodes what it decodes and refuses anything that differs, so a
|
|
24222
|
+
* hand-tweaked blob never gets through, and an SDK-built one always matches.
|
|
24223
|
+
*/
|
|
24224
|
+
/**
|
|
24225
|
+
* The relay's name for each shape. Carried beside the call so the app posts
|
|
24226
|
+
* `kind` from the SDK, never from a table of its own — a request declaring the
|
|
24227
|
+
* wrong kind is refused (`invalid_calls`), so a hand-mapped table that drifts
|
|
24228
|
+
* turns into a lender who cannot withdraw.
|
|
24229
|
+
*/
|
|
24230
|
+
const SPONSORED_CALL_KINDS = [
|
|
24231
|
+
/** `LendingPoolManager.requestWithdrawal(pool, tranche, shares)`. */
|
|
24232
|
+
'withdrawal',
|
|
24233
|
+
/** `LendingPoolManager.cancelDepositRequest(pool, dNftID)`. */
|
|
24234
|
+
'cancel_deposit',
|
|
24235
|
+
/** `LendingPoolManager.cancelWithdrawalRequest(pool, wNftID)`. */
|
|
24236
|
+
'cancel_withdrawal',
|
|
24237
|
+
/** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
|
|
24238
|
+
'withdraw_at_expiry',
|
|
24239
|
+
];
|
|
24240
|
+
/**
|
|
24241
|
+
* Widened to the plain `Interface`: the typechain one overloads
|
|
24242
|
+
* `encodeFunctionData` per literal name, and the name here comes from
|
|
24243
|
+
* `FUNCTION_OF`.
|
|
24244
|
+
*/
|
|
24245
|
+
const LENDING_POOL_MANAGER$2 = ILendingPoolManagerAbi__factory.createInterface();
|
|
24246
|
+
/** The `LendingPoolManager` function behind each kind. */
|
|
24247
|
+
const FUNCTION_OF = Object.freeze({
|
|
24248
|
+
withdrawal: 'requestWithdrawal',
|
|
24249
|
+
cancel_deposit: 'cancelDepositRequest',
|
|
24250
|
+
cancel_withdrawal: 'cancelWithdrawalRequest',
|
|
24251
|
+
withdraw_at_expiry: 'requestFixedTermDepositWithdrawal',
|
|
24252
|
+
});
|
|
24253
|
+
/**
|
|
24254
|
+
* The 4-byte selector of each kind's function, DERIVED from the generated
|
|
24255
|
+
* `ILendingPoolManager` ABI — never typed in by hand, so an ABI change moves
|
|
24256
|
+
* it and the cross-check spec against the relay's own pins goes red.
|
|
24257
|
+
*/
|
|
24258
|
+
const SPONSORED_CALL_SELECTORS = Object.freeze({
|
|
24259
|
+
withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdrawal),
|
|
24260
|
+
cancel_deposit: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_deposit),
|
|
24261
|
+
cancel_withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_withdrawal),
|
|
24262
|
+
withdraw_at_expiry: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdraw_at_expiry),
|
|
24263
|
+
});
|
|
24264
|
+
/**
|
|
24265
|
+
* What an encoder throws for an input it will not encode. `code` names the
|
|
24266
|
+
* argument; the message names no party, so a wallet-rejection classifier can
|
|
24267
|
+
* never read it as something the lender did. Nothing has been sent when this
|
|
24268
|
+
* is thrown.
|
|
24269
|
+
*/
|
|
24270
|
+
class SponsoredCallInputError extends Error {
|
|
24271
|
+
constructor(code) {
|
|
24272
|
+
super(`Sponsored call input refused (${code})`);
|
|
24273
|
+
this.name = 'SponsoredCallInputError';
|
|
24274
|
+
this.code = code;
|
|
24275
|
+
Object.setPrototypeOf(this, SponsoredCallInputError.prototype);
|
|
24276
|
+
}
|
|
24277
|
+
}
|
|
24278
|
+
const ADDRESS_RE$2 = /^0x[0-9a-fA-F]{40}$/;
|
|
24279
|
+
function assertAddress(value, code) {
|
|
24280
|
+
if (typeof value !== 'string' || !ADDRESS_RE$2.test(value)) {
|
|
24281
|
+
throw new SponsoredCallInputError(code);
|
|
24282
|
+
}
|
|
24283
|
+
}
|
|
24284
|
+
/** A uint256 — an NFT or lock id. Zero is a valid id as far as encoding goes. */
|
|
24285
|
+
function toUint256(value, code) {
|
|
24286
|
+
let parsed;
|
|
24287
|
+
try {
|
|
24288
|
+
parsed = BigNumber.from(value);
|
|
24289
|
+
}
|
|
24290
|
+
catch (_a) {
|
|
24291
|
+
throw new SponsoredCallInputError(code);
|
|
24292
|
+
}
|
|
24293
|
+
if (parsed.isNegative() || parsed.gt(constants.MaxUint256)) {
|
|
24294
|
+
throw new SponsoredCallInputError(code);
|
|
24295
|
+
}
|
|
24296
|
+
return parsed;
|
|
24297
|
+
}
|
|
24298
|
+
function encode(kind, lendingPoolManager, args) {
|
|
24299
|
+
return {
|
|
24300
|
+
kind,
|
|
24301
|
+
call: {
|
|
24302
|
+
to: lendingPoolManager,
|
|
24303
|
+
data: LENDING_POOL_MANAGER$2.encodeFunctionData(FUNCTION_OF[kind], args),
|
|
24304
|
+
},
|
|
24305
|
+
};
|
|
24306
|
+
}
|
|
24307
|
+
/**
|
|
24308
|
+
* `requestWithdrawal(poolId, trancheId, shares)` — `withdrawal`.
|
|
24309
|
+
*
|
|
24310
|
+
* `shares` are tranche SHARES, not assets: `kasu.calls.requestWithdrawal`
|
|
24311
|
+
* converts an asset amount (or `'max'`) into them exactly as
|
|
24312
|
+
* `UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax` do. Zero
|
|
24313
|
+
* shares is refused here: the contract reverts on it
|
|
24314
|
+
* (`RequestWithdrawSharesAreZero`) and the relay refuses it before that.
|
|
24315
|
+
*/
|
|
24316
|
+
function encodeRequestWithdrawalCall(lendingPoolManager, poolId, trancheId, shares) {
|
|
24317
|
+
assertAddress(lendingPoolManager, 'lending-pool-manager');
|
|
24318
|
+
assertAddress(poolId, 'pool');
|
|
24319
|
+
assertAddress(trancheId, 'tranche');
|
|
24320
|
+
const amount = toUint256(shares, 'shares');
|
|
24321
|
+
if (amount.isZero())
|
|
24322
|
+
throw new SponsoredCallInputError('zero-shares');
|
|
24323
|
+
return encode('withdrawal', lendingPoolManager, [
|
|
24324
|
+
poolId,
|
|
24325
|
+
trancheId,
|
|
24326
|
+
amount,
|
|
24327
|
+
]);
|
|
24328
|
+
}
|
|
24329
|
+
/** `cancelDepositRequest(poolId, dNftId)` — `cancel_deposit`. */
|
|
24330
|
+
function encodeCancelDepositCall(lendingPoolManager, poolId, dNftId) {
|
|
24331
|
+
assertAddress(lendingPoolManager, 'lending-pool-manager');
|
|
24332
|
+
assertAddress(poolId, 'pool');
|
|
24333
|
+
return encode('cancel_deposit', lendingPoolManager, [
|
|
24334
|
+
poolId,
|
|
24335
|
+
toUint256(dNftId, 'nft-id'),
|
|
24336
|
+
]);
|
|
24337
|
+
}
|
|
24338
|
+
/** `cancelWithdrawalRequest(poolId, wNftId)` — `cancel_withdrawal`. */
|
|
24339
|
+
function encodeCancelWithdrawalCall(lendingPoolManager, poolId, wNftId) {
|
|
24340
|
+
assertAddress(lendingPoolManager, 'lending-pool-manager');
|
|
24341
|
+
assertAddress(poolId, 'pool');
|
|
24342
|
+
return encode('cancel_withdrawal', lendingPoolManager, [
|
|
24343
|
+
poolId,
|
|
24344
|
+
toUint256(wNftId, 'nft-id'),
|
|
24345
|
+
]);
|
|
24346
|
+
}
|
|
24347
|
+
/**
|
|
24348
|
+
* `requestFixedTermDepositWithdrawal(poolId, fixedTermDepositId)` —
|
|
24349
|
+
* `withdraw_at_expiry`. The contract function takes the pool and the LOCK id
|
|
24350
|
+
* only (no tranche): the manager forwards to the fixed-term contract on the
|
|
24351
|
+
* lender's behalf, which knows the lock's tranche.
|
|
24352
|
+
*/
|
|
24353
|
+
function encodeWithdrawAtExpiryCall(lendingPoolManager, poolId, fixedTermDepositId) {
|
|
24354
|
+
assertAddress(lendingPoolManager, 'lending-pool-manager');
|
|
24355
|
+
assertAddress(poolId, 'pool');
|
|
24356
|
+
return encode('withdraw_at_expiry', lendingPoolManager, [
|
|
24357
|
+
poolId,
|
|
24358
|
+
toUint256(fixedTermDepositId, 'lock-id'),
|
|
24359
|
+
]);
|
|
24360
|
+
}
|
|
24361
|
+
|
|
24362
|
+
const ADDRESS_RE$1 = /^0x[0-9a-fA-F]{40}$/;
|
|
24363
|
+
/**
|
|
24364
|
+
* Encoded, ready-to-sponsor `LendingPoolManager` calls — read, then encode,
|
|
24365
|
+
* NEVER send. Each returns `{ kind, call }`: post `kind` and `[call]` to a
|
|
24366
|
+
* sponsored relay (or `submitSponsoredCall`), unchanged.
|
|
24367
|
+
*
|
|
24368
|
+
* ```ts
|
|
24369
|
+
* const { kind, call } = kasu.calls.cancelDepositRequest(poolId, dNftId);
|
|
24370
|
+
* ```
|
|
24371
|
+
*
|
|
24372
|
+
* `call.to` is always THIS chain's `LendingPoolManager`, from the chain
|
|
24373
|
+
* config — the same address the deposit flow approves and calls — so no app
|
|
24374
|
+
* supplies the one argument whose wrong value the relay refuses outright.
|
|
24375
|
+
*
|
|
24376
|
+
* Works on a read-only instance: the only chain access is the share read a
|
|
24377
|
+
* withdrawal needs, and nothing is signed or sent here.
|
|
24378
|
+
*/
|
|
24379
|
+
class CallsFacade {
|
|
24380
|
+
constructor(_userLending, _lendingPoolManager) {
|
|
24381
|
+
this._userLending = _userLending;
|
|
24382
|
+
this._lendingPoolManager = _lendingPoolManager;
|
|
24383
|
+
}
|
|
24384
|
+
/**
|
|
24385
|
+
* `requestWithdrawal(pool, tranche, shares)` for an asset amount or
|
|
24386
|
+
* `'max'`, converted into shares exactly as the direct path does it
|
|
24387
|
+
* (`UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax`): the
|
|
24388
|
+
* tranche's `convertToShares(amount)`, or the lender's whole
|
|
24389
|
+
* `balanceOf(userAddress)`, read now.
|
|
24390
|
+
*
|
|
24391
|
+
* Rejects with `SponsoredCallInputError` — before anything is sent — for
|
|
24392
|
+
* a non-positive amount, a malformed address, or a conversion that comes
|
|
24393
|
+
* to ZERO shares (an empty position, or dust below one share), which the
|
|
24394
|
+
* contract would revert and the relay refuses.
|
|
24395
|
+
*/
|
|
24396
|
+
requestWithdrawal(params) {
|
|
24397
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24398
|
+
const { poolId, trancheId, amount, userAddress } = params;
|
|
24399
|
+
if (typeof userAddress !== 'string' || !ADDRESS_RE$1.test(userAddress)) {
|
|
24400
|
+
throw new SponsoredCallInputError('user');
|
|
24401
|
+
}
|
|
24402
|
+
if (typeof poolId !== 'string' || !ADDRESS_RE$1.test(poolId)) {
|
|
24403
|
+
throw new SponsoredCallInputError('pool');
|
|
24404
|
+
}
|
|
24405
|
+
if (typeof trancheId !== 'string' || !ADDRESS_RE$1.test(trancheId)) {
|
|
24406
|
+
throw new SponsoredCallInputError('tranche');
|
|
24407
|
+
}
|
|
24408
|
+
let shares;
|
|
24409
|
+
if (amount === 'max') {
|
|
24410
|
+
// Lowercased, as `WithdrawFlow` hands `withdrawMax` the address.
|
|
24411
|
+
shares = yield this._userLending.getWithdrawalSharesMax(trancheId, userAddress.toLowerCase());
|
|
24412
|
+
}
|
|
24413
|
+
else {
|
|
24414
|
+
if (!BigNumber.isBigNumber(amount) || amount.lte(0)) {
|
|
24415
|
+
throw new SponsoredCallInputError('non-positive-amount');
|
|
24416
|
+
}
|
|
24417
|
+
shares = yield this._userLending.getWithdrawalSharesForAssets(trancheId, amount);
|
|
24418
|
+
}
|
|
24419
|
+
return encodeRequestWithdrawalCall(this._lendingPoolManager, poolId, trancheId, shares);
|
|
24420
|
+
});
|
|
24421
|
+
}
|
|
24422
|
+
/** `cancelDepositRequest(pool, dNftId)` — the pending deposit's NFT id. */
|
|
24423
|
+
cancelDepositRequest(poolId, dNftId) {
|
|
24424
|
+
return encodeCancelDepositCall(this._lendingPoolManager, poolId, dNftId);
|
|
24425
|
+
}
|
|
24426
|
+
/** `cancelWithdrawalRequest(pool, wNftId)` — the pending withdrawal's NFT id. */
|
|
24427
|
+
cancelWithdrawalRequest(poolId, wNftId) {
|
|
24428
|
+
return encodeCancelWithdrawalCall(this._lendingPoolManager, poolId, wNftId);
|
|
24429
|
+
}
|
|
24430
|
+
/**
|
|
24431
|
+
* `requestFixedTermDepositWithdrawal(pool, fixedTermDepositId)` — what
|
|
24432
|
+
* `UserLending.withdrawAtExpiry(pool, lockId)` sends: the pool and the
|
|
24433
|
+
* LOCK id, no tranche.
|
|
24434
|
+
*/
|
|
24435
|
+
withdrawAtExpiry(poolId, fixedTermDepositId) {
|
|
24436
|
+
return encodeWithdrawAtExpiryCall(this._lendingPoolManager, poolId, fixedTermDepositId);
|
|
24437
|
+
}
|
|
24438
|
+
}
|
|
24439
|
+
|
|
24186
24440
|
/**
|
|
24187
24441
|
* Browser-viable XDC RPCs only. `rpc.xdc.org` / `erpc.xdc.org` answer OPTIONS
|
|
24188
24442
|
* without `access-control-allow-origin`, so a browser call hangs on preflight
|
|
@@ -24240,6 +24494,7 @@ const CHAIN_CONFIGS = {
|
|
|
24240
24494
|
'https://base-rpc.publicnode.com',
|
|
24241
24495
|
'https://mainnet.base.org',
|
|
24242
24496
|
],
|
|
24497
|
+
blockExplorerUrl: 'https://basescan.org',
|
|
24243
24498
|
},
|
|
24244
24499
|
xdc: {
|
|
24245
24500
|
chainId: 50,
|
|
@@ -24275,6 +24530,7 @@ const CHAIN_CONFIGS = {
|
|
|
24275
24530
|
currencyCode: 'AUD',
|
|
24276
24531
|
},
|
|
24277
24532
|
rpcUrls: XDC_RPC_URLS,
|
|
24533
|
+
blockExplorerUrl: 'https://xdcscan.com',
|
|
24278
24534
|
},
|
|
24279
24535
|
'xdc-usdc': {
|
|
24280
24536
|
chainId: 50,
|
|
@@ -24306,6 +24562,7 @@ const CHAIN_CONFIGS = {
|
|
|
24306
24562
|
currencyCode: 'USD',
|
|
24307
24563
|
},
|
|
24308
24564
|
rpcUrls: XDC_RPC_URLS,
|
|
24565
|
+
blockExplorerUrl: 'https://xdcscan.com',
|
|
24309
24566
|
},
|
|
24310
24567
|
plume: {
|
|
24311
24568
|
chainId: 98866,
|
|
@@ -24346,6 +24603,8 @@ const CHAIN_CONFIGS = {
|
|
|
24346
24603
|
// pointing at an endpoint nobody maintains. Pass your own
|
|
24347
24604
|
// `signerOrProvider` to read the frozen history.
|
|
24348
24605
|
rpcUrls: [],
|
|
24606
|
+
// The explorer still serves the frozen history.
|
|
24607
|
+
blockExplorerUrl: 'https://explorer.plume.org',
|
|
24349
24608
|
retired: true,
|
|
24350
24609
|
},
|
|
24351
24610
|
};
|
|
@@ -24465,128 +24724,1084 @@ class DepositsFacade {
|
|
|
24465
24724
|
}
|
|
24466
24725
|
|
|
24467
24726
|
/**
|
|
24468
|
-
* The
|
|
24727
|
+
* The errors an atomic-batch submitter throws, in one module both
|
|
24728
|
+
* `DepositFlow` and `submitBatch` import — so `instanceof` is one class
|
|
24729
|
+
* whichever of them a consumer imports it through.
|
|
24730
|
+
*/
|
|
24731
|
+
/**
|
|
24732
|
+
* What a `depositBatch` port throws when it KNOWS the batch reverted — from a
|
|
24733
|
+
* simulation before sending, from the submitter refusing to broadcast a batch
|
|
24734
|
+
* that would revert, or from a mined receipt whose user operation reverted.
|
|
24735
|
+
* The run ends `{ step: 'request', reason: 'reverted', revertError }`.
|
|
24736
|
+
*
|
|
24737
|
+
* `revertError` is the custom error the contract named (`'ClearingIsPending'`,
|
|
24738
|
+
* `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
|
|
24739
|
+
* batch is known to revert but not why: the consumer then renders its generic
|
|
24740
|
+
* `reverted` copy, which is what an unknown name gets anyway.
|
|
24741
|
+
*
|
|
24742
|
+
* A revert is DEFINITIVE — nothing was deposited. Throw it only when that is
|
|
24743
|
+
* known; anything less certain is a `BatchPendingError`.
|
|
24744
|
+
*/
|
|
24745
|
+
class BatchRevertedError extends Error {
|
|
24746
|
+
constructor(revertError, message) {
|
|
24747
|
+
super(message !== null && message !== void 0 ? message : `DepositFlow: the batch reverted${revertError ? ` with ${revertError}` : ''}`);
|
|
24748
|
+
this.name = 'BatchRevertedError';
|
|
24749
|
+
this.revertError = revertError;
|
|
24750
|
+
// Keeps `instanceof` honest under a down-levelled build.
|
|
24751
|
+
Object.setPrototypeOf(this, BatchRevertedError.prototype);
|
|
24752
|
+
}
|
|
24753
|
+
}
|
|
24754
|
+
/**
|
|
24755
|
+
* Every non-revert failure of a batch submission, carrying its outcome and a
|
|
24756
|
+
* short code (`'status_timeout'`, `'receipt_unavailable'`, …).
|
|
24469
24757
|
*
|
|
24470
|
-
*
|
|
24471
|
-
*
|
|
24472
|
-
*
|
|
24473
|
-
* consumer walked away from. That is the whole requirement, and it is 60 lines.
|
|
24758
|
+
* The message is fixed and names no party: the wallet-error classifier reads a
|
|
24759
|
+
* thrown error's text for a USER rejection, and a relay outage worded "…wallet
|
|
24760
|
+
* refused…" must never reach the lender as something they did.
|
|
24474
24761
|
*
|
|
24475
|
-
*
|
|
24476
|
-
*
|
|
24477
|
-
*
|
|
24478
|
-
*
|
|
24479
|
-
* dead state or, worse, drive the UI back into a phase the consumer has left.
|
|
24762
|
+
* `outcome` has NO default. `failed` is the one outcome that invites a second
|
|
24763
|
+
* deposit, so a port author has to spell it out — a `throw new
|
|
24764
|
+
* BatchSubmissionError(code)` written in a catch block after the POST left
|
|
24765
|
+
* must not quietly become "try again".
|
|
24480
24766
|
*/
|
|
24481
|
-
class
|
|
24482
|
-
constructor(
|
|
24483
|
-
|
|
24484
|
-
this.
|
|
24485
|
-
this.
|
|
24486
|
-
this.
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24491
|
-
|
|
24492
|
-
|
|
24493
|
-
|
|
24494
|
-
|
|
24495
|
-
/** False once `reset()` (or a later `start()`) has superseded `token`. */
|
|
24496
|
-
isCurrent(token) {
|
|
24497
|
-
return this._generation === token;
|
|
24498
|
-
}
|
|
24499
|
-
/**
|
|
24500
|
-
* Subscribe to every transition. Returns the unsubscribe function; calling
|
|
24501
|
-
* it twice is harmless.
|
|
24502
|
-
*
|
|
24503
|
-
* Listeners are NOT called on subscribe — read `state` for the current
|
|
24504
|
-
* value. A listener that throws is not allowed to break the pipeline or
|
|
24505
|
-
* starve the listeners registered after it, so throws are swallowed.
|
|
24506
|
-
*/
|
|
24507
|
-
subscribe(listener) {
|
|
24508
|
-
this._listeners.add(listener);
|
|
24509
|
-
return () => {
|
|
24510
|
-
this._listeners.delete(listener);
|
|
24511
|
-
};
|
|
24512
|
-
}
|
|
24513
|
-
/**
|
|
24514
|
-
* Merge `next` into the state and notify, unless `token` names a
|
|
24515
|
-
* superseded run.
|
|
24516
|
-
*/
|
|
24517
|
-
patch(next, token) {
|
|
24518
|
-
if (token !== undefined && !this.isCurrent(token))
|
|
24519
|
-
return;
|
|
24520
|
-
this._state = Object.assign(Object.assign({}, this._state), next);
|
|
24521
|
-
for (const listener of [...this._listeners]) {
|
|
24522
|
-
try {
|
|
24523
|
-
listener(this._state);
|
|
24524
|
-
}
|
|
24525
|
-
catch (_a) {
|
|
24526
|
-
// A consumer's rendering fault is its own problem; the pipeline
|
|
24527
|
-
// owns real money and keeps going.
|
|
24528
|
-
}
|
|
24767
|
+
class BatchSubmissionError extends Error {
|
|
24768
|
+
constructor(code, outcome, options) {
|
|
24769
|
+
super(`Batch submission ${outcome} (${code})`);
|
|
24770
|
+
this.name = 'BatchSubmissionError';
|
|
24771
|
+
this.code = code;
|
|
24772
|
+
this.outcome = outcome;
|
|
24773
|
+
// Set by hand: `Error`'s own `cause` option is ES2022.
|
|
24774
|
+
if (options && 'cause' in options) {
|
|
24775
|
+
Object.defineProperty(this, 'cause', {
|
|
24776
|
+
value: options.cause,
|
|
24777
|
+
enumerable: false,
|
|
24778
|
+
writable: true,
|
|
24779
|
+
configurable: true,
|
|
24780
|
+
});
|
|
24529
24781
|
}
|
|
24782
|
+
Object.setPrototypeOf(this, BatchSubmissionError.prototype);
|
|
24530
24783
|
}
|
|
24531
|
-
|
|
24532
|
-
|
|
24533
|
-
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
this.
|
|
24540
|
-
|
|
24541
|
-
/** Claim the next run token. Any earlier run is abandoned. */
|
|
24542
|
-
beginRun() {
|
|
24543
|
-
this._generation += 1;
|
|
24544
|
-
return this._generation;
|
|
24784
|
+
}
|
|
24785
|
+
/**
|
|
24786
|
+
* The ambiguous outcome: the batch may have landed. An instance of
|
|
24787
|
+
* `BatchSubmissionError` with `outcome: 'pending'`.
|
|
24788
|
+
*/
|
|
24789
|
+
class BatchPendingError extends BatchSubmissionError {
|
|
24790
|
+
constructor(code, options) {
|
|
24791
|
+
super(code, 'pending', options);
|
|
24792
|
+
this.name = 'BatchPendingError';
|
|
24793
|
+
Object.setPrototypeOf(this, BatchPendingError.prototype);
|
|
24545
24794
|
}
|
|
24546
24795
|
}
|
|
24796
|
+
/** The outcome a thrown error stands for, or `null` if it is not a submission error. */
|
|
24797
|
+
function batchOutcomeOf(err) {
|
|
24798
|
+
return err instanceof BatchSubmissionError ? err.outcome : null;
|
|
24799
|
+
}
|
|
24800
|
+
/**
|
|
24801
|
+
* Nothing was deposited and nothing can be: a known revert, or a definitive
|
|
24802
|
+
* `failed` refusal. Everything else — including an error this SDK did not
|
|
24803
|
+
* produce — is NOT proof of that, and a consumer must not invite a retry on it.
|
|
24804
|
+
*/
|
|
24805
|
+
function isDefinitivelyNotSubmitted(err) {
|
|
24806
|
+
return (err instanceof BatchRevertedError || batchOutcomeOf(err) === 'failed');
|
|
24807
|
+
}
|
|
24547
24808
|
|
|
24809
|
+
// ---------------------------------------------------------------------------
|
|
24810
|
+
// The selector tables
|
|
24811
|
+
// ---------------------------------------------------------------------------
|
|
24548
24812
|
/**
|
|
24549
|
-
*
|
|
24550
|
-
*
|
|
24551
|
-
*
|
|
24552
|
-
*
|
|
24553
|
-
|
|
24554
|
-
|
|
24555
|
-
|
|
24556
|
-
|
|
24557
|
-
|
|
24558
|
-
|
|
24813
|
+
* OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
|
|
24814
|
+
* Kasu contract declares them — they arrive from the TOKEN, through the
|
|
24815
|
+
* manager's `transferFrom`, and they are the modern spelling of the string
|
|
24816
|
+
* reverts below.
|
|
24817
|
+
*/
|
|
24818
|
+
const ERC20_ERROR_ABI = [
|
|
24819
|
+
'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
|
|
24820
|
+
'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
|
|
24821
|
+
];
|
|
24822
|
+
/**
|
|
24823
|
+
* The protocol errors that mean the ERC-20 transfer failed rather than the
|
|
24824
|
+
* protocol refusing.
|
|
24559
24825
|
*
|
|
24560
|
-
* `
|
|
24561
|
-
*
|
|
24562
|
-
* `
|
|
24563
|
-
*
|
|
24564
|
-
*
|
|
24826
|
+
* `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
|
|
24827
|
+
* that returned false or reverted, which on this path IS the `transferFrom`;
|
|
24828
|
+
* `AddressInsufficientBalance` is its native-value sibling. Both belong with
|
|
24829
|
+
* the balance case, not with the pool conditions — a lender who sees one of
|
|
24830
|
+
* them really does need to fund or re-approve.
|
|
24831
|
+
*/
|
|
24832
|
+
const ERC20_FAMILY_NAMES = new Set([
|
|
24833
|
+
'AddressInsufficientBalance',
|
|
24834
|
+
'ERC20InsufficientAllowance',
|
|
24835
|
+
'ERC20InsufficientBalance',
|
|
24836
|
+
'SafeERC20FailedOperation',
|
|
24837
|
+
]);
|
|
24838
|
+
/** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
|
|
24839
|
+
const STRING_REVERT_SELECTOR = '0x08c379a0';
|
|
24840
|
+
/**
|
|
24841
|
+
* The `require` reasons that are a balance or an allowance, across the token
|
|
24842
|
+
* implementations in circulation: OpenZeppelin v4's sentences, and the
|
|
24843
|
+
* shorthand the minimal implementations use.
|
|
24844
|
+
*/
|
|
24845
|
+
const ERC20_REASON_PATTERNS = [
|
|
24846
|
+
/exceeds\s+balance/,
|
|
24847
|
+
/exceeds\s+allowance/,
|
|
24848
|
+
/insufficient\s+balance/,
|
|
24849
|
+
/insufficient\s+allowance/,
|
|
24850
|
+
/transfer_from_failed/,
|
|
24851
|
+
/transferfrom\s+failed/,
|
|
24852
|
+
/low-level\s+call\s+failed/,
|
|
24853
|
+
];
|
|
24854
|
+
/**
|
|
24855
|
+
* selector → error name, built once on first use.
|
|
24565
24856
|
*
|
|
24566
|
-
*
|
|
24567
|
-
*
|
|
24568
|
-
*
|
|
24569
|
-
*
|
|
24570
|
-
* abandoned run's `finally` therefore releases the guard only if it still
|
|
24571
|
-
* holds it — by then a NEW run may own it, and clearing it there would let a
|
|
24572
|
-
* third run start alongside the second.
|
|
24857
|
+
* Lazily, because a consumer that never sees a revert should not pay for three
|
|
24858
|
+
* `Interface` constructions at import. The two Kasu tables are the GENERATED
|
|
24859
|
+
* typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
|
|
24860
|
+
* and the new errors are recognised with no edit here.
|
|
24573
24861
|
*/
|
|
24574
|
-
|
|
24575
|
-
|
|
24576
|
-
|
|
24577
|
-
|
|
24578
|
-
|
|
24579
|
-
|
|
24580
|
-
|
|
24581
|
-
|
|
24582
|
-
|
|
24583
|
-
|
|
24584
|
-
|
|
24585
|
-
|
|
24586
|
-
|
|
24587
|
-
|
|
24588
|
-
|
|
24589
|
-
|
|
24862
|
+
let selectorTable = null;
|
|
24863
|
+
function selectors() {
|
|
24864
|
+
if (selectorTable)
|
|
24865
|
+
return selectorTable;
|
|
24866
|
+
const table = new Map();
|
|
24867
|
+
const interfaces = [
|
|
24868
|
+
// The manager the deposit is called against, and the allow list it
|
|
24869
|
+
// delegates the KYC and block-window checks to.
|
|
24870
|
+
ILendingPoolManagerAbi__factory.createInterface(),
|
|
24871
|
+
IKasuAllowListAbi__factory.createInterface(),
|
|
24872
|
+
new ethers.utils.Interface(ERC20_ERROR_ABI),
|
|
24873
|
+
];
|
|
24874
|
+
for (const iface of interfaces) {
|
|
24875
|
+
for (const fragment of Object.values(iface.errors)) {
|
|
24876
|
+
// First writer wins: the same name declared by both ABIs is the
|
|
24877
|
+
// same selector, so the duplicate would be a no-op anyway.
|
|
24878
|
+
const sighash = iface.getSighash(fragment);
|
|
24879
|
+
if (!table.has(sighash))
|
|
24880
|
+
table.set(sighash, fragment.name);
|
|
24881
|
+
}
|
|
24882
|
+
}
|
|
24883
|
+
selectorTable = table;
|
|
24884
|
+
return table;
|
|
24885
|
+
}
|
|
24886
|
+
// ---------------------------------------------------------------------------
|
|
24887
|
+
// Getting the bytes out of the envelope
|
|
24888
|
+
// ---------------------------------------------------------------------------
|
|
24889
|
+
/**
|
|
24890
|
+
* How deep to look for the revert data. Providers nest it differently —
|
|
24891
|
+
* ethers' own error wraps the provider's, which wraps the RPC body — and a
|
|
24892
|
+
* bound is what keeps a self-referencing error object from being walked
|
|
24893
|
+
* forever.
|
|
24894
|
+
*/
|
|
24895
|
+
const MAX_DEPTH = 6;
|
|
24896
|
+
/** The keys ethers and the providers under it put a wrapped error on. */
|
|
24897
|
+
const NESTED_KEYS = [
|
|
24898
|
+
'data',
|
|
24899
|
+
'error',
|
|
24900
|
+
'originalError',
|
|
24901
|
+
'info',
|
|
24902
|
+
'cause',
|
|
24903
|
+
'body',
|
|
24904
|
+
];
|
|
24905
|
+
/** At least a 4-byte selector, and a whole number of bytes. */
|
|
24906
|
+
function isRevertData(value) {
|
|
24907
|
+
return (value.length >= 10 &&
|
|
24908
|
+
value.length % 2 === 0 &&
|
|
24909
|
+
/^0x[0-9a-fA-F]+$/.test(value));
|
|
24910
|
+
}
|
|
24911
|
+
/**
|
|
24912
|
+
* Find the revert data on an error, wherever the provider left it.
|
|
24913
|
+
*
|
|
24914
|
+
* ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
|
|
24915
|
+
* attached, and each provider nests the bytes one layer differently:
|
|
24916
|
+
* `error.data`, `error.error.data`, `error.data.originalError.data`, or only
|
|
24917
|
+
* inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
|
|
24918
|
+
* products, walk a fixed set of keys to a fixed depth and take the first value
|
|
24919
|
+
* that is shaped like revert data.
|
|
24920
|
+
*/
|
|
24921
|
+
function extractRevertData(err) {
|
|
24922
|
+
return walk(err, 0);
|
|
24923
|
+
}
|
|
24924
|
+
function walk(node, depth) {
|
|
24925
|
+
if (node == null || depth > MAX_DEPTH)
|
|
24926
|
+
return null;
|
|
24927
|
+
if (typeof node === 'string') {
|
|
24928
|
+
if (isRevertData(node))
|
|
24929
|
+
return node;
|
|
24930
|
+
// A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
|
|
24931
|
+
if (!node.startsWith('{'))
|
|
24932
|
+
return null;
|
|
24933
|
+
try {
|
|
24934
|
+
return walk(JSON.parse(node), depth + 1);
|
|
24935
|
+
}
|
|
24936
|
+
catch (_a) {
|
|
24937
|
+
return null;
|
|
24938
|
+
}
|
|
24939
|
+
}
|
|
24940
|
+
if (typeof node !== 'object')
|
|
24941
|
+
return null;
|
|
24942
|
+
const record = node;
|
|
24943
|
+
for (const key of NESTED_KEYS) {
|
|
24944
|
+
const found = walk(record[key], depth + 1);
|
|
24945
|
+
if (found)
|
|
24946
|
+
return found;
|
|
24947
|
+
}
|
|
24948
|
+
return null;
|
|
24949
|
+
}
|
|
24950
|
+
// ---------------------------------------------------------------------------
|
|
24951
|
+
// Decoding
|
|
24952
|
+
// ---------------------------------------------------------------------------
|
|
24953
|
+
/**
|
|
24954
|
+
* Decode a failed call's revert into the error the contract named, or `null`
|
|
24955
|
+
* when there is nothing recognisable to name.
|
|
24956
|
+
*
|
|
24957
|
+
* `null` is the honest answer for an unknown selector, an unrelated `require`
|
|
24958
|
+
* string, or an error with no revert data at all — the caller keeps whatever
|
|
24959
|
+
* it did before rather than inventing a diagnosis from bytes it cannot read.
|
|
24960
|
+
*/
|
|
24961
|
+
function decodeRevert(err) {
|
|
24962
|
+
const data = extractRevertData(err);
|
|
24963
|
+
if (!data)
|
|
24964
|
+
return null;
|
|
24965
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
24966
|
+
if (selector === STRING_REVERT_SELECTOR) {
|
|
24967
|
+
const reason = decodeStringRevert(data);
|
|
24968
|
+
if (reason === null)
|
|
24969
|
+
return null;
|
|
24970
|
+
return isErc20Reason(reason)
|
|
24971
|
+
? { name: 'Error', family: 'erc20', reason }
|
|
24972
|
+
: null;
|
|
24973
|
+
}
|
|
24974
|
+
const name = selectors().get(selector);
|
|
24975
|
+
if (!name)
|
|
24976
|
+
return null;
|
|
24977
|
+
return {
|
|
24978
|
+
name,
|
|
24979
|
+
family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
|
|
24980
|
+
};
|
|
24981
|
+
}
|
|
24982
|
+
function decodeStringRevert(data) {
|
|
24983
|
+
try {
|
|
24984
|
+
const [reason] = ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
|
|
24985
|
+
return reason;
|
|
24986
|
+
}
|
|
24987
|
+
catch (_a) {
|
|
24988
|
+
return null;
|
|
24989
|
+
}
|
|
24990
|
+
}
|
|
24991
|
+
function isErc20Reason(reason) {
|
|
24992
|
+
const lower = reason.toLowerCase();
|
|
24993
|
+
return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
|
|
24994
|
+
}
|
|
24995
|
+
|
|
24996
|
+
/**
|
|
24997
|
+
* The money-path checks around an atomic, sponsored deposit batch — the
|
|
24998
|
+
* `approve + requestDepositWithKyc` pair `DepositFlow` hands its
|
|
24999
|
+
* `depositBatch` port.
|
|
25000
|
+
*
|
|
25001
|
+
* Two questions, both PURE:
|
|
25002
|
+
*
|
|
25003
|
+
* 1. Before anything is signed: is this batch exactly the deposit this run
|
|
25004
|
+
* asked for? (`validateDepositBatchCalls`)
|
|
25005
|
+
* 2. After it is mined: did THIS receipt deposit THIS amount, for THIS
|
|
25006
|
+
* lender, into THIS tranche? (`proveDepositInReceipt`)
|
|
25007
|
+
*
|
|
25008
|
+
* Neither knows a relay, a wallet vendor or a URL; the app's submitter calls
|
|
25009
|
+
* them.
|
|
25010
|
+
*/
|
|
25011
|
+
/**
|
|
25012
|
+
* ERC-4337 EntryPoint v0.7 — the contract a bundler calls `handleOps` on. The
|
|
25013
|
+
* proof reads the user operation's outcome from ITS events, and from no other
|
|
25014
|
+
* contract's.
|
|
25015
|
+
*/
|
|
25016
|
+
const ENTRY_POINT_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032';
|
|
25017
|
+
/** EntryPoint v0.7 events (eth-infinitism `IEntryPoint`). */
|
|
25018
|
+
const ENTRY_POINT_EVENTS = new utils$2.Interface([
|
|
25019
|
+
'event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)',
|
|
25020
|
+
'event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)',
|
|
25021
|
+
'event BeforeExecution()',
|
|
25022
|
+
]);
|
|
25023
|
+
/** kasu-contracts `IPendingPool` — emitted by the strategy's pending pool. */
|
|
25024
|
+
const PENDING_POOL_EVENTS = new utils$2.Interface([
|
|
25025
|
+
'event DepositRequested(address indexed user, address indexed tranche, uint256 indexed dNftID, uint256 epochId, uint256 amount)',
|
|
25026
|
+
]);
|
|
25027
|
+
const USER_OPERATION_EVENT = ENTRY_POINT_EVENTS.getEventTopic('UserOperationEvent');
|
|
25028
|
+
const USER_OPERATION_REVERT_REASON = ENTRY_POINT_EVENTS.getEventTopic('UserOperationRevertReason');
|
|
25029
|
+
const BEFORE_EXECUTION = ENTRY_POINT_EVENTS.getEventTopic('BeforeExecution');
|
|
25030
|
+
const DEPOSIT_REQUESTED = PENDING_POOL_EVENTS.getEventTopic('DepositRequested');
|
|
25031
|
+
const ERC20$1 = IERC20MetadataAbi__factory.createInterface();
|
|
25032
|
+
const LENDING_POOL_MANAGER$1 = ILendingPoolManagerAbi__factory.createInterface();
|
|
25033
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
25034
|
+
/**
|
|
25035
|
+
* Case-insensitive address equality; `false` for anything that is not a
|
|
25036
|
+
* 20-byte hex address. Checksums are not enforced — an all-lowercase address
|
|
25037
|
+
* is the subgraph's own spelling.
|
|
25038
|
+
*/
|
|
25039
|
+
function sameAddress(a, b) {
|
|
25040
|
+
return (typeof a === 'string' &&
|
|
25041
|
+
typeof b === 'string' &&
|
|
25042
|
+
ADDRESS_RE.test(a) &&
|
|
25043
|
+
ADDRESS_RE.test(b) &&
|
|
25044
|
+
a.toLowerCase() === b.toLowerCase());
|
|
25045
|
+
}
|
|
25046
|
+
function isHexData(value) {
|
|
25047
|
+
return typeof value === 'string' && utils$2.isHexString(value);
|
|
25048
|
+
}
|
|
25049
|
+
/**
|
|
25050
|
+
* What `validateDepositBatchCalls` throws. `code` says which pin failed; the
|
|
25051
|
+
* message names no party, so a wallet-rejection classifier can never read it
|
|
25052
|
+
* as something the lender did.
|
|
25053
|
+
*/
|
|
25054
|
+
class DepositBatchMismatchError extends Error {
|
|
25055
|
+
constructor(code) {
|
|
25056
|
+
super(`Deposit batch does not match the run (${code})`);
|
|
25057
|
+
this.name = 'DepositBatchMismatchError';
|
|
25058
|
+
this.code = code;
|
|
25059
|
+
Object.setPrototypeOf(this, DepositBatchMismatchError.prototype);
|
|
25060
|
+
}
|
|
25061
|
+
}
|
|
25062
|
+
function mismatch(code) {
|
|
25063
|
+
throw new DepositBatchMismatchError(code);
|
|
25064
|
+
}
|
|
25065
|
+
function isCallShape(call) {
|
|
25066
|
+
if (call === null || typeof call !== 'object')
|
|
25067
|
+
return false;
|
|
25068
|
+
const { to, data } = call;
|
|
25069
|
+
return typeof to === 'string' && ADDRESS_RE.test(to) && isHexData(data);
|
|
25070
|
+
}
|
|
25071
|
+
function isZeroValue(value) {
|
|
25072
|
+
if (value === undefined)
|
|
25073
|
+
return true;
|
|
25074
|
+
try {
|
|
25075
|
+
return BigNumber.from(value).isZero();
|
|
25076
|
+
}
|
|
25077
|
+
catch (_a) {
|
|
25078
|
+
return false;
|
|
25079
|
+
}
|
|
25080
|
+
}
|
|
25081
|
+
function sameAmount(a, b) {
|
|
25082
|
+
return BigNumber.isBigNumber(a) && a.eq(b);
|
|
25083
|
+
}
|
|
25084
|
+
/**
|
|
25085
|
+
* Decode `data` as a call to `name` on `iface`, and refuse it unless it is
|
|
25086
|
+
* the CANONICAL encoding of what it decodes to. The ABI decoder tolerates
|
|
25087
|
+
* trailing bytes and non-minimal offsets; a signature over bytes that decode
|
|
25088
|
+
* "the same" but are not the bytes the SDK produced is not a signature over
|
|
25089
|
+
* this run's deposit.
|
|
25090
|
+
*/
|
|
25091
|
+
function decodeCanonical(iface, name, data, code) {
|
|
25092
|
+
let parsed;
|
|
25093
|
+
try {
|
|
25094
|
+
parsed = iface.parseTransaction({ data });
|
|
25095
|
+
}
|
|
25096
|
+
catch (_a) {
|
|
25097
|
+
return mismatch(code);
|
|
25098
|
+
}
|
|
25099
|
+
if (parsed.name !== name)
|
|
25100
|
+
return mismatch(code);
|
|
25101
|
+
let canonical;
|
|
25102
|
+
try {
|
|
25103
|
+
canonical = iface.encodeFunctionData(parsed.functionFragment, parsed.args);
|
|
25104
|
+
}
|
|
25105
|
+
catch (_b) {
|
|
25106
|
+
return mismatch(code);
|
|
25107
|
+
}
|
|
25108
|
+
if (canonical.toLowerCase() !== data.toLowerCase()) {
|
|
25109
|
+
return mismatch('non-canonical-encoding');
|
|
25110
|
+
}
|
|
25111
|
+
return parsed.args;
|
|
25112
|
+
}
|
|
25113
|
+
/**
|
|
25114
|
+
* Pin a deposit batch to the run that produced it — PURE; throws a
|
|
25115
|
+
* `DepositBatchMismatchError` on ANY deviation, before the lender's key signs
|
|
25116
|
+
* anything.
|
|
25117
|
+
*
|
|
25118
|
+
* The batch must be exactly two calls, in this order, with no native value:
|
|
25119
|
+
*
|
|
25120
|
+
* 0. `stableAsset.approve(spender, A)`;
|
|
25121
|
+
* 1. `spender.requestDepositWithKyc(poolId, trancheId, A, '0x', fixedTermConfigId, …)`;
|
|
25122
|
+
*
|
|
25123
|
+
* with `A` the run's own amount in BOTH — never more, never unlimited (house
|
|
25124
|
+
* rule: exact-amount approvals) — the run's own fixed-term config, each call
|
|
25125
|
+
* the canonical ABI encoding of its arguments, and no swap data. The deposit
|
|
25126
|
+
* data and KYC signature are not pinned here: they are signatures the
|
|
25127
|
+
* contract and the backend verify.
|
|
25128
|
+
*
|
|
25129
|
+
* This is the client's half; a relaying backend checks the same shape again.
|
|
25130
|
+
*/
|
|
25131
|
+
function validateDepositBatchCalls(calls, expected) {
|
|
25132
|
+
const amount = BigNumber.from(expected.amount);
|
|
25133
|
+
if (amount.lte(0))
|
|
25134
|
+
mismatch('non-positive-amount');
|
|
25135
|
+
if (!Array.isArray(calls) || calls.length !== 2)
|
|
25136
|
+
mismatch('call-count');
|
|
25137
|
+
const [approveCall, depositCall] = calls;
|
|
25138
|
+
for (const call of [approveCall, depositCall]) {
|
|
25139
|
+
if (!isCallShape(call))
|
|
25140
|
+
mismatch('malformed-call');
|
|
25141
|
+
if (!isZeroValue(call.value))
|
|
25142
|
+
mismatch('native-value');
|
|
25143
|
+
}
|
|
25144
|
+
// 0 — the approval.
|
|
25145
|
+
if (!sameAddress(approveCall.to, expected.stableAsset)) {
|
|
25146
|
+
mismatch('approve-token');
|
|
25147
|
+
}
|
|
25148
|
+
const [approveSpender, approveAmount] = decodeCanonical(ERC20$1, 'approve', approveCall.data, 'approve-call');
|
|
25149
|
+
if (!sameAddress(approveSpender, expected.spender)) {
|
|
25150
|
+
mismatch('approve-spender');
|
|
25151
|
+
}
|
|
25152
|
+
// 1 — the deposit.
|
|
25153
|
+
if (!sameAddress(depositCall.to, expected.spender)) {
|
|
25154
|
+
mismatch('deposit-target');
|
|
25155
|
+
}
|
|
25156
|
+
const [pool, tranche, maxAmount, swapData, fixedTermConfigId] = decodeCanonical(LENDING_POOL_MANAGER$1, 'requestDepositWithKyc', depositCall.data, 'deposit-call');
|
|
25157
|
+
if (!sameAddress(pool, expected.poolId))
|
|
25158
|
+
mismatch('deposit-pool');
|
|
25159
|
+
if (!sameAddress(tranche, expected.trancheId))
|
|
25160
|
+
mismatch('deposit-tranche');
|
|
25161
|
+
if (swapData !== '0x')
|
|
25162
|
+
mismatch('swap-data');
|
|
25163
|
+
if (!sameAmount(fixedTermConfigId, BigNumber.from(expected.fixedTermConfigId))) {
|
|
25164
|
+
mismatch('fixed-term-config');
|
|
25165
|
+
}
|
|
25166
|
+
// EXACT amount, on both calls, equal to the run's.
|
|
25167
|
+
if (!sameAmount(approveAmount, amount))
|
|
25168
|
+
mismatch('approve-amount');
|
|
25169
|
+
if (!sameAmount(maxAmount, amount))
|
|
25170
|
+
mismatch('deposit-amount');
|
|
25171
|
+
return { tranche: tranche, amount };
|
|
25172
|
+
}
|
|
25173
|
+
/** A log's first topic, lowercased; `undefined` for a log without a topic array. */
|
|
25174
|
+
function topicOf(log) {
|
|
25175
|
+
var _a;
|
|
25176
|
+
return Array.isArray(log.topics)
|
|
25177
|
+
? (_a = log.topics[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase()
|
|
25178
|
+
: undefined;
|
|
25179
|
+
}
|
|
25180
|
+
function parse(iface, log) {
|
|
25181
|
+
try {
|
|
25182
|
+
return iface.parseLog({ topics: [...log.topics], data: log.data });
|
|
25183
|
+
}
|
|
25184
|
+
catch (_a) {
|
|
25185
|
+
return null;
|
|
25186
|
+
}
|
|
25187
|
+
}
|
|
25188
|
+
/**
|
|
25189
|
+
* Did THIS receipt deposit for THIS lender? — PURE.
|
|
25190
|
+
*
|
|
25191
|
+
* A sponsored batch lands as a bundler's `handleOps` transaction, and that
|
|
25192
|
+
* transaction's `status` is success even when the user operation inside it
|
|
25193
|
+
* reverted: the EntryPoint catches the revert, the account rolls back both
|
|
25194
|
+
* calls, nothing is deposited. So `status` decides NOTHING. What does:
|
|
25195
|
+
*
|
|
25196
|
+
* 1. The EntryPoint's `UserOperationEvent` for OUR operation — emitted BY
|
|
25197
|
+
* the EntryPoint, `sender` = lender, and `userOpHash` = the known hash
|
|
25198
|
+
* when there is one.
|
|
25199
|
+
* 2. Its `success` flag. `false` → `reverted`, naming the protocol error
|
|
25200
|
+
* from the matching `UserOperationRevertReason` when it decodes.
|
|
25201
|
+
* 3. A `DepositRequested` with `user` = lender, `tranche` = the run's and
|
|
25202
|
+
* `amount` = the run's, INSIDE our operation's execution: the logs after
|
|
25203
|
+
* the previous operation's event (or `BeforeExecution`, which precedes
|
|
25204
|
+
* the first) and before ours. A deposit from another operation in the
|
|
25205
|
+
* same bundle does not count, and neither does one emitted by a contract
|
|
25206
|
+
* that is not the pending pool, when the pending pool is known.
|
|
25207
|
+
*
|
|
25208
|
+
* Anything short of all three is `unproven`, which a consumer must treat as
|
|
25209
|
+
* PENDING — the money may have moved — and never as a failure.
|
|
25210
|
+
*/
|
|
25211
|
+
function proveDepositInReceipt(receipt, expected) {
|
|
25212
|
+
// Typed as an array, but a receipt is foreign data: check before reading.
|
|
25213
|
+
if (!Array.isArray(receipt.logs)) {
|
|
25214
|
+
return { kind: 'unproven', reason: 'malformed-receipt' };
|
|
25215
|
+
}
|
|
25216
|
+
const amount = BigNumber.from(expected.amount);
|
|
25217
|
+
const op = locateUserOperation(receipt.logs, {
|
|
25218
|
+
sender: expected.lender,
|
|
25219
|
+
userOperationHash: expected.userOperationHash,
|
|
25220
|
+
entryPoint: expected.entryPoint,
|
|
25221
|
+
});
|
|
25222
|
+
if (op === null)
|
|
25223
|
+
return { kind: 'unproven', reason: 'no-user-operation' };
|
|
25224
|
+
if (op === 'ambiguous') {
|
|
25225
|
+
return { kind: 'unproven', reason: 'ambiguous-operation' };
|
|
25226
|
+
}
|
|
25227
|
+
if (!op.success) {
|
|
25228
|
+
return { kind: 'reverted', revertError: op.revertError() };
|
|
25229
|
+
}
|
|
25230
|
+
for (const log of op.window) {
|
|
25231
|
+
if (topicOf(log) !== DEPOSIT_REQUESTED)
|
|
25232
|
+
continue;
|
|
25233
|
+
if (expected.pendingPool !== undefined &&
|
|
25234
|
+
!sameAddress(log.address, expected.pendingPool)) {
|
|
25235
|
+
continue;
|
|
25236
|
+
}
|
|
25237
|
+
const decoded = parse(PENDING_POOL_EVENTS, log);
|
|
25238
|
+
if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'DepositRequested')
|
|
25239
|
+
continue;
|
|
25240
|
+
if (!sameAddress(decoded.args.user, expected.lender))
|
|
25241
|
+
continue;
|
|
25242
|
+
if (!sameAddress(decoded.args.tranche, expected.tranche))
|
|
25243
|
+
continue;
|
|
25244
|
+
if (!sameAmount(decoded.args.amount, amount))
|
|
25245
|
+
continue;
|
|
25246
|
+
return {
|
|
25247
|
+
kind: 'deposited',
|
|
25248
|
+
dNftId: decoded.args.dNftID,
|
|
25249
|
+
blockNumber: receipt.blockNumber,
|
|
25250
|
+
};
|
|
25251
|
+
}
|
|
25252
|
+
return { kind: 'unproven', reason: 'no-deposit-requested' };
|
|
25253
|
+
}
|
|
25254
|
+
/**
|
|
25255
|
+
* Did THIS receipt execute the sender's user operation? — PURE. The
|
|
25256
|
+
* single-call sibling of `proveDepositInReceipt`, for a sponsored
|
|
25257
|
+
* `requestWithdrawal`, a cancel or a fixed-term withdrawal: the same
|
|
25258
|
+
* EntryPoint `UserOperationEvent` from the same contract, for the same
|
|
25259
|
+
* sender (and hash, when known), read the same way — the `handleOps`
|
|
25260
|
+
* transaction's own `status` decides NOTHING, because it is a success even
|
|
25261
|
+
* when the operation inside it reverted.
|
|
25262
|
+
*
|
|
25263
|
+
* `executed` needs the operation's `success` flag. A single-call operation
|
|
25264
|
+
* that succeeded executed its one call; there is no event to match beyond
|
|
25265
|
+
* that, so pass the `userOperationHash` whenever the submitter reports one —
|
|
25266
|
+
* without it, the sender's ONLY operation in the bundle is the one read, and a
|
|
25267
|
+
* bundle holding two of theirs is `ambiguous-operation` (pending), never the
|
|
25268
|
+
* first one's verdict: with one succeeded and one reverted, "executed" would
|
|
25269
|
+
* tell the lender their request is queued when it is not.
|
|
25270
|
+
*/
|
|
25271
|
+
function proveUserOperationInReceipt(receipt, expected) {
|
|
25272
|
+
if (!Array.isArray(receipt.logs)) {
|
|
25273
|
+
return { kind: 'unproven', reason: 'malformed-receipt' };
|
|
25274
|
+
}
|
|
25275
|
+
const op = locateUserOperation(receipt.logs, expected);
|
|
25276
|
+
if (op === null)
|
|
25277
|
+
return { kind: 'unproven', reason: 'no-user-operation' };
|
|
25278
|
+
if (op === 'ambiguous') {
|
|
25279
|
+
return { kind: 'unproven', reason: 'ambiguous-operation' };
|
|
25280
|
+
}
|
|
25281
|
+
if (!op.success) {
|
|
25282
|
+
return { kind: 'reverted', revertError: op.revertError() };
|
|
25283
|
+
}
|
|
25284
|
+
return { kind: 'executed', blockNumber: receipt.blockNumber };
|
|
25285
|
+
}
|
|
25286
|
+
/**
|
|
25287
|
+
* The sender's user operation in a `handleOps` receipt — its `success` flag
|
|
25288
|
+
* and its execution window — `null` when the receipt does not show one, or
|
|
25289
|
+
* `'ambiguous'` when no hash was given and it shows more than one of theirs.
|
|
25290
|
+
*
|
|
25291
|
+
* - The `UserOperationEvent` must be emitted BY the EntryPoint, with
|
|
25292
|
+
* `sender` = the expected sender and, when one is known, `userOpHash` =
|
|
25293
|
+
* the expected hash. Without a hash the sender must have exactly ONE
|
|
25294
|
+
* operation in the bundle: a bundler may pack two of theirs (sequential
|
|
25295
|
+
* nonces, or parallel nonce keys), and reading the first would attribute
|
|
25296
|
+
* the other one's verdict to this call.
|
|
25297
|
+
* - The window is the logs after the previous operation's event (or
|
|
25298
|
+
* `BeforeExecution`, which precedes the first) and before ours: what our
|
|
25299
|
+
* operation's calls emitted, and nothing another operation in the same
|
|
25300
|
+
* bundle did.
|
|
25301
|
+
*/
|
|
25302
|
+
function locateUserOperation(logs, expected) {
|
|
25303
|
+
var _a, _b, _c;
|
|
25304
|
+
const entryPoint = (_a = expected.entryPoint) !== null && _a !== void 0 ? _a : ENTRY_POINT_V07;
|
|
25305
|
+
const wantedHash = (_c = (_b = expected.userOperationHash) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : null;
|
|
25306
|
+
const fromEntryPoint = (log) => sameAddress(log.address, entryPoint);
|
|
25307
|
+
let opIndex = -1;
|
|
25308
|
+
let success = false;
|
|
25309
|
+
for (let i = 0; i < logs.length; i += 1) {
|
|
25310
|
+
const log = logs[i];
|
|
25311
|
+
if (!fromEntryPoint(log) || topicOf(log) !== USER_OPERATION_EVENT) {
|
|
25312
|
+
continue;
|
|
25313
|
+
}
|
|
25314
|
+
const decoded = parse(ENTRY_POINT_EVENTS, log);
|
|
25315
|
+
if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationEvent')
|
|
25316
|
+
continue;
|
|
25317
|
+
const userOpHash = String(decoded.args.userOpHash).toLowerCase();
|
|
25318
|
+
if (wantedHash !== null && userOpHash !== wantedHash)
|
|
25319
|
+
continue;
|
|
25320
|
+
if (!sameAddress(decoded.args.sender, expected.sender))
|
|
25321
|
+
continue;
|
|
25322
|
+
// A second operation of this sender, and no hash to tell them apart.
|
|
25323
|
+
if (opIndex >= 0)
|
|
25324
|
+
return 'ambiguous';
|
|
25325
|
+
opIndex = i;
|
|
25326
|
+
success = decoded.args.success === true;
|
|
25327
|
+
// A hash names exactly one operation; without one, keep scanning so
|
|
25328
|
+
// a second operation of the sender is noticed.
|
|
25329
|
+
if (wantedHash !== null)
|
|
25330
|
+
break;
|
|
25331
|
+
}
|
|
25332
|
+
if (opIndex < 0)
|
|
25333
|
+
return null;
|
|
25334
|
+
// Our operation's execution window.
|
|
25335
|
+
let windowStart = 0;
|
|
25336
|
+
for (let i = opIndex - 1; i >= 0; i -= 1) {
|
|
25337
|
+
const log = logs[i];
|
|
25338
|
+
const topic = topicOf(log);
|
|
25339
|
+
if (fromEntryPoint(log) &&
|
|
25340
|
+
(topic === USER_OPERATION_EVENT || topic === BEFORE_EXECUTION)) {
|
|
25341
|
+
windowStart = i + 1;
|
|
25342
|
+
break;
|
|
25343
|
+
}
|
|
25344
|
+
}
|
|
25345
|
+
const window = logs.slice(windowStart, opIndex);
|
|
25346
|
+
return {
|
|
25347
|
+
success,
|
|
25348
|
+
window,
|
|
25349
|
+
revertError: () => revertErrorIn(window, expected.sender, fromEntryPoint),
|
|
25350
|
+
};
|
|
25351
|
+
}
|
|
25352
|
+
/**
|
|
25353
|
+
* The protocol error a reverted operation named, or `''`. Only a PROTOCOL
|
|
25354
|
+
* error is passed on: an `Error(string)` or an ERC-20 failure is not a name a
|
|
25355
|
+
* consumer has words for.
|
|
25356
|
+
*/
|
|
25357
|
+
function revertErrorIn(window, lender, fromEntryPoint) {
|
|
25358
|
+
for (const log of window) {
|
|
25359
|
+
if (!fromEntryPoint(log))
|
|
25360
|
+
continue;
|
|
25361
|
+
// Through the same guard as the main loop: a malformed log beside the
|
|
25362
|
+
// revert reason must not turn a DEFINITIVE revert into "pending".
|
|
25363
|
+
if (topicOf(log) !== USER_OPERATION_REVERT_REASON)
|
|
25364
|
+
continue;
|
|
25365
|
+
const decoded = parse(ENTRY_POINT_EVENTS, log);
|
|
25366
|
+
if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationRevertReason')
|
|
25367
|
+
continue;
|
|
25368
|
+
if (!sameAddress(decoded.args.sender, lender))
|
|
25369
|
+
continue;
|
|
25370
|
+
const revert = decodeRevert(decoded.args.revertReason);
|
|
25371
|
+
return (revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol' ? revert.name : '';
|
|
25372
|
+
}
|
|
25373
|
+
return '';
|
|
25374
|
+
}
|
|
25375
|
+
|
|
25376
|
+
/**
|
|
25377
|
+
* First read at once, then 3 s backing off ×1.5 to a 10 s ceiling, for up to
|
|
25378
|
+
* 10 minutes. Running out of time is PENDING: the batch was accepted and may
|
|
25379
|
+
* still land.
|
|
25380
|
+
*/
|
|
25381
|
+
const DEFAULT_BATCH_POLLING_POLICY = Object.freeze({
|
|
25382
|
+
initialMs: 3000,
|
|
25383
|
+
maxMs: 10000,
|
|
25384
|
+
backoff: 1.5,
|
|
25385
|
+
timeoutMs: 10 * 60 * 1000,
|
|
25386
|
+
});
|
|
25387
|
+
function resolvePolicy(policy) {
|
|
25388
|
+
var _a, _b, _c, _d;
|
|
25389
|
+
const resolved = {
|
|
25390
|
+
initialMs: (_a = policy === null || policy === void 0 ? void 0 : policy.initialMs) !== null && _a !== void 0 ? _a : DEFAULT_BATCH_POLLING_POLICY.initialMs,
|
|
25391
|
+
maxMs: (_b = policy === null || policy === void 0 ? void 0 : policy.maxMs) !== null && _b !== void 0 ? _b : DEFAULT_BATCH_POLLING_POLICY.maxMs,
|
|
25392
|
+
backoff: (_c = policy === null || policy === void 0 ? void 0 : policy.backoff) !== null && _c !== void 0 ? _c : DEFAULT_BATCH_POLLING_POLICY.backoff,
|
|
25393
|
+
timeoutMs: (_d = policy === null || policy === void 0 ? void 0 : policy.timeoutMs) !== null && _d !== void 0 ? _d : DEFAULT_BATCH_POLLING_POLICY.timeoutMs,
|
|
25394
|
+
};
|
|
25395
|
+
const { initialMs, maxMs, backoff, timeoutMs } = resolved;
|
|
25396
|
+
if (!(Number.isFinite(initialMs) && initialMs > 0) ||
|
|
25397
|
+
!(Number.isFinite(maxMs) && maxMs >= initialMs) ||
|
|
25398
|
+
!(Number.isFinite(backoff) && backoff >= 1) ||
|
|
25399
|
+
!(Number.isFinite(timeoutMs) && timeoutMs > 0)) {
|
|
25400
|
+
throw new TypeError('submitBatch: invalid polling policy');
|
|
25401
|
+
}
|
|
25402
|
+
return resolved;
|
|
25403
|
+
}
|
|
25404
|
+
/**
|
|
25405
|
+
* Submit `calls` and return once the submitter ACCEPTED them; `wait()` then
|
|
25406
|
+
* polls and proves. Use it as the body of a `DepositFlow` `depositBatch`
|
|
25407
|
+
* port, after `validateDepositBatchCalls` and the app's own signing:
|
|
25408
|
+
*
|
|
25409
|
+
* ```ts
|
|
25410
|
+
* depositBatch: (calls) => {
|
|
25411
|
+
* const { tranche, amount } = validateDepositBatchCalls(calls, expected);
|
|
25412
|
+
* return submitBatch(
|
|
25413
|
+
* // `pendingPool`: the strategy's pending pool, so only ITS
|
|
25414
|
+
* // `DepositRequested` can prove the deposit.
|
|
25415
|
+
* { calls, expect: { lender, tranche, amount, pendingPool } },
|
|
25416
|
+
* { post, getStatus, getReceipt, now: Date.now, sleep },
|
|
25417
|
+
* );
|
|
25418
|
+
* },
|
|
25419
|
+
* ```
|
|
25420
|
+
*
|
|
25421
|
+
* | what happened | outcome |
|
|
25422
|
+
* | ---------------------------------------------------- | ---------------------------------------- |
|
|
25423
|
+
* | `post` throws `BatchRevertedError` | rethrown — reverted, nothing deposited |
|
|
25424
|
+
* | `post` throws `BatchSubmissionError` | rethrown with its own outcome |
|
|
25425
|
+
* | `post` throws anything else | `BatchPendingError('post_unknown')` |
|
|
25426
|
+
* | `post` resolves without a transaction id | `BatchPendingError('malformed_answer')` |
|
|
25427
|
+
* | status `reverted` | `BatchRevertedError('')` |
|
|
25428
|
+
* | status `unknown` | `BatchPendingError(code)` |
|
|
25429
|
+
* | polling runs out of time | `BatchPendingError('status_timeout')` |
|
|
25430
|
+
* | receipt unreadable | `BatchPendingError('receipt_unavailable')` |
|
|
25431
|
+
* | receipt: user operation reverted | `BatchRevertedError(revertError)` |
|
|
25432
|
+
* | receipt: this deposit not shown | `BatchPendingError('deposit_unproven:<reason>')` |
|
|
25433
|
+
* | receipt: this lender's deposit, tranche and amount | resolves `ProvenBatch` |
|
|
25434
|
+
*
|
|
25435
|
+
* Nothing on this path ever produces a `failed` outcome: by the time a batch
|
|
25436
|
+
* has been posted, only a revert proves that nothing was deposited.
|
|
25437
|
+
*/
|
|
25438
|
+
function submitBatch(input, ports, policy) {
|
|
25439
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25440
|
+
const resolved = resolvePolicy(policy);
|
|
25441
|
+
const transactionId = yield postOnce(() => ports.post(input.calls));
|
|
25442
|
+
let settled = null;
|
|
25443
|
+
return {
|
|
25444
|
+
transactionId,
|
|
25445
|
+
// Memoised: a second `wait()` reads the same outcome instead of
|
|
25446
|
+
// polling the submitter twice.
|
|
25447
|
+
wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settle(transactionId, input.expect, ports, resolved))),
|
|
25448
|
+
};
|
|
25449
|
+
});
|
|
25450
|
+
}
|
|
25451
|
+
/**
|
|
25452
|
+
* The post, and what its answer means: the transaction id, or the error the
|
|
25453
|
+
* outcome table names. Shared by both submitters, so the two cannot come to
|
|
25454
|
+
* disagree about which throws are ambiguous.
|
|
25455
|
+
*/
|
|
25456
|
+
function postOnce(post) {
|
|
25457
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25458
|
+
let accepted;
|
|
25459
|
+
try {
|
|
25460
|
+
accepted = yield post();
|
|
25461
|
+
}
|
|
25462
|
+
catch (err) {
|
|
25463
|
+
if (err instanceof BatchRevertedError)
|
|
25464
|
+
throw err;
|
|
25465
|
+
if (err instanceof BatchSubmissionError)
|
|
25466
|
+
throw err;
|
|
25467
|
+
throw new BatchPendingError('post_unknown', { cause: err });
|
|
25468
|
+
}
|
|
25469
|
+
const transactionId = accepted === null || accepted === void 0 ? void 0 : accepted.transactionId;
|
|
25470
|
+
if (typeof transactionId !== 'string' || transactionId === '') {
|
|
25471
|
+
throw new BatchPendingError('malformed_answer');
|
|
25472
|
+
}
|
|
25473
|
+
return transactionId;
|
|
25474
|
+
});
|
|
25475
|
+
}
|
|
25476
|
+
function readStatus(ports, transactionId) {
|
|
25477
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25478
|
+
try {
|
|
25479
|
+
return yield ports.getStatus(transactionId);
|
|
25480
|
+
}
|
|
25481
|
+
catch (_a) {
|
|
25482
|
+
return { kind: 'again' };
|
|
25483
|
+
}
|
|
25484
|
+
});
|
|
25485
|
+
}
|
|
25486
|
+
/** Poll until a mined hash, or a typed reason there will not be one. */
|
|
25487
|
+
function waitForMined(transactionId, ports, policy) {
|
|
25488
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25489
|
+
var _a;
|
|
25490
|
+
const deadline = ports.now() + policy.timeoutMs;
|
|
25491
|
+
let userOperationHash = null;
|
|
25492
|
+
let delay = policy.initialMs;
|
|
25493
|
+
for (;;) {
|
|
25494
|
+
const read = yield readStatus(ports, transactionId);
|
|
25495
|
+
switch (read.kind) {
|
|
25496
|
+
case 'mined':
|
|
25497
|
+
if (typeof read.txHash !== 'string' || read.txHash === '') {
|
|
25498
|
+
break; // a malformed "mined" is not an answer; ask again
|
|
25499
|
+
}
|
|
25500
|
+
return {
|
|
25501
|
+
txHash: read.txHash,
|
|
25502
|
+
userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : userOperationHash,
|
|
25503
|
+
};
|
|
25504
|
+
case 'reverted':
|
|
25505
|
+
throw new BatchRevertedError('');
|
|
25506
|
+
case 'unknown':
|
|
25507
|
+
throw new BatchPendingError(read.code);
|
|
25508
|
+
case 'again':
|
|
25509
|
+
if (read.userOperationHash) {
|
|
25510
|
+
userOperationHash = read.userOperationHash;
|
|
25511
|
+
}
|
|
25512
|
+
break;
|
|
25513
|
+
}
|
|
25514
|
+
if (ports.now() + delay > deadline) {
|
|
25515
|
+
throw new BatchPendingError('status_timeout');
|
|
25516
|
+
}
|
|
25517
|
+
yield ports.sleep(delay);
|
|
25518
|
+
delay = Math.min(policy.maxMs, Math.round(delay * policy.backoff));
|
|
25519
|
+
}
|
|
25520
|
+
});
|
|
25521
|
+
}
|
|
25522
|
+
/** Poll to a mined hash and read its receipt; a receipt read that throws is PENDING. */
|
|
25523
|
+
function minedReceipt(transactionId, ports, policy) {
|
|
25524
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25525
|
+
const mined = yield waitForMined(transactionId, ports, policy);
|
|
25526
|
+
try {
|
|
25527
|
+
return {
|
|
25528
|
+
receipt: yield ports.getReceipt(mined.txHash),
|
|
25529
|
+
txHash: mined.txHash,
|
|
25530
|
+
userOperationHash: mined.userOperationHash,
|
|
25531
|
+
};
|
|
25532
|
+
}
|
|
25533
|
+
catch (err) {
|
|
25534
|
+
throw new BatchPendingError('receipt_unavailable', { cause: err });
|
|
25535
|
+
}
|
|
25536
|
+
});
|
|
25537
|
+
}
|
|
25538
|
+
function settle(transactionId, expect, ports, policy) {
|
|
25539
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25540
|
+
const mined = yield minedReceipt(transactionId, ports, policy);
|
|
25541
|
+
let proof;
|
|
25542
|
+
try {
|
|
25543
|
+
proof = proveDepositInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
|
|
25544
|
+
}
|
|
25545
|
+
catch (err) {
|
|
25546
|
+
// A receipt the proof cannot even read proves nothing either way.
|
|
25547
|
+
throw new BatchPendingError('deposit_unproven', { cause: err });
|
|
25548
|
+
}
|
|
25549
|
+
if (proof.kind === 'reverted')
|
|
25550
|
+
throw new BatchRevertedError(proof.revertError);
|
|
25551
|
+
if (proof.kind === 'unproven') {
|
|
25552
|
+
// The proof's reason rides in the code: "no-user-operation" (the hash
|
|
25553
|
+
// is not this lender's operation at all) and "no-deposit-requested"
|
|
25554
|
+
// (the operation ran and shows no deposit) are the two things an
|
|
25555
|
+
// operator reading the alert needs to tell apart first.
|
|
25556
|
+
throw new BatchPendingError(`deposit_unproven:${proof.reason}`);
|
|
25557
|
+
}
|
|
25558
|
+
return { proof, receipt: mined.receipt, txHash: mined.txHash };
|
|
25559
|
+
});
|
|
25560
|
+
}
|
|
25561
|
+
/**
|
|
25562
|
+
* What became of an earlier submission that ended PENDING — ONE status read,
|
|
25563
|
+
* no polling — for an app's guard against lending twice:
|
|
25564
|
+
*
|
|
25565
|
+
* - `deposited` — mined, and the receipt proves the deposit;
|
|
25566
|
+
* - `not-deposited` — definitively reverted (vendor status or the user
|
|
25567
|
+
* operation's own flag);
|
|
25568
|
+
* - `unknown` — anything else, including a read or receipt that failed.
|
|
25569
|
+
*
|
|
25570
|
+
* Only `not-deposited` may let a new lend proceed unprompted; `deposited`
|
|
25571
|
+
* means the lender is told the earlier one landed (`already-submitted`).
|
|
25572
|
+
*/
|
|
25573
|
+
function checkBatchOutcome(transactionId, expect, ports) {
|
|
25574
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25575
|
+
var _a;
|
|
25576
|
+
let read;
|
|
25577
|
+
try {
|
|
25578
|
+
read = yield ports.getStatus(transactionId);
|
|
25579
|
+
}
|
|
25580
|
+
catch (_b) {
|
|
25581
|
+
return 'unknown';
|
|
25582
|
+
}
|
|
25583
|
+
if (read.kind === 'reverted')
|
|
25584
|
+
return 'not-deposited';
|
|
25585
|
+
if (read.kind !== 'mined' || typeof read.txHash !== 'string') {
|
|
25586
|
+
return 'unknown';
|
|
25587
|
+
}
|
|
25588
|
+
try {
|
|
25589
|
+
const receipt = yield ports.getReceipt(read.txHash);
|
|
25590
|
+
const proof = proveDepositInReceipt(receipt, Object.assign(Object.assign({}, expect), { userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : null }));
|
|
25591
|
+
if (proof.kind === 'deposited')
|
|
25592
|
+
return 'deposited';
|
|
25593
|
+
if (proof.kind === 'reverted')
|
|
25594
|
+
return 'not-deposited';
|
|
25595
|
+
return 'unknown';
|
|
25596
|
+
}
|
|
25597
|
+
catch (_c) {
|
|
25598
|
+
return 'unknown';
|
|
25599
|
+
}
|
|
25600
|
+
});
|
|
25601
|
+
}
|
|
25602
|
+
/**
|
|
25603
|
+
* `submitBatch` for ONE sponsored `LendingPoolManager` call — the body of a
|
|
25604
|
+
* `WithdrawFlow` `submitCall` port, and of an app's cancel or
|
|
25605
|
+
* withdraw-at-expiry action. Same post, same polling policy, same outcome
|
|
25606
|
+
* table; the proof is `proveUserOperationInReceipt` (the sender's operation
|
|
25607
|
+
* succeeded) instead of the deposit proof, and an unproven receipt is
|
|
25608
|
+
* `call_unproven:<reason>`.
|
|
25609
|
+
*
|
|
25610
|
+
* ```ts
|
|
25611
|
+
* submitCall: (call) =>
|
|
25612
|
+
* submitSponsoredCall(
|
|
25613
|
+
* { call, expect: { sender: lender } },
|
|
25614
|
+
* { post: (c) => postToMyRelay(c.kind, [c.call]), getStatus, getReceipt, now, sleep },
|
|
25615
|
+
* ),
|
|
25616
|
+
* ```
|
|
25617
|
+
*
|
|
25618
|
+
* The call is checked against its kind before anything is posted — a known
|
|
25619
|
+
* kind, exactly `{ to, data }`, and `data` opening with that kind's selector —
|
|
25620
|
+
* and a mismatch is `BatchSubmissionError('call_mismatch', 'failed')`: nothing
|
|
25621
|
+
* was sent. Build calls with `kasu.calls`, and this never fires.
|
|
25622
|
+
*
|
|
25623
|
+
* As on the deposit path, nothing after the post ever produces `failed`.
|
|
25624
|
+
*/
|
|
25625
|
+
function submitSponsoredCall(input, ports, policy) {
|
|
25626
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25627
|
+
const resolved = resolvePolicy(policy);
|
|
25628
|
+
if (!isWellFormedSponsoredCall(input.call)) {
|
|
25629
|
+
throw new BatchSubmissionError('call_mismatch', 'failed');
|
|
25630
|
+
}
|
|
25631
|
+
const transactionId = yield postOnce(() => ports.post(input.call));
|
|
25632
|
+
let settled = null;
|
|
25633
|
+
return {
|
|
25634
|
+
transactionId,
|
|
25635
|
+
wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settleCall(transactionId, input.expect, ports, resolved))),
|
|
25636
|
+
};
|
|
25637
|
+
});
|
|
25638
|
+
}
|
|
25639
|
+
function isWellFormedSponsoredCall(value) {
|
|
25640
|
+
if (value === null || typeof value !== 'object')
|
|
25641
|
+
return false;
|
|
25642
|
+
const { kind, call } = value;
|
|
25643
|
+
if (typeof kind !== 'string' ||
|
|
25644
|
+
!SPONSORED_CALL_KINDS.includes(kind)) {
|
|
25645
|
+
return false;
|
|
25646
|
+
}
|
|
25647
|
+
if (call === null || typeof call !== 'object')
|
|
25648
|
+
return false;
|
|
25649
|
+
const keys = Object.keys(call);
|
|
25650
|
+
if (keys.length !== 2 || !keys.includes('to') || !keys.includes('data')) {
|
|
25651
|
+
return false;
|
|
25652
|
+
}
|
|
25653
|
+
const { to, data } = call;
|
|
25654
|
+
return (typeof to === 'string' &&
|
|
25655
|
+
/^0x[0-9a-fA-F]{40}$/.test(to) &&
|
|
25656
|
+
typeof data === 'string' &&
|
|
25657
|
+
/^0x(?:[0-9a-fA-F]{2})*$/.test(data) &&
|
|
25658
|
+
data
|
|
25659
|
+
.toLowerCase()
|
|
25660
|
+
.startsWith(SPONSORED_CALL_SELECTORS[kind]));
|
|
25661
|
+
}
|
|
25662
|
+
function settleCall(transactionId, expect, ports, policy) {
|
|
25663
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25664
|
+
const mined = yield minedReceipt(transactionId, ports, policy);
|
|
25665
|
+
let proof;
|
|
25666
|
+
try {
|
|
25667
|
+
proof = proveUserOperationInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
|
|
25668
|
+
}
|
|
25669
|
+
catch (err) {
|
|
25670
|
+
throw new BatchPendingError('call_unproven', { cause: err });
|
|
25671
|
+
}
|
|
25672
|
+
if (proof.kind === 'reverted') {
|
|
25673
|
+
throw new BatchRevertedError(proof.revertError);
|
|
25674
|
+
}
|
|
25675
|
+
if (proof.kind === 'unproven') {
|
|
25676
|
+
throw new BatchPendingError(`call_unproven:${proof.reason}`);
|
|
25677
|
+
}
|
|
25678
|
+
return { proof, receipt: mined.receipt, txHash: mined.txHash };
|
|
25679
|
+
});
|
|
25680
|
+
}
|
|
25681
|
+
|
|
25682
|
+
/**
|
|
25683
|
+
* The tiny observable-state kernel both flows are built on.
|
|
25684
|
+
*
|
|
25685
|
+
* Deliberately not a framework store: no React, no signals, no dependency.
|
|
25686
|
+
* A flow is a long asynchronous run that has to publish every transition to
|
|
25687
|
+
* whatever is rendering it, and has to be able to abandon a run that the
|
|
25688
|
+
* consumer walked away from. That is the whole requirement, and it is 60 lines.
|
|
25689
|
+
*
|
|
25690
|
+
* The `generation` counter is the abandonment mechanism. `reset()` bumps it;
|
|
25691
|
+
* an in-flight run captured the previous value, so every later `patch` it
|
|
25692
|
+
* attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
|
|
25693
|
+
* what makes `reset()` safe to call mid-flight — the run cannot resurrect a
|
|
25694
|
+
* dead state or, worse, drive the UI back into a phase the consumer has left.
|
|
25695
|
+
*/
|
|
25696
|
+
class FlowStore {
|
|
25697
|
+
constructor(_initial) {
|
|
25698
|
+
this._initial = _initial;
|
|
25699
|
+
this._generation = 0;
|
|
25700
|
+
this._listeners = new Set();
|
|
25701
|
+
this._state = _initial;
|
|
25702
|
+
}
|
|
25703
|
+
get state() {
|
|
25704
|
+
return this._state;
|
|
25705
|
+
}
|
|
25706
|
+
/** The run token an in-flight pipeline captures at `start()`. */
|
|
25707
|
+
get generation() {
|
|
25708
|
+
return this._generation;
|
|
25709
|
+
}
|
|
25710
|
+
/** False once `reset()` (or a later `start()`) has superseded `token`. */
|
|
25711
|
+
isCurrent(token) {
|
|
25712
|
+
return this._generation === token;
|
|
25713
|
+
}
|
|
25714
|
+
/**
|
|
25715
|
+
* Subscribe to every transition. Returns the unsubscribe function; calling
|
|
25716
|
+
* it twice is harmless.
|
|
25717
|
+
*
|
|
25718
|
+
* Listeners are NOT called on subscribe — read `state` for the current
|
|
25719
|
+
* value. A listener that throws is not allowed to break the pipeline or
|
|
25720
|
+
* starve the listeners registered after it, so throws are swallowed.
|
|
25721
|
+
*/
|
|
25722
|
+
subscribe(listener) {
|
|
25723
|
+
this._listeners.add(listener);
|
|
25724
|
+
return () => {
|
|
25725
|
+
this._listeners.delete(listener);
|
|
25726
|
+
};
|
|
25727
|
+
}
|
|
25728
|
+
/**
|
|
25729
|
+
* Merge `next` into the state and notify, unless `token` names a
|
|
25730
|
+
* superseded run.
|
|
25731
|
+
*/
|
|
25732
|
+
patch(next, token) {
|
|
25733
|
+
if (token !== undefined && !this.isCurrent(token))
|
|
25734
|
+
return;
|
|
25735
|
+
this._state = Object.assign(Object.assign({}, this._state), next);
|
|
25736
|
+
for (const listener of [...this._listeners]) {
|
|
25737
|
+
try {
|
|
25738
|
+
listener(this._state);
|
|
25739
|
+
}
|
|
25740
|
+
catch (_a) {
|
|
25741
|
+
// A consumer's rendering fault is its own problem; the pipeline
|
|
25742
|
+
// owns real money and keeps going.
|
|
25743
|
+
}
|
|
25744
|
+
}
|
|
25745
|
+
}
|
|
25746
|
+
/**
|
|
25747
|
+
* Back to the initial state, abandoning any in-flight run.
|
|
25748
|
+
*
|
|
25749
|
+
* Subscribers are kept: the consumer that was watching the last run is the
|
|
25750
|
+
* one that will watch the next.
|
|
25751
|
+
*/
|
|
25752
|
+
reset() {
|
|
25753
|
+
this._generation += 1;
|
|
25754
|
+
this.patch(this._initial);
|
|
25755
|
+
}
|
|
25756
|
+
/** Claim the next run token. Any earlier run is abandoned. */
|
|
25757
|
+
beginRun() {
|
|
25758
|
+
this._generation += 1;
|
|
25759
|
+
return this._generation;
|
|
25760
|
+
}
|
|
25761
|
+
}
|
|
25762
|
+
|
|
25763
|
+
/**
|
|
25764
|
+
* The run lifecycle both flows share: one run at a time, every transition
|
|
25765
|
+
* published, and a `reset()` that is safe to call in the middle of one.
|
|
25766
|
+
*
|
|
25767
|
+
* `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
|
|
25768
|
+
* re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
|
|
25769
|
+
* `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
|
|
25770
|
+
* wrong, on the money path, which is the same reason the pipelines themselves
|
|
25771
|
+
* moved out of the applications.
|
|
25772
|
+
*
|
|
25773
|
+
* ## The guard and the run token are separate, and both matter
|
|
25774
|
+
*
|
|
25775
|
+
* `_runToken` is the re-entrancy guard: claimed synchronously, before the
|
|
25776
|
+
* first `await`, so a double tap cannot launch two pipelines. The store's
|
|
25777
|
+
* `generation` is the abandonment mechanism: an in-flight run holds the value
|
|
25778
|
+
* it claimed, and every `patch` and `isCurrent` checkpoint it makes after
|
|
25779
|
+
* `reset()` is dropped.
|
|
25780
|
+
*
|
|
25781
|
+
* `reset()` releases the guard IMMEDIATELY rather than waiting for the
|
|
25782
|
+
* abandoned run to notice. It has to: a run parked on a wallet prompt that
|
|
25783
|
+
* never answers is a promise that may never settle, and a consumer that has
|
|
25784
|
+
* left that screen must be able to start a new run in the same tick. The
|
|
25785
|
+
* abandoned run's `finally` therefore releases the guard only if it still
|
|
25786
|
+
* holds it — by then a NEW run may own it, and clearing it there would let a
|
|
25787
|
+
* third run start alongside the second.
|
|
25788
|
+
*/
|
|
25789
|
+
class Flow {
|
|
25790
|
+
constructor(initial) {
|
|
25791
|
+
/**
|
|
25792
|
+
* The token of the run currently holding the guard, or `null` when none
|
|
25793
|
+
* does. Not a boolean: releasing the guard has to be attributable to the
|
|
25794
|
+
* run that claimed it.
|
|
25795
|
+
*/
|
|
25796
|
+
this._runToken = null;
|
|
25797
|
+
this._store = new FlowStore(initial);
|
|
25798
|
+
}
|
|
25799
|
+
/** The current state. Every transition is also published to `subscribe`. */
|
|
25800
|
+
get state() {
|
|
25801
|
+
return this._store.state;
|
|
25802
|
+
}
|
|
25803
|
+
/** True while a run is in flight, including while parked. */
|
|
25804
|
+
get isRunning() {
|
|
24590
25805
|
return this._runToken !== null;
|
|
24591
25806
|
}
|
|
24592
25807
|
/**
|
|
@@ -24804,193 +26019,6 @@ function buildFullNameRequestMessage(address, timestampMs) {
|
|
|
24804
26019
|
return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
|
|
24805
26020
|
}
|
|
24806
26021
|
|
|
24807
|
-
// ---------------------------------------------------------------------------
|
|
24808
|
-
// The selector tables
|
|
24809
|
-
// ---------------------------------------------------------------------------
|
|
24810
|
-
/**
|
|
24811
|
-
* OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
|
|
24812
|
-
* Kasu contract declares them — they arrive from the TOKEN, through the
|
|
24813
|
-
* manager's `transferFrom`, and they are the modern spelling of the string
|
|
24814
|
-
* reverts below.
|
|
24815
|
-
*/
|
|
24816
|
-
const ERC20_ERROR_ABI = [
|
|
24817
|
-
'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
|
|
24818
|
-
'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
|
|
24819
|
-
];
|
|
24820
|
-
/**
|
|
24821
|
-
* The protocol errors that mean the ERC-20 transfer failed rather than the
|
|
24822
|
-
* protocol refusing.
|
|
24823
|
-
*
|
|
24824
|
-
* `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
|
|
24825
|
-
* that returned false or reverted, which on this path IS the `transferFrom`;
|
|
24826
|
-
* `AddressInsufficientBalance` is its native-value sibling. Both belong with
|
|
24827
|
-
* the balance case, not with the pool conditions — a lender who sees one of
|
|
24828
|
-
* them really does need to fund or re-approve.
|
|
24829
|
-
*/
|
|
24830
|
-
const ERC20_FAMILY_NAMES = new Set([
|
|
24831
|
-
'AddressInsufficientBalance',
|
|
24832
|
-
'ERC20InsufficientAllowance',
|
|
24833
|
-
'ERC20InsufficientBalance',
|
|
24834
|
-
'SafeERC20FailedOperation',
|
|
24835
|
-
]);
|
|
24836
|
-
/** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
|
|
24837
|
-
const STRING_REVERT_SELECTOR = '0x08c379a0';
|
|
24838
|
-
/**
|
|
24839
|
-
* The `require` reasons that are a balance or an allowance, across the token
|
|
24840
|
-
* implementations in circulation: OpenZeppelin v4's sentences, and the
|
|
24841
|
-
* shorthand the minimal implementations use.
|
|
24842
|
-
*/
|
|
24843
|
-
const ERC20_REASON_PATTERNS = [
|
|
24844
|
-
/exceeds\s+balance/,
|
|
24845
|
-
/exceeds\s+allowance/,
|
|
24846
|
-
/insufficient\s+balance/,
|
|
24847
|
-
/insufficient\s+allowance/,
|
|
24848
|
-
/transfer_from_failed/,
|
|
24849
|
-
/transferfrom\s+failed/,
|
|
24850
|
-
/low-level\s+call\s+failed/,
|
|
24851
|
-
];
|
|
24852
|
-
/**
|
|
24853
|
-
* selector → error name, built once on first use.
|
|
24854
|
-
*
|
|
24855
|
-
* Lazily, because a consumer that never sees a revert should not pay for three
|
|
24856
|
-
* `Interface` constructions at import. The two Kasu tables are the GENERATED
|
|
24857
|
-
* typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
|
|
24858
|
-
* and the new errors are recognised with no edit here.
|
|
24859
|
-
*/
|
|
24860
|
-
let selectorTable = null;
|
|
24861
|
-
function selectors() {
|
|
24862
|
-
if (selectorTable)
|
|
24863
|
-
return selectorTable;
|
|
24864
|
-
const table = new Map();
|
|
24865
|
-
const interfaces = [
|
|
24866
|
-
// The manager the deposit is called against, and the allow list it
|
|
24867
|
-
// delegates the KYC and block-window checks to.
|
|
24868
|
-
ILendingPoolManagerAbi__factory.createInterface(),
|
|
24869
|
-
IKasuAllowListAbi__factory.createInterface(),
|
|
24870
|
-
new ethers.utils.Interface(ERC20_ERROR_ABI),
|
|
24871
|
-
];
|
|
24872
|
-
for (const iface of interfaces) {
|
|
24873
|
-
for (const fragment of Object.values(iface.errors)) {
|
|
24874
|
-
// First writer wins: the same name declared by both ABIs is the
|
|
24875
|
-
// same selector, so the duplicate would be a no-op anyway.
|
|
24876
|
-
const sighash = iface.getSighash(fragment);
|
|
24877
|
-
if (!table.has(sighash))
|
|
24878
|
-
table.set(sighash, fragment.name);
|
|
24879
|
-
}
|
|
24880
|
-
}
|
|
24881
|
-
selectorTable = table;
|
|
24882
|
-
return table;
|
|
24883
|
-
}
|
|
24884
|
-
// ---------------------------------------------------------------------------
|
|
24885
|
-
// Getting the bytes out of the envelope
|
|
24886
|
-
// ---------------------------------------------------------------------------
|
|
24887
|
-
/**
|
|
24888
|
-
* How deep to look for the revert data. Providers nest it differently —
|
|
24889
|
-
* ethers' own error wraps the provider's, which wraps the RPC body — and a
|
|
24890
|
-
* bound is what keeps a self-referencing error object from being walked
|
|
24891
|
-
* forever.
|
|
24892
|
-
*/
|
|
24893
|
-
const MAX_DEPTH = 6;
|
|
24894
|
-
/** The keys ethers and the providers under it put a wrapped error on. */
|
|
24895
|
-
const NESTED_KEYS = [
|
|
24896
|
-
'data',
|
|
24897
|
-
'error',
|
|
24898
|
-
'originalError',
|
|
24899
|
-
'info',
|
|
24900
|
-
'cause',
|
|
24901
|
-
'body',
|
|
24902
|
-
];
|
|
24903
|
-
/** At least a 4-byte selector, and a whole number of bytes. */
|
|
24904
|
-
function isRevertData(value) {
|
|
24905
|
-
return (value.length >= 10 &&
|
|
24906
|
-
value.length % 2 === 0 &&
|
|
24907
|
-
/^0x[0-9a-fA-F]+$/.test(value));
|
|
24908
|
-
}
|
|
24909
|
-
/**
|
|
24910
|
-
* Find the revert data on an error, wherever the provider left it.
|
|
24911
|
-
*
|
|
24912
|
-
* ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
|
|
24913
|
-
* attached, and each provider nests the bytes one layer differently:
|
|
24914
|
-
* `error.data`, `error.error.data`, `error.data.originalError.data`, or only
|
|
24915
|
-
* inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
|
|
24916
|
-
* products, walk a fixed set of keys to a fixed depth and take the first value
|
|
24917
|
-
* that is shaped like revert data.
|
|
24918
|
-
*/
|
|
24919
|
-
function extractRevertData(err) {
|
|
24920
|
-
return walk(err, 0);
|
|
24921
|
-
}
|
|
24922
|
-
function walk(node, depth) {
|
|
24923
|
-
if (node == null || depth > MAX_DEPTH)
|
|
24924
|
-
return null;
|
|
24925
|
-
if (typeof node === 'string') {
|
|
24926
|
-
if (isRevertData(node))
|
|
24927
|
-
return node;
|
|
24928
|
-
// A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
|
|
24929
|
-
if (!node.startsWith('{'))
|
|
24930
|
-
return null;
|
|
24931
|
-
try {
|
|
24932
|
-
return walk(JSON.parse(node), depth + 1);
|
|
24933
|
-
}
|
|
24934
|
-
catch (_a) {
|
|
24935
|
-
return null;
|
|
24936
|
-
}
|
|
24937
|
-
}
|
|
24938
|
-
if (typeof node !== 'object')
|
|
24939
|
-
return null;
|
|
24940
|
-
const record = node;
|
|
24941
|
-
for (const key of NESTED_KEYS) {
|
|
24942
|
-
const found = walk(record[key], depth + 1);
|
|
24943
|
-
if (found)
|
|
24944
|
-
return found;
|
|
24945
|
-
}
|
|
24946
|
-
return null;
|
|
24947
|
-
}
|
|
24948
|
-
// ---------------------------------------------------------------------------
|
|
24949
|
-
// Decoding
|
|
24950
|
-
// ---------------------------------------------------------------------------
|
|
24951
|
-
/**
|
|
24952
|
-
* Decode a failed call's revert into the error the contract named, or `null`
|
|
24953
|
-
* when there is nothing recognisable to name.
|
|
24954
|
-
*
|
|
24955
|
-
* `null` is the honest answer for an unknown selector, an unrelated `require`
|
|
24956
|
-
* string, or an error with no revert data at all — the caller keeps whatever
|
|
24957
|
-
* it did before rather than inventing a diagnosis from bytes it cannot read.
|
|
24958
|
-
*/
|
|
24959
|
-
function decodeRevert(err) {
|
|
24960
|
-
const data = extractRevertData(err);
|
|
24961
|
-
if (!data)
|
|
24962
|
-
return null;
|
|
24963
|
-
const selector = data.slice(0, 10).toLowerCase();
|
|
24964
|
-
if (selector === STRING_REVERT_SELECTOR) {
|
|
24965
|
-
const reason = decodeStringRevert(data);
|
|
24966
|
-
if (reason === null)
|
|
24967
|
-
return null;
|
|
24968
|
-
return isErc20Reason(reason)
|
|
24969
|
-
? { name: 'Error', family: 'erc20', reason }
|
|
24970
|
-
: null;
|
|
24971
|
-
}
|
|
24972
|
-
const name = selectors().get(selector);
|
|
24973
|
-
if (!name)
|
|
24974
|
-
return null;
|
|
24975
|
-
return {
|
|
24976
|
-
name,
|
|
24977
|
-
family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
|
|
24978
|
-
};
|
|
24979
|
-
}
|
|
24980
|
-
function decodeStringRevert(data) {
|
|
24981
|
-
try {
|
|
24982
|
-
const [reason] = ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
|
|
24983
|
-
return reason;
|
|
24984
|
-
}
|
|
24985
|
-
catch (_a) {
|
|
24986
|
-
return null;
|
|
24987
|
-
}
|
|
24988
|
-
}
|
|
24989
|
-
function isErc20Reason(reason) {
|
|
24990
|
-
const lower = reason.toLowerCase();
|
|
24991
|
-
return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
|
|
24992
|
-
}
|
|
24993
|
-
|
|
24994
26022
|
/**
|
|
24995
26023
|
* Wallet and RPC error predicates — pure functions over `unknown`.
|
|
24996
26024
|
*
|
|
@@ -25172,9 +26200,43 @@ const INITIAL$1 = {
|
|
|
25172
26200
|
stepIndex: 0,
|
|
25173
26201
|
stepTotal: 4,
|
|
25174
26202
|
approvalRequired: true,
|
|
26203
|
+
batched: false,
|
|
25175
26204
|
contract: null,
|
|
25176
26205
|
failure: null,
|
|
25177
26206
|
};
|
|
26207
|
+
const ERC20 = IERC20MetadataAbi__factory.createInterface();
|
|
26208
|
+
const LENDING_POOL_MANAGER = ILendingPoolManagerAbi__factory.createInterface();
|
|
26209
|
+
/**
|
|
26210
|
+
* The batch path's two calls, encoded by the SDK so no consumer has to: the
|
|
26211
|
+
* EXACT-amount approve, then the very `requestDepositWithKyc` the default
|
|
26212
|
+
* `deposit` port sends — `swapData` `'0x'`, no value. Both read `amount` from
|
|
26213
|
+
* the same variable, so the batch cannot approve one figure and deposit
|
|
26214
|
+
* another.
|
|
26215
|
+
*/
|
|
26216
|
+
function encodeDepositBatch(args) {
|
|
26217
|
+
const { stableAsset, spender, input, depositData, kyc } = args;
|
|
26218
|
+
return [
|
|
26219
|
+
{
|
|
26220
|
+
to: stableAsset,
|
|
26221
|
+
data: ERC20.encodeFunctionData('approve', [spender, input.amount]),
|
|
26222
|
+
},
|
|
26223
|
+
{
|
|
26224
|
+
to: spender,
|
|
26225
|
+
data: LENDING_POOL_MANAGER.encodeFunctionData('requestDepositWithKyc', [
|
|
26226
|
+
input.poolId,
|
|
26227
|
+
input.trancheId,
|
|
26228
|
+
input.amount,
|
|
26229
|
+
'0x',
|
|
26230
|
+
input.fixedTermConfigId,
|
|
26231
|
+
depositData,
|
|
26232
|
+
{
|
|
26233
|
+
blockExpiration: kyc.blockExpiration,
|
|
26234
|
+
signature: kyc.signature,
|
|
26235
|
+
},
|
|
26236
|
+
]),
|
|
26237
|
+
},
|
|
26238
|
+
];
|
|
26239
|
+
}
|
|
25178
26240
|
/** 1-based badge position, with `approve` dropped when it is out of scope. */
|
|
25179
26241
|
function stepIndexOf(step, approvalRequired) {
|
|
25180
26242
|
const order = approvalRequired
|
|
@@ -25218,7 +26280,33 @@ function classifyRequest(err) {
|
|
|
25218
26280
|
if (revert !== null || isUnpredictableGas(err)) {
|
|
25219
26281
|
return { step: 'request', reason: 'insufficient-balance', error: err };
|
|
25220
26282
|
}
|
|
25221
|
-
return classifyWalletFailure('request', err);
|
|
26283
|
+
return classifyWalletFailure('request', err);
|
|
26284
|
+
}
|
|
26285
|
+
/**
|
|
26286
|
+
* A `depositBatch` rejection. A submitter that KNOWS the batch reverts says so
|
|
26287
|
+
* with `BatchRevertedError`; anything else is read exactly as a direct
|
|
26288
|
+
* deposit's throw is, so a non-wallet error can never become `cancelled`.
|
|
26289
|
+
*/
|
|
26290
|
+
function classifyBatch(err) {
|
|
26291
|
+
// A submission outcome is read FIRST and taken at its word. It must never
|
|
26292
|
+
// reach the revert-data walk below: that walk follows `cause`, and an
|
|
26293
|
+
// ambiguous outcome whose cause happens to carry revert bytes would read
|
|
26294
|
+
// as `reverted` or `insufficient-balance` — "nothing was deposited, try
|
|
26295
|
+
// again" — on a batch that may have moved the money.
|
|
26296
|
+
if (err instanceof BatchSubmissionError) {
|
|
26297
|
+
return err.outcome === 'failed'
|
|
26298
|
+
? { step: 'request', reason: 'failed', error: err }
|
|
26299
|
+
: { step: 'request', reason: err.outcome, error: err };
|
|
26300
|
+
}
|
|
26301
|
+
if (err instanceof BatchRevertedError) {
|
|
26302
|
+
return {
|
|
26303
|
+
step: 'request',
|
|
26304
|
+
reason: 'reverted',
|
|
26305
|
+
revertError: err.revertError,
|
|
26306
|
+
error: err,
|
|
26307
|
+
};
|
|
26308
|
+
}
|
|
26309
|
+
return classifyRequest(err);
|
|
25222
26310
|
}
|
|
25223
26311
|
// ---------------------------------------------------------------------------
|
|
25224
26312
|
// The flow
|
|
@@ -25246,8 +26334,15 @@ class DepositFlow extends Flow {
|
|
|
25246
26334
|
* A stale token simply is not the current generation.
|
|
25247
26335
|
*/
|
|
25248
26336
|
this._acceptingFor = null;
|
|
26337
|
+
/**
|
|
26338
|
+
* The batch path's authorisation handshake. Same shape and lifetime as
|
|
26339
|
+
* `_accept`: the run parks on it, `authoriseTransfer`, `declineTransfer`
|
|
26340
|
+
* and `reset` settle it, and it is cleared the moment it settles.
|
|
26341
|
+
*/
|
|
26342
|
+
this._authorise = null;
|
|
25249
26343
|
this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
|
|
25250
26344
|
this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
|
|
26345
|
+
this._stableAsset = opts === null || opts === void 0 ? void 0 : opts.stableAsset;
|
|
25251
26346
|
// Called through the ports object, never captured off it: a consumer
|
|
25252
26347
|
// whose clock is a method on its own object keeps its `this`.
|
|
25253
26348
|
this._now = () => { var _a, _b; return (_b = (_a = _ports.now) === null || _a === void 0 ? void 0 : _a.call(_ports)) !== null && _b !== void 0 ? _b : Date.now(); };
|
|
@@ -25309,6 +26404,33 @@ class DepositFlow extends Flow {
|
|
|
25309
26404
|
this._accept = null;
|
|
25310
26405
|
bridge.settle({ kind: 'declined' });
|
|
25311
26406
|
}
|
|
26407
|
+
/**
|
|
26408
|
+
* Batch path: authorise the single transaction that approves and deposits,
|
|
26409
|
+
* and resume the parked run. A no-op when nothing is parked on
|
|
26410
|
+
* `awaiting-authorise`, so a double tap cannot submit twice.
|
|
26411
|
+
*
|
|
26412
|
+
* The batch has no wallet prompt per call, so this is the lender's
|
|
26413
|
+
* explicit consent to the transfer — call it only from their action.
|
|
26414
|
+
*/
|
|
26415
|
+
authoriseTransfer() {
|
|
26416
|
+
const bridge = this._authorise;
|
|
26417
|
+
if (!bridge)
|
|
26418
|
+
return;
|
|
26419
|
+
this._authorise = null;
|
|
26420
|
+
bridge.settle({ kind: 'authorised' });
|
|
26421
|
+
}
|
|
26422
|
+
/**
|
|
26423
|
+
* Batch path: refuse the transfer. The run ends on `error` with `{ step:
|
|
26424
|
+
* 'request', reason: 'cancelled' }` and nothing is submitted. A no-op when
|
|
26425
|
+
* nothing is parked on `awaiting-authorise`.
|
|
26426
|
+
*/
|
|
26427
|
+
declineTransfer() {
|
|
26428
|
+
const bridge = this._authorise;
|
|
26429
|
+
if (!bridge)
|
|
26430
|
+
return;
|
|
26431
|
+
this._authorise = null;
|
|
26432
|
+
bridge.settle({ kind: 'declined' });
|
|
26433
|
+
}
|
|
25312
26434
|
/** True only while THIS generation is waiting on the acceptance signature. */
|
|
25313
26435
|
_isAccepting() {
|
|
25314
26436
|
return (this._acceptingFor !== null &&
|
|
@@ -25320,6 +26442,9 @@ class DepositFlow extends Flow {
|
|
|
25320
26442
|
this._accept = null;
|
|
25321
26443
|
this._acceptingFor = null;
|
|
25322
26444
|
bridge === null || bridge === void 0 ? void 0 : bridge.settle({ kind: 'abandoned' });
|
|
26445
|
+
const authorise = this._authorise;
|
|
26446
|
+
this._authorise = null;
|
|
26447
|
+
authorise === null || authorise === void 0 ? void 0 : authorise.settle({ kind: 'abandoned' });
|
|
25323
26448
|
}
|
|
25324
26449
|
// -----------------------------------------------------------------------
|
|
25325
26450
|
_run(input, token) {
|
|
@@ -25327,6 +26452,7 @@ class DepositFlow extends Flow {
|
|
|
25327
26452
|
var _a;
|
|
25328
26453
|
this._accept = null;
|
|
25329
26454
|
this._acceptingFor = null;
|
|
26455
|
+
this._authorise = null;
|
|
25330
26456
|
const ports = this._ports;
|
|
25331
26457
|
const owner = input.userAddress.toLowerCase();
|
|
25332
26458
|
// 0. The spender, from the input or from the chain config the facade
|
|
@@ -25347,21 +26473,32 @@ class DepositFlow extends Flow {
|
|
|
25347
26473
|
// live, never cached: an exact-amount approval is fully consumed by
|
|
25348
26474
|
// the deposit it paid for, so a stale allowance is exactly the value
|
|
25349
26475
|
// that would wrongly skip the approve and revert the deposit.
|
|
25350
|
-
let
|
|
26476
|
+
let allowanceShort = true;
|
|
25351
26477
|
try {
|
|
25352
26478
|
const allowance = yield ports.readAllowance(owner, spender);
|
|
25353
|
-
|
|
26479
|
+
allowanceShort = allowance.lt(input.amount);
|
|
25354
26480
|
}
|
|
25355
26481
|
catch (_b) {
|
|
25356
26482
|
// A read failure is not a reason to skip an approval. Assume one is
|
|
25357
26483
|
// needed: the cost is a redundant approve, the alternative is a
|
|
25358
26484
|
// reverted deposit.
|
|
25359
|
-
|
|
26485
|
+
allowanceShort = true;
|
|
25360
26486
|
}
|
|
25361
26487
|
if (!this._store.isCurrent(token))
|
|
25362
26488
|
return;
|
|
26489
|
+
// The path is chosen HERE, with the allowance, so the badge is right
|
|
26490
|
+
// from the first state the lender sees. Batch only when there is an
|
|
26491
|
+
// approval to fold in, a port to submit it, and a known token to
|
|
26492
|
+
// approve on — never a guessed one. A batched run never enters the
|
|
26493
|
+
// `approve` step, so every badge counts it as a three-step run.
|
|
26494
|
+
const stableAsset = this._stableAsset;
|
|
26495
|
+
const batched = allowanceShort &&
|
|
26496
|
+
typeof ports.depositBatch === 'function' &&
|
|
26497
|
+
!!stableAsset;
|
|
26498
|
+
const approvalRequired = allowanceShort && !batched;
|
|
25363
26499
|
// 2. Generate — sign the auth message.
|
|
25364
|
-
this._store.patch(Object.assign({ approvalRequired,
|
|
26500
|
+
this._store.patch(Object.assign({ approvalRequired,
|
|
26501
|
+
batched, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
|
|
25365
26502
|
const timestamp = this._now();
|
|
25366
26503
|
const signedMessage = buildAuthMessage(input, owner, timestamp);
|
|
25367
26504
|
let signature;
|
|
@@ -25463,6 +26600,18 @@ class DepositFlow extends Flow {
|
|
|
25463
26600
|
});
|
|
25464
26601
|
return;
|
|
25465
26602
|
}
|
|
26603
|
+
if (batched && stableAsset) {
|
|
26604
|
+
yield this._runBatch({
|
|
26605
|
+
token,
|
|
26606
|
+
input,
|
|
26607
|
+
owner,
|
|
26608
|
+
spender,
|
|
26609
|
+
stableAsset,
|
|
26610
|
+
depositData,
|
|
26611
|
+
contract,
|
|
26612
|
+
});
|
|
26613
|
+
return;
|
|
26614
|
+
}
|
|
25466
26615
|
// 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
|
|
25467
26616
|
// unlimited allowance outlives the deposit it was granted for, and a
|
|
25468
26617
|
// later exploit of the spender would drain a wallet that has long
|
|
@@ -25526,6 +26675,108 @@ class DepositFlow extends Flow {
|
|
|
25526
26675
|
this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
|
|
25527
26676
|
});
|
|
25528
26677
|
}
|
|
26678
|
+
/**
|
|
26679
|
+
* The batch path, from the accepted agreement to the receipt.
|
|
26680
|
+
*
|
|
26681
|
+
* 1. PARK on `awaiting-authorise`. A batch puts no wallet prompt in front
|
|
26682
|
+
* of each call, so the flow waits for an explicit authorisation before
|
|
26683
|
+
* any money moves. `declineTransfer()` ends it as `cancelled`.
|
|
26684
|
+
* 2. Re-check the TTL: the lender may have idled on the authorisation as
|
|
26685
|
+
* long as they did on the agreement.
|
|
26686
|
+
* 3. Only now fetch the KYC signature — it has a block expiration of its
|
|
26687
|
+
* own, and an authorisation left open must not spend it.
|
|
26688
|
+
* 4. Encode both calls and hand them to `depositBatch`; then the receipt.
|
|
26689
|
+
*/
|
|
26690
|
+
_runBatch(args) {
|
|
26691
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26692
|
+
const { token, input, owner, spender, stableAsset, depositData } = args;
|
|
26693
|
+
const ports = this._ports;
|
|
26694
|
+
// A batched run is a three-step run on every badge.
|
|
26695
|
+
const approvalRequired = false;
|
|
26696
|
+
// Every settler clears the handshake before settling it, so there is
|
|
26697
|
+
// nothing to clear here — and clearing it anyway could drop the
|
|
26698
|
+
// handshake of a NEWER run that `reset(); start()` already parked.
|
|
26699
|
+
const outcome = yield new Promise((resolve) => {
|
|
26700
|
+
this._authorise = { settle: resolve };
|
|
26701
|
+
this._store.patch(Object.assign({ phase: 'awaiting-authorise' }, badgeFor('request', approvalRequired)), token);
|
|
26702
|
+
});
|
|
26703
|
+
if (!this._store.isCurrent(token))
|
|
26704
|
+
return;
|
|
26705
|
+
if (outcome.kind === 'declined') {
|
|
26706
|
+
this._fail(token, approvalRequired, {
|
|
26707
|
+
step: 'request',
|
|
26708
|
+
reason: 'cancelled',
|
|
26709
|
+
});
|
|
26710
|
+
return;
|
|
26711
|
+
}
|
|
26712
|
+
if (outcome.kind !== 'authorised')
|
|
26713
|
+
return;
|
|
26714
|
+
if (this._now() > args.contract.timestamp + this._ttlMs) {
|
|
26715
|
+
this._fail(token, approvalRequired, {
|
|
26716
|
+
step: 'request',
|
|
26717
|
+
reason: 'contract-expired',
|
|
26718
|
+
});
|
|
26719
|
+
return;
|
|
26720
|
+
}
|
|
26721
|
+
this._store.patch(Object.assign({ phase: 'request-sign' }, badgeFor('request', approvalRequired)), token);
|
|
26722
|
+
// The consumer's backend, so `failed` — never `cancelled` — exactly as
|
|
26723
|
+
// on the two-transaction path.
|
|
26724
|
+
let kyc;
|
|
26725
|
+
try {
|
|
26726
|
+
const kycParams = yield ports.buildKycParams(owner);
|
|
26727
|
+
kyc = yield ports.getKycSignature(kycParams);
|
|
26728
|
+
}
|
|
26729
|
+
catch (err) {
|
|
26730
|
+
this._fail(token, approvalRequired, {
|
|
26731
|
+
step: 'request',
|
|
26732
|
+
reason: 'failed',
|
|
26733
|
+
error: err,
|
|
26734
|
+
});
|
|
26735
|
+
return;
|
|
26736
|
+
}
|
|
26737
|
+
if (!this._store.isCurrent(token))
|
|
26738
|
+
return;
|
|
26739
|
+
// Encoded from values a backend returned (the KYC signature), so a
|
|
26740
|
+
// shape the coder refuses is a failure the consumer can render, not a
|
|
26741
|
+
// rejection out of a `start()` that promises it has none.
|
|
26742
|
+
let calls;
|
|
26743
|
+
try {
|
|
26744
|
+
calls = encodeDepositBatch({
|
|
26745
|
+
stableAsset,
|
|
26746
|
+
spender,
|
|
26747
|
+
input,
|
|
26748
|
+
depositData,
|
|
26749
|
+
kyc,
|
|
26750
|
+
});
|
|
26751
|
+
}
|
|
26752
|
+
catch (err) {
|
|
26753
|
+
this._fail(token, approvalRequired, {
|
|
26754
|
+
step: 'request',
|
|
26755
|
+
reason: 'failed',
|
|
26756
|
+
error: err,
|
|
26757
|
+
});
|
|
26758
|
+
return;
|
|
26759
|
+
}
|
|
26760
|
+
try {
|
|
26761
|
+
// Called through the ports object, so a port that is a method
|
|
26762
|
+
// keeps its `this`. Its presence decided the path; a consumer
|
|
26763
|
+
// that removed it since gets a `failed`, not a crash.
|
|
26764
|
+
if (typeof ports.depositBatch !== 'function') {
|
|
26765
|
+
throw new Error('DepositFlow: the depositBatch port is gone');
|
|
26766
|
+
}
|
|
26767
|
+
const tx = yield ports.depositBatch(calls);
|
|
26768
|
+
this._store.patch(Object.assign({ phase: 'request-confirm' }, badgeFor('request', approvalRequired)), token);
|
|
26769
|
+
yield tx.wait();
|
|
26770
|
+
}
|
|
26771
|
+
catch (err) {
|
|
26772
|
+
this._fail(token, approvalRequired, classifyBatch(err));
|
|
26773
|
+
return;
|
|
26774
|
+
}
|
|
26775
|
+
if (!this._store.isCurrent(token))
|
|
26776
|
+
return;
|
|
26777
|
+
this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
|
|
26778
|
+
});
|
|
26779
|
+
}
|
|
25529
26780
|
_fail(token, approvalRequired, failure) {
|
|
25530
26781
|
this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
|
|
25531
26782
|
}
|
|
@@ -25572,16 +26823,50 @@ const INITIAL = {
|
|
|
25572
26823
|
phase: 'idle',
|
|
25573
26824
|
step: null,
|
|
25574
26825
|
isMax: false,
|
|
26826
|
+
sponsored: false,
|
|
25575
26827
|
failure: null,
|
|
25576
26828
|
};
|
|
25577
26829
|
/** What a KYC pre-check with no `buildKycParams` to call fails with. */
|
|
25578
26830
|
const NO_KYC_PARAMS_MESSAGE = 'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
|
|
26831
|
+
/** What a sponsored run with no `encodeWithdrawal` to call fails with. */
|
|
26832
|
+
const NO_WITHDRAWAL_ENCODER_MESSAGE = 'WithdrawFlow: submitCall was supplied without encodeWithdrawal; build the flow with kasu.flows.withdraw() or pass both';
|
|
26833
|
+
/** What a sponsored run fails with when the encoder returned another kind of call. */
|
|
26834
|
+
const NOT_A_WITHDRAWAL_CALL_MESSAGE = 'WithdrawFlow: encodeWithdrawal returned a call that is not a withdrawal';
|
|
26835
|
+
/**
|
|
26836
|
+
* A `submitCall` / `wait()` rejection, read with the batch path's taxonomy. A
|
|
26837
|
+
* submission outcome is taken at its word FIRST — never through the wallet
|
|
26838
|
+
* classifier, which would read a relay's "request rejected" as the lender's
|
|
26839
|
+
* own doing. Then, from `submitCall` only, a wallet rejection is `cancelled`:
|
|
26840
|
+
* the lender declined to sign and nothing was posted — the same reading a raw
|
|
26841
|
+
* `depositBatch` throw gets. From `wait()` the call has already left, so a
|
|
26842
|
+
* rejection-worded error there is as ambiguous as any other. Everything
|
|
26843
|
+
* unrecognised is `pending`: it may have left.
|
|
26844
|
+
*/
|
|
26845
|
+
function classifySponsored(err, from) {
|
|
26846
|
+
if (err instanceof BatchSubmissionError) {
|
|
26847
|
+
return err.outcome === 'failed'
|
|
26848
|
+
? { step: 'request', reason: 'failed', error: err }
|
|
26849
|
+
: { step: 'request', reason: err.outcome, error: err };
|
|
26850
|
+
}
|
|
26851
|
+
if (err instanceof BatchRevertedError) {
|
|
26852
|
+
return {
|
|
26853
|
+
step: 'request',
|
|
26854
|
+
reason: 'reverted',
|
|
26855
|
+
revertError: err.revertError,
|
|
26856
|
+
error: err,
|
|
26857
|
+
};
|
|
26858
|
+
}
|
|
26859
|
+
if (from === 'submit' && isUserRejected(err)) {
|
|
26860
|
+
return { step: 'request', reason: 'cancelled' };
|
|
26861
|
+
}
|
|
26862
|
+
return { step: 'request', reason: 'pending', error: err };
|
|
26863
|
+
}
|
|
25579
26864
|
// ---------------------------------------------------------------------------
|
|
25580
26865
|
// The flow
|
|
25581
26866
|
// ---------------------------------------------------------------------------
|
|
25582
26867
|
class WithdrawFlow extends Flow {
|
|
25583
26868
|
constructor(_ports) {
|
|
25584
|
-
super(INITIAL);
|
|
26869
|
+
super(Object.assign(Object.assign({}, INITIAL), { sponsored: typeof _ports.submitCall === 'function' }));
|
|
25585
26870
|
this._ports = _ports;
|
|
25586
26871
|
}
|
|
25587
26872
|
_run(input, token) {
|
|
@@ -25612,6 +26897,10 @@ class WithdrawFlow extends Flow {
|
|
|
25612
26897
|
if (!this._store.isCurrent(token))
|
|
25613
26898
|
return;
|
|
25614
26899
|
}
|
|
26900
|
+
if (ports.submitCall) {
|
|
26901
|
+
yield this._runSponsored(input, token, isMax);
|
|
26902
|
+
return;
|
|
26903
|
+
}
|
|
25615
26904
|
// 2. The submission. This one IS a wallet call, so it keeps the
|
|
25616
26905
|
// `cancelled` / `failed` split.
|
|
25617
26906
|
this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
|
|
@@ -25638,6 +26927,72 @@ class WithdrawFlow extends Flow {
|
|
|
25638
26927
|
this._store.patch({ phase: 'success' }, token);
|
|
25639
26928
|
});
|
|
25640
26929
|
}
|
|
26930
|
+
/**
|
|
26931
|
+
* 2'. The sponsored submission: encode, re-check the run, submit, settle.
|
|
26932
|
+
*
|
|
26933
|
+
* The encode reads the chain (the shares), so a run abandoned during it
|
|
26934
|
+
* must stop BEFORE `submitCall` — nothing has been sent yet, and an
|
|
26935
|
+
* abandoned run must never send. A throw while encoding is `failed`:
|
|
26936
|
+
* nothing left. After `submitCall` is called, only the taxonomy decides.
|
|
26937
|
+
*/
|
|
26938
|
+
_runSponsored(input, token, isMax) {
|
|
26939
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26940
|
+
const ports = this._ports;
|
|
26941
|
+
// Only reached when `submitCall` is supplied; narrows it for below.
|
|
26942
|
+
if (!ports.submitCall)
|
|
26943
|
+
return;
|
|
26944
|
+
this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
|
|
26945
|
+
let call;
|
|
26946
|
+
try {
|
|
26947
|
+
if (!ports.encodeWithdrawal) {
|
|
26948
|
+
throw new Error(NO_WITHDRAWAL_ENCODER_MESSAGE);
|
|
26949
|
+
}
|
|
26950
|
+
call = yield ports.encodeWithdrawal({
|
|
26951
|
+
poolId: input.poolId,
|
|
26952
|
+
trancheId: input.trancheId,
|
|
26953
|
+
amount: input.amount,
|
|
26954
|
+
userAddress: input.userAddress,
|
|
26955
|
+
});
|
|
26956
|
+
if ((call === null || call === void 0 ? void 0 : call.kind) !== 'withdrawal') {
|
|
26957
|
+
throw new Error(NOT_A_WITHDRAWAL_CALL_MESSAGE);
|
|
26958
|
+
}
|
|
26959
|
+
}
|
|
26960
|
+
catch (err) {
|
|
26961
|
+
this._store.patch({
|
|
26962
|
+
phase: 'error',
|
|
26963
|
+
failure: { step: 'request', reason: 'failed', error: err },
|
|
26964
|
+
}, token);
|
|
26965
|
+
return;
|
|
26966
|
+
}
|
|
26967
|
+
if (!this._store.isCurrent(token))
|
|
26968
|
+
return;
|
|
26969
|
+
let tx;
|
|
26970
|
+
try {
|
|
26971
|
+
tx = yield ports.submitCall(call);
|
|
26972
|
+
}
|
|
26973
|
+
catch (err) {
|
|
26974
|
+
this._store.patch({ phase: 'error', failure: classifySponsored(err, 'submit') }, token);
|
|
26975
|
+
return;
|
|
26976
|
+
}
|
|
26977
|
+
// The call has left. A run abandoned meanwhile stops HERE, without
|
|
26978
|
+
// `wait()`: nothing it could learn would be published, and a sponsored
|
|
26979
|
+
// `wait()` is minutes of polling against the submitter, not a cheap
|
|
26980
|
+
// receipt read. The submission itself cannot be taken back.
|
|
26981
|
+
if (!this._store.isCurrent(token))
|
|
26982
|
+
return;
|
|
26983
|
+
this._store.patch({ phase: 'request-confirm' }, token);
|
|
26984
|
+
try {
|
|
26985
|
+
yield tx.wait();
|
|
26986
|
+
}
|
|
26987
|
+
catch (err) {
|
|
26988
|
+
this._store.patch({ phase: 'error', failure: classifySponsored(err, 'wait') }, token);
|
|
26989
|
+
return;
|
|
26990
|
+
}
|
|
26991
|
+
if (!this._store.isCurrent(token))
|
|
26992
|
+
return;
|
|
26993
|
+
this._store.patch({ phase: 'success' }, token);
|
|
26994
|
+
});
|
|
26995
|
+
}
|
|
25641
26996
|
}
|
|
25642
26997
|
|
|
25643
26998
|
/**
|
|
@@ -25678,11 +27033,26 @@ class FlowsFacade {
|
|
|
25678
27033
|
* an address whose only wrong value grants an approval to the wrong
|
|
25679
27034
|
* contract.
|
|
25680
27035
|
*/
|
|
25681
|
-
_spender
|
|
27036
|
+
_spender,
|
|
27037
|
+
/**
|
|
27038
|
+
* This chain's stable token address — the target of the batch path's
|
|
27039
|
+
* approve. `undefined` for a hand-built config without `stableAsset`,
|
|
27040
|
+
* in which case a run never batches: it does not guess a token.
|
|
27041
|
+
*/
|
|
27042
|
+
_stableAsset,
|
|
27043
|
+
/**
|
|
27044
|
+
* This instance's call encoders — the sponsored withdraw path's
|
|
27045
|
+
* default `encodeWithdrawal`. Optional so a hand-built facade keeps
|
|
27046
|
+
* constructing; without it a sponsored run fails (`failed`, nothing
|
|
27047
|
+
* sent) unless the consumer passes `encodeWithdrawal` itself.
|
|
27048
|
+
*/
|
|
27049
|
+
_calls) {
|
|
25682
27050
|
this._deposits = _deposits;
|
|
25683
27051
|
this._erc20 = _erc20;
|
|
25684
27052
|
this._isReadOnly = _isReadOnly;
|
|
25685
27053
|
this._spender = _spender;
|
|
27054
|
+
this._stableAsset = _stableAsset;
|
|
27055
|
+
this._calls = _calls;
|
|
25686
27056
|
}
|
|
25687
27057
|
/**
|
|
25688
27058
|
* A deposit pipeline. `readAllowance`, `approve`, `deposit` and
|
|
@@ -25695,9 +27065,13 @@ class FlowsFacade {
|
|
|
25695
27065
|
* delete the default it was meant to keep, and
|
|
25696
27066
|
* `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
|
|
25697
27067
|
* consumer writes a conditional override.
|
|
27068
|
+
*
|
|
27069
|
+
* `depositBatch` has NO default: how several calls become one atomic
|
|
27070
|
+
* transaction is the application's business. Supplying it lets a run
|
|
27071
|
+
* whose allowance is short approve and deposit in one batch.
|
|
25698
27072
|
*/
|
|
25699
27073
|
deposit(ports, opts) {
|
|
25700
|
-
var _a, _b, _c, _d, _e;
|
|
27074
|
+
var _a, _b, _c, _d, _e, _f;
|
|
25701
27075
|
return new DepositFlow({
|
|
25702
27076
|
signMessage: ports.signMessage,
|
|
25703
27077
|
generateContract: ports.generateContract,
|
|
@@ -25712,21 +27086,26 @@ class FlowsFacade {
|
|
|
25712
27086
|
return yield this._erc20().approve(spender, amount);
|
|
25713
27087
|
})),
|
|
25714
27088
|
deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
|
|
27089
|
+
depositBatch: ports.depositBatch,
|
|
25715
27090
|
now: ports.now,
|
|
25716
27091
|
}, {
|
|
25717
27092
|
contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
|
|
25718
27093
|
// Per key here too, for the same reason the ports are: an
|
|
25719
27094
|
// explicit `spender: undefined` must not delete the default.
|
|
25720
27095
|
spender: (_e = opts === null || opts === void 0 ? void 0 : opts.spender) !== null && _e !== void 0 ? _e : this._spender,
|
|
27096
|
+
stableAsset: (_f = opts === null || opts === void 0 ? void 0 : opts.stableAsset) !== null && _f !== void 0 ? _f : this._stableAsset,
|
|
25721
27097
|
});
|
|
25722
27098
|
}
|
|
25723
27099
|
/**
|
|
25724
27100
|
* A withdrawal pipeline. Both write ports and `buildKycParams` default to
|
|
25725
27101
|
* this instance; supplying `getKycSignature` turns the KYC pre-check on.
|
|
27102
|
+
* Supplying `submitCall` turns the sponsored path on, and its encoder
|
|
27103
|
+
* defaults to `kasu.calls.requestWithdrawal` on this instance — which
|
|
27104
|
+
* reads, never writes, so it works on a read-only instance too.
|
|
25726
27105
|
* Defaults are applied per key, for the reason `deposit()` gives.
|
|
25727
27106
|
*/
|
|
25728
27107
|
withdraw(ports = {}) {
|
|
25729
|
-
var _a, _b, _c;
|
|
27108
|
+
var _a, _b, _c, _d;
|
|
25730
27109
|
return new WithdrawFlow({
|
|
25731
27110
|
buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
|
|
25732
27111
|
getKycSignature: ports.getKycSignature,
|
|
@@ -25736,8 +27115,17 @@ class FlowsFacade {
|
|
|
25736
27115
|
amount: params.amount,
|
|
25737
27116
|
})),
|
|
25738
27117
|
withdrawMax: (_c = ports.withdrawMax) !== null && _c !== void 0 ? _c : ((poolId, trancheId, userAddress) => this._deposits.withdrawMax(poolId, trancheId, userAddress)),
|
|
27118
|
+
submitCall: ports.submitCall,
|
|
27119
|
+
encodeWithdrawal: (_d = ports.encodeWithdrawal) !== null && _d !== void 0 ? _d : this._defaultEncoder(),
|
|
25739
27120
|
});
|
|
25740
27121
|
}
|
|
27122
|
+
/** `kasu.calls.requestWithdrawal`, when this facade was given the encoders. */
|
|
27123
|
+
_defaultEncoder() {
|
|
27124
|
+
const calls = this._calls;
|
|
27125
|
+
if (!calls)
|
|
27126
|
+
return undefined;
|
|
27127
|
+
return (params) => calls.requestWithdrawal(params);
|
|
27128
|
+
}
|
|
25741
27129
|
_assertWritable() {
|
|
25742
27130
|
if (this._isReadOnly) {
|
|
25743
27131
|
throw new Error(READ_ONLY_MESSAGE);
|
|
@@ -26032,9 +27420,72 @@ function maxNetRateCeiling(pools, feePercent) {
|
|
|
26032
27420
|
* Handles epoch fetching internally so callers never need to know about epochs.
|
|
26033
27421
|
*/
|
|
26034
27422
|
class StrategiesFacade {
|
|
26035
|
-
constructor(_dataService, _userLending
|
|
27423
|
+
constructor(_dataService, _userLending,
|
|
27424
|
+
/**
|
|
27425
|
+
* Chain pool id → the pool id its CMS record is keyed by (XDC pools
|
|
27426
|
+
* share their Base counterpart's editorial record). Lowercase keys.
|
|
27427
|
+
*/
|
|
27428
|
+
_poolMetadataMapping = {}) {
|
|
26036
27429
|
this._dataService = _dataService;
|
|
26037
27430
|
this._userLending = _userLending;
|
|
27431
|
+
this._poolMetadataMapping = _poolMetadataMapping;
|
|
27432
|
+
}
|
|
27433
|
+
/**
|
|
27434
|
+
* The pool's loss rate and first-loss capital, from its CMS
|
|
27435
|
+
* risk-management record. `null` when the pool has no record.
|
|
27436
|
+
*
|
|
27437
|
+
* Each figure is a NUMBER or `null` — never a guess. The CMS returns
|
|
27438
|
+
* "numeric" fields as a number, a numeric string, an empty string or
|
|
27439
|
+
* null, and an empty string must not become a confident `0`: anything that
|
|
27440
|
+
* is not a finite number reads as `null` (render "—"). `poolLossRate` is a
|
|
27441
|
+
* 0..1 fraction.
|
|
27442
|
+
*
|
|
27443
|
+
* Requires `directusUrl`; without it the call rejects with
|
|
27444
|
+
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
|
|
27445
|
+
*/
|
|
27446
|
+
getRiskPerformance(poolId) {
|
|
27447
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27448
|
+
const metadataId = this._metadataIdOf(poolId);
|
|
27449
|
+
const records = yield this._dataService.getRiskManagement();
|
|
27450
|
+
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
27451
|
+
if (!record)
|
|
27452
|
+
return null;
|
|
27453
|
+
const performance = record.riskPerformance;
|
|
27454
|
+
return {
|
|
27455
|
+
poolLossRate: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.poolLossRate),
|
|
27456
|
+
firstLossCapital: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.firstLossCapital),
|
|
27457
|
+
};
|
|
27458
|
+
});
|
|
27459
|
+
}
|
|
27460
|
+
/**
|
|
27461
|
+
* The credit originator's profile and lending history for a pool — the
|
|
27462
|
+
* CMS record whose `otherKASUPools` lists it. `null` when none does.
|
|
27463
|
+
*
|
|
27464
|
+
* Matched on the chain pool id, then on its metadata id, case-
|
|
27465
|
+
* insensitively. The record is returned as the service maps it; its
|
|
27466
|
+
* numeric fields are CMS-authored, so read them defensively.
|
|
27467
|
+
*
|
|
27468
|
+
* The service filters `otherKASUPools` through the CURRENT chain's
|
|
27469
|
+
* subgraph with the CMS record's (Base-keyed) ids, so on an XDC
|
|
27470
|
+
* deployment every profile lists no pools and this resolves `null` —
|
|
27471
|
+
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
27472
|
+
* candidate becomes live once the service resolves the mapping.
|
|
27473
|
+
*/
|
|
27474
|
+
getOriginator(poolId) {
|
|
27475
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27476
|
+
var _a;
|
|
27477
|
+
const candidates = new Set([
|
|
27478
|
+
poolId.toLowerCase(),
|
|
27479
|
+
this._metadataIdOf(poolId),
|
|
27480
|
+
]);
|
|
27481
|
+
const profiles = yield this._dataService.getPoolDelegateProfileAndHistory();
|
|
27482
|
+
return ((_a = profiles.find((profile) => profile.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())))) !== null && _a !== void 0 ? _a : null);
|
|
27483
|
+
});
|
|
27484
|
+
}
|
|
27485
|
+
_metadataIdOf(poolId) {
|
|
27486
|
+
var _a;
|
|
27487
|
+
const lower = poolId.toLowerCase();
|
|
27488
|
+
return ((_a = this._poolMetadataMapping[lower]) !== null && _a !== void 0 ? _a : lower).toLowerCase();
|
|
26038
27489
|
}
|
|
26039
27490
|
/**
|
|
26040
27491
|
* Fetch all active lending strategies.
|
|
@@ -26113,9 +27564,7 @@ class StrategiesFacade {
|
|
|
26113
27564
|
*/
|
|
26114
27565
|
calculateDepositLimits(tranche) {
|
|
26115
27566
|
const raw = tranche._raw;
|
|
26116
|
-
const min = parseFloat(raw.minimumDeposit) < 1
|
|
26117
|
-
? '1'
|
|
26118
|
-
: raw.minimumDeposit;
|
|
27567
|
+
const min = parseFloat(raw.minimumDeposit) < 1 ? '1' : raw.minimumDeposit;
|
|
26119
27568
|
const max = parseFloat(raw.maximumDeposit) > parseFloat(raw.poolCapacity)
|
|
26120
27569
|
? raw.poolCapacity
|
|
26121
27570
|
: raw.maximumDeposit;
|
|
@@ -26163,14 +27612,146 @@ class StrategiesFacade {
|
|
|
26163
27612
|
};
|
|
26164
27613
|
}
|
|
26165
27614
|
}
|
|
27615
|
+
/**
|
|
27616
|
+
* A CMS "numeric" field as a finite number, or `null`. An empty or blank
|
|
27617
|
+
* string is UNAUTHORED, not zero.
|
|
27618
|
+
*/
|
|
27619
|
+
function finiteOrNull(value) {
|
|
27620
|
+
if (typeof value === 'number')
|
|
27621
|
+
return Number.isFinite(value) ? value : null;
|
|
27622
|
+
if (typeof value === 'string') {
|
|
27623
|
+
if (value.trim() === '')
|
|
27624
|
+
return null;
|
|
27625
|
+
const numeric = Number(value);
|
|
27626
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
27627
|
+
}
|
|
27628
|
+
return null;
|
|
27629
|
+
}
|
|
27630
|
+
|
|
27631
|
+
var UserRequestStatus;
|
|
27632
|
+
(function (UserRequestStatus) {
|
|
27633
|
+
UserRequestStatus["REQUESTED"] = "Requested";
|
|
27634
|
+
UserRequestStatus["PROCESSING"] = "Processing";
|
|
27635
|
+
UserRequestStatus["PROCESSED"] = "Processed";
|
|
27636
|
+
})(UserRequestStatus || (UserRequestStatus = {}));
|
|
27637
|
+
|
|
27638
|
+
/**
|
|
27639
|
+
* Cycles and the clearing window — the protocol's weekly clock, as numbers.
|
|
27640
|
+
*
|
|
27641
|
+
* Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
|
|
27642
|
+
* and `features/lending/lib/cycle-dates.ts`. The two formatters that live
|
|
27643
|
+
* beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
|
|
27644
|
+
* print words and stay in the applications; everything here is unix seconds in
|
|
27645
|
+
* and unix seconds out.
|
|
27646
|
+
*
|
|
27647
|
+
* The clearing window is the fixed 48 hours immediately preceding an epoch
|
|
27648
|
+
* end. Inside it, pending requests are being processed and cannot be modified,
|
|
27649
|
+
* and the countdown runs to the epoch end. Outside it, the countdown runs to
|
|
27650
|
+
* the next clearing-window start.
|
|
27651
|
+
*
|
|
27652
|
+
* The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
|
|
27653
|
+
* the SDK's `getNextEpochDate`) — the same value the protocol's own
|
|
27654
|
+
* `getNextClearingPeriodDate` derives from — so the window always lines up
|
|
27655
|
+
* with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
|
|
27656
|
+
* a projected subgraph timestamp that can drift off the grid.
|
|
27657
|
+
*/
|
|
27658
|
+
/**
|
|
27659
|
+
* Clearing-window length — a fixed 48h protocol constant. Exported so every
|
|
27660
|
+
* consumer derives the window from the same number this module runs on.
|
|
27661
|
+
*/
|
|
27662
|
+
const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
|
|
27663
|
+
/**
|
|
27664
|
+
* Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
|
|
27665
|
+
* Used only to roll a cycle forward when a request lands inside a window that
|
|
27666
|
+
* has already closed.
|
|
27667
|
+
*/
|
|
27668
|
+
const WEEK_SECONDS = 7 * 24 * 60 * 60;
|
|
27669
|
+
/**
|
|
27670
|
+
* Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
|
|
27671
|
+
* of it.
|
|
27672
|
+
*
|
|
27673
|
+
* `'unknown'` when no epoch boundary has been loaded yet, or when the one on
|
|
27674
|
+
* hand is stale (it has already elapsed — the on-chain value refetches to the
|
|
27675
|
+
* next boundary shortly after rollover). A caller must render its "no cycle
|
|
27676
|
+
* loaded" state there, never a zeroed countdown.
|
|
27677
|
+
*/
|
|
27678
|
+
function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
|
|
27679
|
+
// No epoch boundary loaded yet, or a stale one that already elapsed.
|
|
27680
|
+
if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
|
|
27681
|
+
return { phase: 'unknown' };
|
|
27682
|
+
}
|
|
27683
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
27684
|
+
if (nowSeconds < clearingStart) {
|
|
27685
|
+
return {
|
|
27686
|
+
phase: 'awaiting',
|
|
27687
|
+
secondsUntilClearing: clearingStart - nowSeconds,
|
|
27688
|
+
nextClearingStart: clearingStart,
|
|
27689
|
+
};
|
|
27690
|
+
}
|
|
27691
|
+
// nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
|
|
27692
|
+
return {
|
|
27693
|
+
phase: 'clearing',
|
|
27694
|
+
secondsUntilEpochEnd: nextEpochStart - nowSeconds,
|
|
27695
|
+
epochEnd: nextEpochStart,
|
|
27696
|
+
};
|
|
27697
|
+
}
|
|
27698
|
+
/**
|
|
27699
|
+
* The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
|
|
27700
|
+
* cycle close (`nextEpochStart − 48h`) while the window is still open, the
|
|
27701
|
+
* epoch end once we are inside it.
|
|
27702
|
+
*
|
|
27703
|
+
* `undefined` when there is no boundary left to wait for: no epoch boundary
|
|
27704
|
+
* loaded, or a cached one that has already elapsed — the same staleness rule
|
|
27705
|
+
* `computeSettlementWindow` applies before it reports `'unknown'`.
|
|
27706
|
+
*
|
|
27707
|
+
* Split out of the state machine because some consumers need the INSTANT
|
|
27708
|
+
* rather than the phase: one to flush the cycle-dependent caches when the
|
|
27709
|
+
* clock crosses it, one to move a pre-commit screen's snapshot clock at the
|
|
27710
|
+
* same moment.
|
|
27711
|
+
*/
|
|
27712
|
+
function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
|
|
27713
|
+
if (!nextEpochStart || nextEpochStart <= 0)
|
|
27714
|
+
return undefined;
|
|
27715
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
27716
|
+
if (nowSeconds < clearingStart)
|
|
27717
|
+
return clearingStart;
|
|
27718
|
+
if (nowSeconds < nextEpochStart)
|
|
27719
|
+
return nextEpochStart;
|
|
27720
|
+
return undefined;
|
|
27721
|
+
}
|
|
27722
|
+
/**
|
|
27723
|
+
* The cycle-close and outcome dates for a request submitted `now`.
|
|
27724
|
+
*
|
|
27725
|
+
* - The cycle "closes" (stops accepting requests, starts processing) at the
|
|
27726
|
+
* start of the 48h clearing window, i.e. 48h before the epoch end.
|
|
27727
|
+
* - Processing takes up to 48h, so the outcome is confirmed by the epoch end
|
|
27728
|
+
* (close + 48h).
|
|
27729
|
+
*
|
|
27730
|
+
* Returns `null` when the epoch boundary is not available or is stale — a
|
|
27731
|
+
* caller then omits the dates entirely (omit, don't stub).
|
|
27732
|
+
*
|
|
27733
|
+
* The common case is a request submitted OUTSIDE the clearing window: the
|
|
27734
|
+
* close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
|
|
27735
|
+
* the request lands INSIDE the current clearing window (that close is already
|
|
27736
|
+
* in the past), it queues for the NEXT weekly cycle, so the close is advanced
|
|
27737
|
+
* by whole weeks until it is in the future.
|
|
27738
|
+
*/
|
|
27739
|
+
function deriveCycleDates(nextEpochStart, nowSeconds) {
|
|
27740
|
+
// No boundary loaded, or a stale one that already elapsed — the same
|
|
27741
|
+
// staleness rule as `computeSettlementWindow`.
|
|
27742
|
+
if (!nextEpochStart || nextEpochStart <= nowSeconds)
|
|
27743
|
+
return null;
|
|
27744
|
+
let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
|
|
27745
|
+
let outcome = nextEpochStart;
|
|
27746
|
+
// Inside the current clearing window the close already passed; a request
|
|
27747
|
+
// now is queued for the next weekly cycle.
|
|
27748
|
+
while (close <= nowSeconds) {
|
|
27749
|
+
close += WEEK_SECONDS;
|
|
27750
|
+
outcome += WEEK_SECONDS;
|
|
27751
|
+
}
|
|
27752
|
+
return { close, outcome };
|
|
27753
|
+
}
|
|
26166
27754
|
|
|
26167
|
-
var UserRequestStatus;
|
|
26168
|
-
(function (UserRequestStatus) {
|
|
26169
|
-
UserRequestStatus["REQUESTED"] = "Requested";
|
|
26170
|
-
UserRequestStatus["PROCESSING"] = "Processing";
|
|
26171
|
-
UserRequestStatus["PROCESSED"] = "Processed";
|
|
26172
|
-
})(UserRequestStatus || (UserRequestStatus = {}));
|
|
26173
|
-
|
|
26174
27755
|
/**
|
|
26175
27756
|
* Events that represent a lender SUBMISSION into the bundle. A dNFT position
|
|
26176
27757
|
* aggregates every submission the lender made into the same pool/tranche this
|
|
@@ -26231,6 +27812,38 @@ function lastEventTimestamp(events, fallback) {
|
|
|
26231
27812
|
function isCycleClosed(request) {
|
|
26232
27813
|
return !request.canCancel;
|
|
26233
27814
|
}
|
|
27815
|
+
/**
|
|
27816
|
+
* Until when can THIS request still be cancelled? — unix seconds, or `null`
|
|
27817
|
+
* when it cannot be cancelled at all, or when no epoch boundary is loaded.
|
|
27818
|
+
*
|
|
27819
|
+
* The answer is the next cycle close (`deriveCycleDates(...).close`): the
|
|
27820
|
+
* clearing window opens there, and the contract refuses a cancel while the
|
|
27821
|
+
* pool's clearing is pending (`CannotCancelRequestIfClearingIsPending`).
|
|
27822
|
+
* Inside a window whose clearing already ENDED, a still-cancellable request is
|
|
27823
|
+
* queued for the next cycle, so its deadline is next week's close — which is
|
|
27824
|
+
* exactly what `deriveCycleDates` rolls forward to.
|
|
27825
|
+
*
|
|
27826
|
+
* `null` unless the request is BOTH:
|
|
27827
|
+
*
|
|
27828
|
+
* - `canCancel` — the SDK's per-pool signal (see `isCycleClosed`);
|
|
27829
|
+
* - live: `statusCode` `pending`, or `partial` (a partly-filled withdrawal
|
|
27830
|
+
* whose remainder is back in the queue — a resolved partial has
|
|
27831
|
+
* `canCancel: false`). A cancelled, forced, reallocated, rejected or complete
|
|
27832
|
+
* request has nothing left to cancel, whatever `canCancel` says.
|
|
27833
|
+
*
|
|
27834
|
+
* `canCancel` is read ONCE, when the request was fetched. A request held
|
|
27835
|
+
* across a cycle boundary (`nextCycleBoundary`) must be refetched before this
|
|
27836
|
+
* is shown again — the deadline is a hint, the contract is the gate.
|
|
27837
|
+
*/
|
|
27838
|
+
function cancelDeadline(request, nextEpochStart, nowSeconds) {
|
|
27839
|
+
var _a, _b;
|
|
27840
|
+
if (!request.canCancel)
|
|
27841
|
+
return null;
|
|
27842
|
+
if (request.statusCode !== 'pending' && request.statusCode !== 'partial') {
|
|
27843
|
+
return null;
|
|
27844
|
+
}
|
|
27845
|
+
return (_b = (_a = deriveCycleDates(nextEpochStart, nowSeconds)) === null || _a === void 0 ? void 0 : _a.close) !== null && _b !== void 0 ? _b : null;
|
|
27846
|
+
}
|
|
26234
27847
|
const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
|
|
26235
27848
|
const isForced = (events) => events.some((e) => e.requestType === 'Forced');
|
|
26236
27849
|
const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
|
|
@@ -26457,8 +28070,8 @@ class PortfolioFacade {
|
|
|
26457
28070
|
/**
|
|
26458
28071
|
* High-level entry point for external integrators.
|
|
26459
28072
|
*
|
|
26460
|
-
* Provides
|
|
26461
|
-
* `flows` — and exposes the underlying `KasuSdk` services via `.services` for
|
|
28073
|
+
* Provides five domain facades — `strategies`, `deposits`, `portfolio`,
|
|
28074
|
+
* `flows`, `calls` — and exposes the underlying `KasuSdk` services via `.services` for
|
|
26462
28075
|
* power-users.
|
|
26463
28076
|
*
|
|
26464
28077
|
* ```ts
|
|
@@ -26478,18 +28091,22 @@ class PortfolioFacade {
|
|
|
26478
28091
|
*/
|
|
26479
28092
|
class Kasu {
|
|
26480
28093
|
constructor(sdk, chainConfig, signerOrProvider, configOverrides) {
|
|
28094
|
+
var _a, _b;
|
|
26481
28095
|
this._sdk = sdk;
|
|
26482
28096
|
this._chainConfig = chainConfig;
|
|
26483
28097
|
this._signerOrProvider = signerOrProvider;
|
|
26484
28098
|
this._configOverrides = configOverrides;
|
|
26485
|
-
this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending);
|
|
28099
|
+
this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending, normaliseMapping((_a = configOverrides.poolMetadataMapping) !== null && _a !== void 0 ? _a : chainConfig.poolMetadataMapping));
|
|
26486
28100
|
// Derived ONCE and handed to every facade that needs it. Each of them
|
|
26487
28101
|
// re-deriving `Signer.isSigner` is how two facades come to disagree
|
|
26488
28102
|
// about whether the same instance can write.
|
|
26489
28103
|
const isReadOnly = !Signer.isSigner(signerOrProvider);
|
|
26490
28104
|
this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
|
|
26491
28105
|
this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
|
|
26492
|
-
|
|
28106
|
+
// The same `LendingPoolManager` the deposit flow approves and calls:
|
|
28107
|
+
// the chain config's, which is what a relay allow-lists `to` against.
|
|
28108
|
+
this.calls = new CallsFacade(sdk.UserLending, chainConfig.contracts.LendingPoolManager);
|
|
28109
|
+
this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager, (_b = stableAssetOf(chainConfig)) === null || _b === void 0 ? void 0 : _b.address, this.calls);
|
|
26493
28110
|
}
|
|
26494
28111
|
/**
|
|
26495
28112
|
* Create a Kasu instance with built-in chain config.
|
|
@@ -26674,6 +28291,14 @@ function resolveChainConfig(chain) {
|
|
|
26674
28291
|
}
|
|
26675
28292
|
return chain;
|
|
26676
28293
|
}
|
|
28294
|
+
/** Lowercase both sides, so a lookup by any casing of a pool id matches. */
|
|
28295
|
+
function normaliseMapping(mapping) {
|
|
28296
|
+
const out = {};
|
|
28297
|
+
for (const [from, to] of Object.entries(mapping !== null && mapping !== void 0 ? mapping : {})) {
|
|
28298
|
+
out[from.toLowerCase()] = to.toLowerCase();
|
|
28299
|
+
}
|
|
28300
|
+
return out;
|
|
28301
|
+
}
|
|
26677
28302
|
|
|
26678
28303
|
/** Directus instance holding Kasu's pool editorial content. */
|
|
26679
28304
|
const DEFAULT_DIRECTUS_URL = 'https://kasu-finance.directus.app/';
|
|
@@ -26961,17 +28586,143 @@ function ceilToCents(n) {
|
|
|
26961
28586
|
return snapped < n ? (k + 1) / CENTS : snapped;
|
|
26962
28587
|
}
|
|
26963
28588
|
|
|
28589
|
+
const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
28590
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
28591
|
+
/** `https://host[:port][/path]`, no query, fragment, credentials or space. */
|
|
28592
|
+
const HTTPS_BASE = /^https:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._~\-/]*)?$/;
|
|
28593
|
+
function explorerBase(config) {
|
|
28594
|
+
// Read defensively: a custom config written before 2.10.0 has no field,
|
|
28595
|
+
// and TypeScript only tells its author once they bump.
|
|
28596
|
+
const base = config === null || config === void 0 ? void 0 : config.blockExplorerUrl;
|
|
28597
|
+
if (typeof base !== 'string' || !HTTPS_BASE.test(base)) {
|
|
28598
|
+
throw new Error('explorer url: the chain config has no valid https blockExplorerUrl');
|
|
28599
|
+
}
|
|
28600
|
+
return base.replace(/\/+$/, '');
|
|
28601
|
+
}
|
|
28602
|
+
/**
|
|
28603
|
+
* `<blockExplorerUrl>/tx/<txHash>`. Throws unless `txHash` is exactly `0x`
|
|
28604
|
+
* followed by 64 hex digits.
|
|
28605
|
+
*/
|
|
28606
|
+
function explorerTxUrl(config, txHash) {
|
|
28607
|
+
if (typeof txHash !== 'string' || !TX_HASH.test(txHash)) {
|
|
28608
|
+
throw new Error('explorerTxUrl: not a 32-byte 0x-prefixed hex hash');
|
|
28609
|
+
}
|
|
28610
|
+
return `${explorerBase(config)}/tx/${txHash}`;
|
|
28611
|
+
}
|
|
28612
|
+
/**
|
|
28613
|
+
* `<blockExplorerUrl>/address/<address>`, the address as given. Throws unless
|
|
28614
|
+
* `address` is `0x` followed by 40 hex digits with a valid checksum when it
|
|
28615
|
+
* is mixed-case (all-lower and all-upper carry no checksum and are accepted).
|
|
28616
|
+
*/
|
|
28617
|
+
function explorerAddressUrl(config, address) {
|
|
28618
|
+
if (typeof address !== 'string' ||
|
|
28619
|
+
!ADDRESS.test(address) ||
|
|
28620
|
+
!utils$2.isAddress(address)) {
|
|
28621
|
+
throw new Error('explorerAddressUrl: not a valid 0x-prefixed address');
|
|
28622
|
+
}
|
|
28623
|
+
return `${explorerBase(config)}/address/${address}`;
|
|
28624
|
+
}
|
|
28625
|
+
|
|
28626
|
+
/**
|
|
28627
|
+
* One epoch's nominal length in days. A lock is counted in epochs, and the
|
|
28628
|
+
* loan-agreement text states its term from the same 7-day epoch.
|
|
28629
|
+
*/
|
|
28630
|
+
const DAYS_PER_EPOCH = 7;
|
|
28631
|
+
/**
|
|
28632
|
+
* The average calendar month, 365.25 / 12. A lock is counted in epochs, so a
|
|
28633
|
+
* "12-month" term is 52 epochs = 364 days, and only the average-month divisor
|
|
28634
|
+
* lands that on 12 (364 / 30.4375 = 11.96 → 12).
|
|
28635
|
+
*/
|
|
28636
|
+
const DAYS_PER_MONTH = 30.4375;
|
|
28637
|
+
/**
|
|
28638
|
+
* The shortest lock stated in months. Below it a term reads in weeks or days,
|
|
28639
|
+
* so nothing may claim a month count for it — a 3-epoch lock is 21 days, which
|
|
28640
|
+
* the average-month divisor would otherwise round UP to one month.
|
|
28641
|
+
*/
|
|
28642
|
+
const MIN_DAYS_FOR_MONTHS = 30;
|
|
28643
|
+
/**
|
|
28644
|
+
* `'0'`, empty or absent is the variable-rate sentinel, not a fixed term.
|
|
28645
|
+
* Every surface that tags a rate "variable" or "fixed" must derive the tag
|
|
28646
|
+
* through this, so no screen can hardcode a tag that contradicts the rate
|
|
28647
|
+
* beside it.
|
|
28648
|
+
*/
|
|
28649
|
+
function isFixedTermConfigId(configId) {
|
|
28650
|
+
if (configId == null)
|
|
28651
|
+
return false;
|
|
28652
|
+
const trimmed = configId.trim();
|
|
28653
|
+
return trimmed !== '' && trimmed !== '0';
|
|
28654
|
+
}
|
|
28655
|
+
/**
|
|
28656
|
+
* The fixed-term configs this wallet may actually select on a tranche: a
|
|
28657
|
+
* config open to `Everyone`, or an `AllowlistedOnly` config whose allowlist
|
|
28658
|
+
* names the wallet. Anything else is dropped (hide-ineligible): the contract
|
|
28659
|
+
* refuses a deposit into an allowlisted config from a wallet that is not on
|
|
28660
|
+
* it, so offering one is offering a transaction that reverts.
|
|
28661
|
+
*
|
|
28662
|
+
* Address matching is case-insensitive. An allowlist entry whose
|
|
28663
|
+
* `isAllowlisted` is `false` is a wallet that was REMOVED — the subgraph keeps
|
|
28664
|
+
* the entity and flips the flag — so it does not count. An entry that carries
|
|
28665
|
+
* no flag at all counts by its presence, which is how every entry read before
|
|
28666
|
+
* 2.9.0 arrived (the query did not select the field).
|
|
28667
|
+
*
|
|
28668
|
+
* `address` `null` / `undefined` — no wallet yet — sees only the `Everyone`
|
|
28669
|
+
* configs. Order is the tranche's own.
|
|
28670
|
+
*/
|
|
28671
|
+
function availableFixedTermConfigs(tranche, address) {
|
|
28672
|
+
if (!tranche)
|
|
28673
|
+
return [];
|
|
28674
|
+
const lower = address === null || address === void 0 ? void 0 : address.toLowerCase();
|
|
28675
|
+
return tranche.fixedTermConfig.filter((config) => config.fixedTermDepositStatus === 'Everyone' ||
|
|
28676
|
+
(lower != null &&
|
|
28677
|
+
lower !== '' &&
|
|
28678
|
+
config.fixedTermDepositAllowlist.some((entry) => entry.isAllowlisted !==
|
|
28679
|
+
false && entry.userId.toLowerCase() === lower)));
|
|
28680
|
+
}
|
|
28681
|
+
/**
|
|
28682
|
+
* A lock's length in days: `epochLockDuration × 7`.
|
|
28683
|
+
*
|
|
28684
|
+
* FAILS CLOSED: an unparseable, non-finite, zero or negative epoch count
|
|
28685
|
+
* returns `NaN`, never `0` — a zero-day lock is not a term, and a consumer
|
|
28686
|
+
* that rendered it would state one.
|
|
28687
|
+
*
|
|
28688
|
+
* @param epochLockDuration the config's `epochLockDuration`, a decimal string.
|
|
28689
|
+
*/
|
|
28690
|
+
function lockDurationDays(epochLockDuration) {
|
|
28691
|
+
const epochs = parseFloat(epochLockDuration);
|
|
28692
|
+
if (!Number.isFinite(epochs) || epochs <= 0)
|
|
28693
|
+
return NaN;
|
|
28694
|
+
return epochs * DAYS_PER_EPOCH;
|
|
28695
|
+
}
|
|
28696
|
+
/**
|
|
28697
|
+
* A lock in WHOLE months, or `null` when the epoch count is not a positive
|
|
28698
|
+
* number or the lock is shorter than `MIN_DAYS_FOR_MONTHS`.
|
|
28699
|
+
*
|
|
28700
|
+
* Unlike a year-rounding formatter it never rolls up into years: a 104-epoch
|
|
28701
|
+
* lock is 24 months. A consumer that states years does that conversion in its
|
|
28702
|
+
* own words.
|
|
28703
|
+
*/
|
|
28704
|
+
function lockDurationMonths(epochLockDuration) {
|
|
28705
|
+
const days = lockDurationDays(epochLockDuration);
|
|
28706
|
+
if (Number.isNaN(days) || days < MIN_DAYS_FOR_MONTHS)
|
|
28707
|
+
return null;
|
|
28708
|
+
const months = Math.round(days / DAYS_PER_MONTH);
|
|
28709
|
+
return months > 0 ? months : null;
|
|
28710
|
+
}
|
|
28711
|
+
|
|
26964
28712
|
const APXIUM = {
|
|
26965
28713
|
name: 'Apxium',
|
|
26966
28714
|
url: 'https://www.apxium.com/',
|
|
28715
|
+
region: 'Australia',
|
|
26967
28716
|
};
|
|
26968
28717
|
const INVOICEMATE = {
|
|
26969
28718
|
name: 'InvoiceMate',
|
|
26970
28719
|
url: 'https://invoicemate.net/',
|
|
28720
|
+
region: 'Global',
|
|
26971
28721
|
};
|
|
26972
28722
|
const RIXON_CAPITAL = {
|
|
26973
28723
|
name: 'Rixon Capital',
|
|
26974
28724
|
url: 'https://rixon.capital/',
|
|
28725
|
+
region: 'Australia',
|
|
26975
28726
|
};
|
|
26976
28727
|
function poolNameOf(pool) {
|
|
26977
28728
|
var _a, _b;
|
|
@@ -27003,122 +28754,22 @@ function getCreditOriginator(pool) {
|
|
|
27003
28754
|
function getInstitutionalLender(originator) {
|
|
27004
28755
|
return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
|
|
27005
28756
|
}
|
|
27006
|
-
|
|
27007
|
-
/**
|
|
27008
|
-
* Cycles and the clearing window — the protocol's weekly clock, as numbers.
|
|
27009
|
-
*
|
|
27010
|
-
* Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
|
|
27011
|
-
* and `features/lending/lib/cycle-dates.ts`. The two formatters that live
|
|
27012
|
-
* beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
|
|
27013
|
-
* print words and stay in the applications; everything here is unix seconds in
|
|
27014
|
-
* and unix seconds out.
|
|
27015
|
-
*
|
|
27016
|
-
* The clearing window is the fixed 48 hours immediately preceding an epoch
|
|
27017
|
-
* end. Inside it, pending requests are being processed and cannot be modified,
|
|
27018
|
-
* and the countdown runs to the epoch end. Outside it, the countdown runs to
|
|
27019
|
-
* the next clearing-window start.
|
|
27020
|
-
*
|
|
27021
|
-
* The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
|
|
27022
|
-
* the SDK's `getNextEpochDate`) — the same value the protocol's own
|
|
27023
|
-
* `getNextClearingPeriodDate` derives from — so the window always lines up
|
|
27024
|
-
* with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
|
|
27025
|
-
* a projected subgraph timestamp that can drift off the grid.
|
|
27026
|
-
*/
|
|
27027
|
-
/**
|
|
27028
|
-
* Clearing-window length — a fixed 48h protocol constant. Exported so every
|
|
27029
|
-
* consumer derives the window from the same number this module runs on.
|
|
27030
|
-
*/
|
|
27031
|
-
const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
|
|
27032
|
-
/**
|
|
27033
|
-
* Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
|
|
27034
|
-
* Used only to roll a cycle forward when a request lands inside a window that
|
|
27035
|
-
* has already closed.
|
|
27036
|
-
*/
|
|
27037
|
-
const WEEK_SECONDS = 7 * 24 * 60 * 60;
|
|
27038
|
-
/**
|
|
27039
|
-
* Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
|
|
27040
|
-
* of it.
|
|
27041
|
-
*
|
|
27042
|
-
* `'unknown'` when no epoch boundary has been loaded yet, or when the one on
|
|
27043
|
-
* hand is stale (it has already elapsed — the on-chain value refetches to the
|
|
27044
|
-
* next boundary shortly after rollover). A caller must render its "no cycle
|
|
27045
|
-
* loaded" state there, never a zeroed countdown.
|
|
27046
|
-
*/
|
|
27047
|
-
function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
|
|
27048
|
-
// No epoch boundary loaded yet, or a stale one that already elapsed.
|
|
27049
|
-
if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
|
|
27050
|
-
return { phase: 'unknown' };
|
|
27051
|
-
}
|
|
27052
|
-
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
27053
|
-
if (nowSeconds < clearingStart) {
|
|
27054
|
-
return {
|
|
27055
|
-
phase: 'awaiting',
|
|
27056
|
-
secondsUntilClearing: clearingStart - nowSeconds,
|
|
27057
|
-
nextClearingStart: clearingStart,
|
|
27058
|
-
};
|
|
27059
|
-
}
|
|
27060
|
-
// nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
|
|
27061
|
-
return {
|
|
27062
|
-
phase: 'clearing',
|
|
27063
|
-
secondsUntilEpochEnd: nextEpochStart - nowSeconds,
|
|
27064
|
-
epochEnd: nextEpochStart,
|
|
27065
|
-
};
|
|
27066
|
-
}
|
|
27067
|
-
/**
|
|
27068
|
-
* The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
|
|
27069
|
-
* cycle close (`nextEpochStart − 48h`) while the window is still open, the
|
|
27070
|
-
* epoch end once we are inside it.
|
|
27071
|
-
*
|
|
27072
|
-
* `undefined` when there is no boundary left to wait for: no epoch boundary
|
|
27073
|
-
* loaded, or a cached one that has already elapsed — the same staleness rule
|
|
27074
|
-
* `computeSettlementWindow` applies before it reports `'unknown'`.
|
|
27075
|
-
*
|
|
27076
|
-
* Split out of the state machine because some consumers need the INSTANT
|
|
27077
|
-
* rather than the phase: one to flush the cycle-dependent caches when the
|
|
27078
|
-
* clock crosses it, one to move a pre-commit screen's snapshot clock at the
|
|
27079
|
-
* same moment.
|
|
27080
|
-
*/
|
|
27081
|
-
function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
|
|
27082
|
-
if (!nextEpochStart || nextEpochStart <= 0)
|
|
27083
|
-
return undefined;
|
|
27084
|
-
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
27085
|
-
if (nowSeconds < clearingStart)
|
|
27086
|
-
return clearingStart;
|
|
27087
|
-
if (nowSeconds < nextEpochStart)
|
|
27088
|
-
return nextEpochStart;
|
|
27089
|
-
return undefined;
|
|
27090
|
-
}
|
|
27091
28757
|
/**
|
|
27092
|
-
* The
|
|
27093
|
-
*
|
|
27094
|
-
*
|
|
27095
|
-
*
|
|
27096
|
-
* -
|
|
27097
|
-
*
|
|
27098
|
-
*
|
|
27099
|
-
*
|
|
27100
|
-
*
|
|
27101
|
-
*
|
|
27102
|
-
*
|
|
27103
|
-
*
|
|
27104
|
-
* the request lands INSIDE the current clearing window (that close is already
|
|
27105
|
-
* in the past), it queues for the NEXT weekly cycle, so the close is advanced
|
|
27106
|
-
* by whole weeks until it is in the future.
|
|
28758
|
+
* The strategy's region — `'Australia'` for the Apxium family, `'Global'` for
|
|
28759
|
+
* PayFi — derived from its credit originator, exactly as every surface that
|
|
28760
|
+
* states a region must derive it.
|
|
28761
|
+
*
|
|
28762
|
+
* LOAD-BEARING ON THE MONEY PATH: this is the `region` a lender's wallet signs
|
|
28763
|
+
* in the loan-agreement message (`buildLoanAgreementSignMessage`). The backend
|
|
28764
|
+
* rebuilds that message from whatever region the client sends and treats it
|
|
28765
|
+
* as display-only, so nothing but this function keeps the signed word equal to
|
|
28766
|
+
* the word on screen. Never state a region from a constant — that is how a
|
|
28767
|
+
* PayFi lender came to pick "Global · Junior" and sign "Australia".
|
|
28768
|
+
*
|
|
28769
|
+
* Replace the inference with a per-pool field (in the SDK) when one ships.
|
|
27107
28770
|
*/
|
|
27108
|
-
function
|
|
27109
|
-
|
|
27110
|
-
// staleness rule as `computeSettlementWindow`.
|
|
27111
|
-
if (!nextEpochStart || nextEpochStart <= nowSeconds)
|
|
27112
|
-
return null;
|
|
27113
|
-
let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
|
|
27114
|
-
let outcome = nextEpochStart;
|
|
27115
|
-
// Inside the current clearing window the close already passed; a request
|
|
27116
|
-
// now is queued for the next weekly cycle.
|
|
27117
|
-
while (close <= nowSeconds) {
|
|
27118
|
-
close += WEEK_SECONDS;
|
|
27119
|
-
outcome += WEEK_SECONDS;
|
|
27120
|
-
}
|
|
27121
|
-
return { close, outcome };
|
|
28771
|
+
function getStrategyRegion(pool) {
|
|
28772
|
+
return getCreditOriginator(pool).region;
|
|
27122
28773
|
}
|
|
27123
28774
|
|
|
27124
28775
|
// Business rename (2026-07): Apxium strategies market their top retail tranche
|
|
@@ -27150,4 +28801,155 @@ function getTrancheDisplayName(trancheName, pool) {
|
|
|
27150
28801
|
: trancheName;
|
|
27151
28802
|
}
|
|
27152
28803
|
|
|
27153
|
-
|
|
28804
|
+
/**
|
|
28805
|
+
* Forward-looking interest on a lender's CURRENT balances — the figures a
|
|
28806
|
+
* portfolio shows as "weekly", "daily" and the moving part of "lifetime"
|
|
28807
|
+
* interest, and the "≈ $Y a year" line beside a lend amount.
|
|
28808
|
+
*
|
|
28809
|
+
* Every figure is NET of the platform performance fee, the way the chain
|
|
28810
|
+
* credits it: `LendingPool._applyTrancheInterest` mints the lender
|
|
28811
|
+
* `interest × (1 − performanceFee)` at each cycle close, and only that net
|
|
28812
|
+
* amount ever reaches the balance. A gross projection would out-run what the
|
|
28813
|
+
* next close actually credits.
|
|
28814
|
+
*
|
|
28815
|
+
* Units: balances and results are display units of the deployment's stable
|
|
28816
|
+
* asset (not base units); rates are 0..1 fractions; `feePercent` is a
|
|
28817
|
+
* PERCENTAGE IN 0..100 — see `netEffectiveApy`.
|
|
28818
|
+
*/
|
|
28819
|
+
/**
|
|
28820
|
+
* Seconds in one epoch's nominal week. The per-epoch rate covers exactly this
|
|
28821
|
+
* window, and the accrual spreads an epoch's interest across it.
|
|
28822
|
+
*/
|
|
28823
|
+
const SECONDS_PER_WEEK = 604800;
|
|
28824
|
+
function isUsableFeePercent(feePercent) {
|
|
28825
|
+
return (feePercent !== null &&
|
|
28826
|
+
Number.isFinite(feePercent) &&
|
|
28827
|
+
feePercent >= 0 &&
|
|
28828
|
+
feePercent <= 100);
|
|
28829
|
+
}
|
|
28830
|
+
/**
|
|
28831
|
+
* Interest the slices accrue over ONE epoch, net of the fee.
|
|
28832
|
+
*
|
|
28833
|
+
* ```
|
|
28834
|
+
* weekly = Σ(slice.balance × slice.epochRate) × (1 − feePercent / 100)
|
|
28835
|
+
* ```
|
|
28836
|
+
*
|
|
28837
|
+
* **The rate is the raw per-epoch one, never the APY.** An APY is that rate
|
|
28838
|
+
* already compounded over `EPOCHS_IN_YEAR`; dividing it by 365 charges a day
|
|
28839
|
+
* of compounded annual growth to a single day and overstates it.
|
|
28840
|
+
*
|
|
28841
|
+
* FAILS CLOSED, returning `null` rather than a wrong number:
|
|
28842
|
+
* - a `null`, non-finite or out-of-domain fee — a fee-less projection dressed
|
|
28843
|
+
* up as the lender's net earnings is the failure this guards;
|
|
28844
|
+
* - any slice with a POSITIVE balance and a non-finite rate — skipping it
|
|
28845
|
+
* would return a silently partial total, quietly missing that slice.
|
|
28846
|
+
*
|
|
28847
|
+
* Non-finite and non-positive BALANCES are skipped: a slice with nothing in it
|
|
28848
|
+
* earns nothing whatever its rate, so it cannot hide earnings. A portfolio
|
|
28849
|
+
* with nothing lent is therefore a legitimate `0`, never `null`.
|
|
28850
|
+
*/
|
|
28851
|
+
function weeklyInterest(slices, feePercent) {
|
|
28852
|
+
if (!isUsableFeePercent(feePercent))
|
|
28853
|
+
return null;
|
|
28854
|
+
let gross = 0;
|
|
28855
|
+
for (const slice of slices) {
|
|
28856
|
+
if (!Number.isFinite(slice.balance) || slice.balance <= 0)
|
|
28857
|
+
continue;
|
|
28858
|
+
if (!Number.isFinite(slice.epochRate))
|
|
28859
|
+
return null;
|
|
28860
|
+
gross += slice.balance * slice.epochRate;
|
|
28861
|
+
}
|
|
28862
|
+
return gross * (1 - feePercent / 100);
|
|
28863
|
+
}
|
|
28864
|
+
/**
|
|
28865
|
+
* A day's share of the epoch's net interest: a seventh of it, NOT a
|
|
28866
|
+
* de-compounded daily rate. The chain mints interest once per epoch, so
|
|
28867
|
+
* nothing compounds within the cycle and the accrual is linear across it.
|
|
28868
|
+
*
|
|
28869
|
+
* Fails closed with its input: `null` or non-finite → `null`.
|
|
28870
|
+
*/
|
|
28871
|
+
function dailyInterest(weekly) {
|
|
28872
|
+
if (weekly === null || !Number.isFinite(weekly))
|
|
28873
|
+
return null;
|
|
28874
|
+
return weekly / 7;
|
|
28875
|
+
}
|
|
28876
|
+
/**
|
|
28877
|
+
* Interest accrued since each pool's OWN last cycle close — the moving part
|
|
28878
|
+
* of lifetime interest (lifetime = settled on chain + this).
|
|
28879
|
+
*
|
|
28880
|
+
* ```
|
|
28881
|
+
* accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
|
|
28882
|
+
* ```
|
|
28883
|
+
*
|
|
28884
|
+
* **Per pool, because pools clear independently.** One shared anchor accrues
|
|
28885
|
+
* a stale pool's share from a close it never had, and when another pool
|
|
28886
|
+
* clears it drops the total by the stale pool's whole weekly share. Anchored
|
|
28887
|
+
* per pool, the total is monotonic across any single pool's clearing: that
|
|
28888
|
+
* pool swaps its accrued share for a capitalised one, and no other term moves.
|
|
28889
|
+
*
|
|
28890
|
+
* **The one-epoch cap is load-bearing.** Clearings are not exactly weekly,
|
|
28891
|
+
* and `weeklyInterest` is per EPOCH, so extrapolating past seven days would
|
|
28892
|
+
* promise interest the close will not credit; a late clearing under-reports
|
|
28893
|
+
* instead. The low clamp makes a close ahead of the local clock zero, never
|
|
28894
|
+
* negative.
|
|
28895
|
+
*
|
|
28896
|
+
* A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
|
|
28897
|
+
* nothing. Callers fail closed upstream by not offering an accrual for a pool
|
|
28898
|
+
* whose weekly interest or anchor is unknown.
|
|
28899
|
+
*/
|
|
28900
|
+
function accruedInterest(accruals, nowSeconds) {
|
|
28901
|
+
if (!Number.isFinite(nowSeconds))
|
|
28902
|
+
return 0;
|
|
28903
|
+
let accrued = 0;
|
|
28904
|
+
for (const { weeklyInterest: weekly, clearingTimestamp } of accruals) {
|
|
28905
|
+
if (!Number.isFinite(weekly) || !Number.isFinite(clearingTimestamp)) {
|
|
28906
|
+
continue;
|
|
28907
|
+
}
|
|
28908
|
+
const elapsed = Math.min(Math.max(nowSeconds - clearingTimestamp, 0), SECONDS_PER_WEEK);
|
|
28909
|
+
accrued += (weekly * elapsed) / SECONDS_PER_WEEK;
|
|
28910
|
+
}
|
|
28911
|
+
return accrued;
|
|
28912
|
+
}
|
|
28913
|
+
/**
|
|
28914
|
+
* What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
|
|
28915
|
+
* Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
|
|
28916
|
+
* earnings, so the fee must already be off the rate.
|
|
28917
|
+
*
|
|
28918
|
+
* FAILS CLOSED: a non-finite or negative amount or rate returns `NaN`, which
|
|
28919
|
+
* the consumer renders as "no figure" — exactly what `netEffectiveApy`
|
|
28920
|
+
* returns when the fee is unknown, so that state carries straight through.
|
|
28921
|
+
*
|
|
28922
|
+
* @param amount display units of the stable asset.
|
|
28923
|
+
* @param netApy the net effective annual rate as a 0..1 fraction.
|
|
28924
|
+
*/
|
|
28925
|
+
function projectedAnnualEarnings(amount, netApy) {
|
|
28926
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
28927
|
+
return NaN;
|
|
28928
|
+
if (!Number.isFinite(netApy) || netApy < 0)
|
|
28929
|
+
return NaN;
|
|
28930
|
+
return amount * netApy;
|
|
28931
|
+
}
|
|
28932
|
+
/**
|
|
28933
|
+
* Balance-weighted average APY across slices, skipping non-finite and
|
|
28934
|
+
* non-positive balances and non-finite rates; `0` when nothing carries a
|
|
28935
|
+
* positive balance. Whatever basis goes in comes out — feed it NET rates for
|
|
28936
|
+
* an average effective (net) rate.
|
|
28937
|
+
*/
|
|
28938
|
+
function weightedAverageApy(slices) {
|
|
28939
|
+
let totalBalance = 0;
|
|
28940
|
+
let weighted = 0;
|
|
28941
|
+
for (const slice of slices) {
|
|
28942
|
+
if (!Number.isFinite(slice.balance) || !Number.isFinite(slice.apy)) {
|
|
28943
|
+
continue;
|
|
28944
|
+
}
|
|
28945
|
+
if (slice.balance <= 0)
|
|
28946
|
+
continue;
|
|
28947
|
+
totalBalance += slice.balance;
|
|
28948
|
+
weighted += slice.balance * slice.apy;
|
|
28949
|
+
}
|
|
28950
|
+
if (totalBalance <= 0)
|
|
28951
|
+
return 0;
|
|
28952
|
+
return weighted / totalBalance;
|
|
28953
|
+
}
|
|
28954
|
+
|
|
28955
|
+
export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, BatchPendingError, BatchRevertedError, BatchSubmissionError, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, CallsFacade, DAYS_PER_EPOCH, DAYS_PER_MONTH, DEFAULT_BATCH_POLLING_POLICY, DepositBatchMismatchError, DepositFlow, DepositsFacade, ENTRY_POINT_V07, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_DAYS_FOR_MONTHS, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NOT_A_WITHDRAWAL_CALL_MESSAGE, NO_DIRECTUS_URL_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, NO_WITHDRAWAL_ENCODER_MESSAGE, PortfolioFacade, READ_ONLY_MESSAGE, RIXON_CAPITAL, SECONDS_PER_WEEK, SPONSORED_CALL_KINDS, SPONSORED_CALL_SELECTORS, SdkConfig, SponsoredCallInputError, StrategiesFacade, UPPER_MEZZANINE, WithdrawFlow, accruedInterest, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, availableFixedTermConfigs, batchOutcomeOf, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, cancelDeadline, ceilToCents, checkBatchOutcome, classifyWalletFailure, compareTrancheSeniority, computeSettlementWindow, countSubmissions, dailyInterest, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeCancelDepositCall, encodeCancelWithdrawalCall, encodeDepositData, encodeRequestWithdrawalCall, encodeWithdrawAtExpiryCall, epochRateToApy, explorerAddressUrl, explorerTxUrl, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getStrategyRegion, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isDefinitivelyNotSubmitted, isFixedTermConfigId, isUnpredictableGas, isUserRejected, lastEventTimestamp, lockDurationDays, lockDurationMonths, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, projectedAnnualEarnings, proveDepositInReceipt, proveUserOperationInReceipt, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, submitBatch, submitSponsoredCall, trancheApyBounds, trancheHasCapacity, trancheRiskRank, validateDepositBatchCalls, weeklyInterest, weightedAverageApy };
|