@kasufinance/kasu-sdk 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20835,12 +20835,12 @@ function toURLEncodedForm(data, options) {
20835
20835
  });
20836
20836
  }
20837
20837
 
20838
- const MAX_DEPTH = DEFAULT_FORM_DATA_MAX_DEPTH;
20838
+ const MAX_DEPTH$1 = DEFAULT_FORM_DATA_MAX_DEPTH;
20839
20839
 
20840
20840
  function throwIfDepthExceeded(index) {
20841
- if (index > MAX_DEPTH) {
20841
+ if (index > MAX_DEPTH$1) {
20842
20842
  throw new AxiosError$1(
20843
- 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH,
20843
+ 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH$1,
20844
20844
  AxiosError$1.ERR_FORM_DATA_DEPTH_EXCEEDED
20845
20845
  );
20846
20846
  }
@@ -24804,6 +24804,193 @@ function buildFullNameRequestMessage(address, timestampMs) {
24804
24804
  return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24805
24805
  }
24806
24806
 
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
+
24807
24994
  /**
24808
24995
  * Wallet and RPC error predicates — pure functions over `unknown`.
24809
24996
  *
@@ -24963,6 +25150,22 @@ function classifyWalletFailure(step, err) {
24963
25150
  const CONTRACT_TTL_MS = 5 * 60 * 1000;
24964
25151
  /** What a run without a spender, from either source, fails with. */
24965
25152
  const NO_SPENDER_MESSAGE = 'DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input';
25153
+ /** What a malformed auth signature fails the generate step with. */
25154
+ const INVALID_AUTH_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed authentication signature; expected 65 bytes of 0x-prefixed hex';
25155
+ /** What a malformed acceptance signature fails the confirm step with. */
25156
+ const INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed acceptance signature; expected 65 bytes of 0x-prefixed hex';
25157
+ /**
25158
+ * Is this an EIP-191 personal sign, in the only shape the protocol accepts?
25159
+ *
25160
+ * 65 bytes — `r`, `s`, `v` — 0x-prefixed. Nothing downstream checks it: the
25161
+ * ABI coder takes any even-length hex as `bytes`, so `'0x'` and a 64-byte
25162
+ * string both encode happily into a `depositData` the agreements service then
25163
+ * cannot verify against anything. The check has to happen where the signature
25164
+ * is taken, which is here.
25165
+ */
25166
+ function isPersonalSignature(signature) {
25167
+ return utils$2.isHexString(signature, 65);
25168
+ }
24966
25169
  const INITIAL$1 = {
24967
25170
  phase: 'idle',
24968
25171
  step: null,
@@ -24987,13 +25190,32 @@ function badgeFor(step, approvalRequired) {
24987
25190
  };
24988
25191
  }
24989
25192
  /**
24990
- * The request step has a third outcome. `UNPREDICTABLE_GAS_LIMIT` here is
24991
- * almost always `transferFrom` reverting on a balance that cannot cover the
24992
- * deposit, and it takes precedence: nothing was refused by the lender, so
24993
- * inviting a retry would just reproduce it.
25193
+ * The request step's extra outcomes, in the order the evidence supports.
25194
+ *
25195
+ * A revert is read for WHAT reverted before it is read as a shortfall.
25196
+ * `requestDepositWithKyc` reverts for a whole family of declared reasons —
25197
+ * `LendingPoolIsStopped`, `ClearingIsPending`, `UserNotKycd`, `UserBlocked`,
25198
+ * `UserNotInAllowList`, `InvalidTranche`, `BlockExpired` — and every one of
25199
+ * them used to arrive as `insufficient-balance`, which told a fully funded
25200
+ * lender to top up a wallet that was never short while their pool was simply
25201
+ * mid-clearing. So: a decoded protocol error is `reverted` and names itself.
25202
+ *
25203
+ * Only a token balance or allowance failure — or revert data nothing can
25204
+ * decode, on the `UNPREDICTABLE_GAS_LIMIT` ethers raises for a failed gas
25205
+ * estimate — is still `insufficient-balance`, which is the case that reason
25206
+ * was named for and the one where topping up is genuinely the answer.
24994
25207
  */
24995
25208
  function classifyRequest(err) {
24996
- if (isUnpredictableGas(err)) {
25209
+ const revert = decodeRevert(err);
25210
+ if ((revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol') {
25211
+ return {
25212
+ step: 'request',
25213
+ reason: 'reverted',
25214
+ revertError: revert.name,
25215
+ error: err,
25216
+ };
25217
+ }
25218
+ if (revert !== null || isUnpredictableGas(err)) {
24997
25219
  return { step: 'request', reason: 'insufficient-balance', error: err };
24998
25220
  }
24999
25221
  return classifyWalletFailure('request', err);
@@ -25045,6 +25267,18 @@ class DepositFlow extends Flow {
25045
25267
  this._store.patch(Object.assign({ phase: 'accepting-sign' }, badgeFor('confirm', this._store.state.approvalRequired)), token);
25046
25268
  try {
25047
25269
  const signature = yield this._ports.signMessage(contract.contractMessage);
25270
+ // Checked HERE, not at the encoder: this is the signature the
25271
+ // agreements service verifies the deposit against, and a wallet
25272
+ // that returned something other than 65 bytes has not signed
25273
+ // anything. Failing the confirm step names the step that produced
25274
+ // it; letting it through would put an unverifiable blob on chain.
25275
+ if (!isPersonalSignature(signature)) {
25276
+ bridge.settle({
25277
+ kind: 'failed',
25278
+ error: new Error(INVALID_ACCEPTANCE_SIGNATURE_MESSAGE),
25279
+ });
25280
+ return;
25281
+ }
25048
25282
  bridge.settle({ kind: 'accepted', signature });
25049
25283
  }
25050
25284
  catch (err) {
@@ -25138,6 +25372,18 @@ class DepositFlow extends Flow {
25138
25372
  this._fail(token, approvalRequired, classifyWalletFailure('generate', err));
25139
25373
  return;
25140
25374
  }
25375
+ // The same EIP-191 shape as the acceptance, and the same reason to
25376
+ // check it: kasu-backend verifies this one to decide whether to issue
25377
+ // an agreement at all, so a malformed signature is a 4xx that says
25378
+ // nothing about which step produced it. Named here instead.
25379
+ if (!isPersonalSignature(signature)) {
25380
+ this._fail(token, approvalRequired, {
25381
+ step: 'generate',
25382
+ reason: 'failed',
25383
+ error: new Error(INVALID_AUTH_SIGNATURE_MESSAGE),
25384
+ });
25385
+ return;
25386
+ }
25141
25387
  if (!this._store.isCurrent(token))
25142
25388
  return;
25143
25389
  // 3. Generate — POST the request.
@@ -25194,12 +25440,29 @@ class DepositFlow extends Flow {
25194
25440
  });
25195
25441
  return;
25196
25442
  }
25197
- const depositData = encodeDepositData({
25198
- signature: outcome.signature,
25199
- timestamp: contract.timestamp,
25200
- contractVersion: contract.contractVersion,
25201
- contractType: asContractType(contract.contractType),
25202
- });
25443
+ // The acceptance signature is already known to be 65 bytes, but the
25444
+ // encoder also reads `timestamp`, `contractVersion` and
25445
+ // `contractType` off a response the SDK did not produce. `start()`
25446
+ // promises it does not reject, so anything that throws in here becomes
25447
+ // a failure the consumer can render rather than an unhandled rejection
25448
+ // in an application that was told it never had to catch one.
25449
+ let depositData;
25450
+ try {
25451
+ depositData = encodeDepositData({
25452
+ signature: outcome.signature,
25453
+ timestamp: contract.timestamp,
25454
+ contractVersion: contract.contractVersion,
25455
+ contractType: asContractType(contract.contractType),
25456
+ });
25457
+ }
25458
+ catch (err) {
25459
+ this._fail(token, approvalRequired, {
25460
+ step: 'request',
25461
+ reason: 'failed',
25462
+ error: err,
25463
+ });
25464
+ return;
25465
+ }
25203
25466
  // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25204
25467
  // unlimited allowance outlives the deposit it was granted for, and a
25205
25468
  // later exploit of the spender would drain a wallet that has long
@@ -26887,4 +27150,4 @@ function getTrancheDisplayName(trancheName, pool) {
26887
27150
  : trancheName;
26888
27151
  }
26889
27152
 
26890
- export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, DepositFlow, DepositsFacade, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NO_DIRECTUS_URL_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, PortfolioFacade, READ_ONLY_MESSAGE, RIXON_CAPITAL, SdkConfig, StrategiesFacade, UPPER_MEZZANINE, WithdrawFlow, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, ceilToCents, classifyWalletFailure, compareTrancheSeniority, computeSettlementWindow, countSubmissions, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeDepositData, epochRateToApy, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isUnpredictableGas, isUserRejected, lastEventTimestamp, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, trancheApyBounds, trancheHasCapacity, trancheRiskRank };
27153
+ export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, DepositFlow, DepositsFacade, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NO_DIRECTUS_URL_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, PortfolioFacade, READ_ONLY_MESSAGE, RIXON_CAPITAL, SdkConfig, StrategiesFacade, UPPER_MEZZANINE, WithdrawFlow, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, ceilToCents, classifyWalletFailure, compareTrancheSeniority, computeSettlementWindow, countSubmissions, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeDepositData, epochRateToApy, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isUnpredictableGas, isUserRejected, lastEventTimestamp, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, trancheApyBounds, trancheHasCapacity, trancheRiskRank };
@@ -32,6 +32,8 @@ export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePo
32
32
  export type { BestTranche } from './pools';
33
33
  export { countSubmissions, deriveRequestState, firstSubmissionTimestamp, isCycleClosed, lastEventTimestamp, submissionEvents, } from './requests';
34
34
  export type { RequestKind, RequestState, RequestStatusCode, } from './requests';
35
+ export { decodeRevert, extractRevertData } from './revert-errors';
36
+ export type { DecodedRevert, RevertFamily } from './revert-errors';
35
37
  export { CLEARING_WINDOW_SECONDS, computeSettlementWindow, deriveCycleDates, nextCycleBoundary, } from './settlement';
36
38
  export type { CycleDates, SettlementWindowInput, SettlementWindowState, } from './settlement';
37
39
  export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
@@ -26,6 +26,7 @@ export { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON
26
26
  export { asContractType, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, encodeDepositData, formatSignTimestampUtc, parseFormattedMessage, } from './loan-contract';
27
27
  export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
28
28
  export { countSubmissions, deriveRequestState, firstSubmissionTimestamp, isCycleClosed, lastEventTimestamp, submissionEvents, } from './requests';
29
+ export { decodeRevert, extractRevertData } from './revert-errors';
29
30
  export { CLEARING_WINDOW_SECONDS, computeSettlementWindow, deriveCycleDates, nextCycleBoundary, } from './settlement';
30
31
  export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
31
32
  export { compareTrancheSeniority, derivePoolStatus, MIN_TRANCHE_CAPACITY, netTrancheApyBounds, pickDefaultTrancheId, poolAllTranchesFull, trancheApyBounds, trancheHasCapacity, trancheRiskRank, } from './tranches';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACH,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACH,SAAS,EACT,2BAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACvB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACH,MAAM,EACN,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,aAAa,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACH,cAAc,EACd,wBAAwB,EACxB,2BAA2B,EAC3B,iCAAiC,EACjC,6BAA6B,EAC7B,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,GACxB,MAAM,iBAAiB,CAAC;AAYzB,OAAO,EACH,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,kBAAkB,GACrB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,YAAY,CAAC;AAOpB,OAAO,EACH,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAOtB,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EACH,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GAClB,MAAM,YAAY,CAAC;AAOpB,OAAO,EACH,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,GACjB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACH,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACH,SAAS,EACT,2BAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACvB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACH,MAAM,EACN,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,aAAa,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACH,cAAc,EACd,wBAAwB,EACxB,2BAA2B,EAC3B,iCAAiC,EACjC,6BAA6B,EAC7B,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,GACxB,MAAM,iBAAiB,CAAC;AAYzB,OAAO,EACH,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,kBAAkB,GACrB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,YAAY,CAAC;AAOpB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EACH,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAOtB,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EACH,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GAClB,MAAM,YAAY,CAAC;AAOpB,OAAO,EACH,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,GACjB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Revert decoding — turn the bytes a failed call came back with into the name
3
+ * the contract actually reverted with.
4
+ *
5
+ * `isUnpredictableGas` says only THAT the call would revert. That was enough
6
+ * while the answer was always the same one: a `transferFrom` failing on a
7
+ * balance that cannot cover the deposit. It is not enough any more, because
8
+ * `requestDepositWithKyc` reverts for a whole family of reasons the ABI
9
+ * declares and the lender can do nothing about — the pool is stopped, the pool
10
+ * is mid-clearing, their KYC lapsed, they are not on the allow list, the
11
+ * allow-list signature's block window closed. Every one of those was reported
12
+ * as "insufficient balance", so a fully funded lender was told to top up a
13
+ * wallet that was never short.
14
+ *
15
+ * The revert data carries the answer. This module extracts it from whatever
16
+ * envelope the provider wrapped it in and matches the 4-byte selector against
17
+ * the custom errors the protocol declares.
18
+ *
19
+ * Pure, like everything else in `domain/`: bytes in, a name out. It never
20
+ * decides what a lender is shown — `DepositFlow` maps the name to a code and
21
+ * the application maps the code to its own words.
22
+ */
23
+ /**
24
+ * Which family a recognised revert belongs to, because the two are acted on
25
+ * differently.
26
+ *
27
+ * `erc20` is the case `insufficient-balance` was named for and still means:
28
+ * the token transfer itself failed, so the lender genuinely has to fund or
29
+ * re-approve. `protocol` is everything else the protocol declares — a
30
+ * condition of the pool or the lender's standing, which topping up cannot fix.
31
+ */
32
+ export type RevertFamily = 'protocol' | 'erc20';
33
+ export interface DecodedRevert {
34
+ /** The custom error's name, exactly as the ABI declares it. */
35
+ name: string;
36
+ family: RevertFamily;
37
+ /** The reason of a `require(..., "…")` revert, when it was one. */
38
+ reason?: string;
39
+ }
40
+ /**
41
+ * Find the revert data on an error, wherever the provider left it.
42
+ *
43
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
44
+ * attached, and each provider nests the bytes one layer differently:
45
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
46
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
47
+ * products, walk a fixed set of keys to a fixed depth and take the first value
48
+ * that is shaped like revert data.
49
+ */
50
+ export declare function extractRevertData(err: unknown): string | null;
51
+ /**
52
+ * Decode a failed call's revert into the error the contract named, or `null`
53
+ * when there is nothing recognisable to name.
54
+ *
55
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
56
+ * string, or an error with no revert data at all — the caller keeps whatever
57
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
58
+ */
59
+ export declare function decodeRevert(err: unknown): DecodedRevert | null;
@@ -0,0 +1,190 @@
1
+ import { ethers } from 'ethers';
2
+ import { IKasuAllowListAbi__factory } from '../contracts/factories/IKasuAllowListAbi__factory';
3
+ import { ILendingPoolManagerAbi__factory } from '../contracts/factories/ILendingPoolManagerAbi__factory';
4
+ // ---------------------------------------------------------------------------
5
+ // The selector tables
6
+ // ---------------------------------------------------------------------------
7
+ /**
8
+ * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
9
+ * Kasu contract declares them — they arrive from the TOKEN, through the
10
+ * manager's `transferFrom`, and they are the modern spelling of the string
11
+ * reverts below.
12
+ */
13
+ const ERC20_ERROR_ABI = [
14
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
15
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
16
+ ];
17
+ /**
18
+ * The protocol errors that mean the ERC-20 transfer failed rather than the
19
+ * protocol refusing.
20
+ *
21
+ * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
22
+ * that returned false or reverted, which on this path IS the `transferFrom`;
23
+ * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24
+ * the balance case, not with the pool conditions — a lender who sees one of
25
+ * them really does need to fund or re-approve.
26
+ */
27
+ const ERC20_FAMILY_NAMES = new Set([
28
+ 'AddressInsufficientBalance',
29
+ 'ERC20InsufficientAllowance',
30
+ 'ERC20InsufficientBalance',
31
+ 'SafeERC20FailedOperation',
32
+ ]);
33
+ /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
34
+ const STRING_REVERT_SELECTOR = '0x08c379a0';
35
+ /**
36
+ * The `require` reasons that are a balance or an allowance, across the token
37
+ * implementations in circulation: OpenZeppelin v4's sentences, and the
38
+ * shorthand the minimal implementations use.
39
+ */
40
+ const ERC20_REASON_PATTERNS = [
41
+ /exceeds\s+balance/,
42
+ /exceeds\s+allowance/,
43
+ /insufficient\s+balance/,
44
+ /insufficient\s+allowance/,
45
+ /transfer_from_failed/,
46
+ /transferfrom\s+failed/,
47
+ /low-level\s+call\s+failed/,
48
+ ];
49
+ /**
50
+ * selector → error name, built once on first use.
51
+ *
52
+ * Lazily, because a consumer that never sees a revert should not pay for three
53
+ * `Interface` constructions at import. The two Kasu tables are the GENERATED
54
+ * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
55
+ * and the new errors are recognised with no edit here.
56
+ */
57
+ let selectorTable = null;
58
+ function selectors() {
59
+ if (selectorTable)
60
+ return selectorTable;
61
+ const table = new Map();
62
+ const interfaces = [
63
+ // The manager the deposit is called against, and the allow list it
64
+ // delegates the KYC and block-window checks to.
65
+ ILendingPoolManagerAbi__factory.createInterface(),
66
+ IKasuAllowListAbi__factory.createInterface(),
67
+ new ethers.utils.Interface(ERC20_ERROR_ABI),
68
+ ];
69
+ for (const iface of interfaces) {
70
+ for (const fragment of Object.values(iface.errors)) {
71
+ // First writer wins: the same name declared by both ABIs is the
72
+ // same selector, so the duplicate would be a no-op anyway.
73
+ const sighash = iface.getSighash(fragment);
74
+ if (!table.has(sighash))
75
+ table.set(sighash, fragment.name);
76
+ }
77
+ }
78
+ selectorTable = table;
79
+ return table;
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Getting the bytes out of the envelope
83
+ // ---------------------------------------------------------------------------
84
+ /**
85
+ * How deep to look for the revert data. Providers nest it differently —
86
+ * ethers' own error wraps the provider's, which wraps the RPC body — and a
87
+ * bound is what keeps a self-referencing error object from being walked
88
+ * forever.
89
+ */
90
+ const MAX_DEPTH = 6;
91
+ /** The keys ethers and the providers under it put a wrapped error on. */
92
+ const NESTED_KEYS = [
93
+ 'data',
94
+ 'error',
95
+ 'originalError',
96
+ 'info',
97
+ 'cause',
98
+ 'body',
99
+ ];
100
+ /** At least a 4-byte selector, and a whole number of bytes. */
101
+ function isRevertData(value) {
102
+ return (value.length >= 10 &&
103
+ value.length % 2 === 0 &&
104
+ /^0x[0-9a-fA-F]+$/.test(value));
105
+ }
106
+ /**
107
+ * Find the revert data on an error, wherever the provider left it.
108
+ *
109
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
110
+ * attached, and each provider nests the bytes one layer differently:
111
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
112
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
113
+ * products, walk a fixed set of keys to a fixed depth and take the first value
114
+ * that is shaped like revert data.
115
+ */
116
+ export function extractRevertData(err) {
117
+ return walk(err, 0);
118
+ }
119
+ function walk(node, depth) {
120
+ if (node == null || depth > MAX_DEPTH)
121
+ return null;
122
+ if (typeof node === 'string') {
123
+ if (isRevertData(node))
124
+ return node;
125
+ // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
126
+ if (!node.startsWith('{'))
127
+ return null;
128
+ try {
129
+ return walk(JSON.parse(node), depth + 1);
130
+ }
131
+ catch (_a) {
132
+ return null;
133
+ }
134
+ }
135
+ if (typeof node !== 'object')
136
+ return null;
137
+ const record = node;
138
+ for (const key of NESTED_KEYS) {
139
+ const found = walk(record[key], depth + 1);
140
+ if (found)
141
+ return found;
142
+ }
143
+ return null;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Decoding
147
+ // ---------------------------------------------------------------------------
148
+ /**
149
+ * Decode a failed call's revert into the error the contract named, or `null`
150
+ * when there is nothing recognisable to name.
151
+ *
152
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
153
+ * string, or an error with no revert data at all — the caller keeps whatever
154
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
155
+ */
156
+ export function decodeRevert(err) {
157
+ const data = extractRevertData(err);
158
+ if (!data)
159
+ return null;
160
+ const selector = data.slice(0, 10).toLowerCase();
161
+ if (selector === STRING_REVERT_SELECTOR) {
162
+ const reason = decodeStringRevert(data);
163
+ if (reason === null)
164
+ return null;
165
+ return isErc20Reason(reason)
166
+ ? { name: 'Error', family: 'erc20', reason }
167
+ : null;
168
+ }
169
+ const name = selectors().get(selector);
170
+ if (!name)
171
+ return null;
172
+ return {
173
+ name,
174
+ family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
175
+ };
176
+ }
177
+ function decodeStringRevert(data) {
178
+ try {
179
+ const [reason] = ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
180
+ return reason;
181
+ }
182
+ catch (_a) {
183
+ return null;
184
+ }
185
+ }
186
+ function isErc20Reason(reason) {
187
+ const lower = reason.toLowerCase();
188
+ return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
189
+ }
190
+ //# sourceMappingURL=revert-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revert-errors.js","sourceRoot":"","sources":["../../src/domain/revert-errors.ts"],"names":[],"mappings":"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"}