@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.
- package/README.md +43 -5
- package/dist/bundle.cjs.js +281 -14
- package/dist/bundle.esm.js +278 -15
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +1 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/revert-errors.d.ts +59 -0
- package/dist/domain/revert-errors.js +190 -0
- package/dist/domain/revert-errors.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +35 -4
- package/dist/flows/deposit-flow.js +89 -11
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +3 -0
- package/src/domain/revert-errors.ts +248 -0
- package/src/flows/deposit-flow.ts +125 -16
- package/src/flows/index.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BigNumber } from 'ethers';
|
|
1
|
+
import { BigNumber, utils } from 'ethers';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
asContractType,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
encodeDepositData,
|
|
8
8
|
GenerateContractResponse,
|
|
9
9
|
} from '../domain/loan-contract';
|
|
10
|
+
import { decodeRevert } from '../domain/revert-errors';
|
|
10
11
|
import {
|
|
11
12
|
classifyWalletFailure,
|
|
12
13
|
isUnpredictableGas,
|
|
@@ -64,10 +65,21 @@ import { WaitableTransaction } from './observable';
|
|
|
64
65
|
* their mind, and telling them something broke would be a lie. Only a WALLET
|
|
65
66
|
* call is ever classified that way: the HTTP ports are always `'failed'`,
|
|
66
67
|
* because a backend that words a refusal "declined" did not involve the
|
|
67
|
-
* lender's wallet. A
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* lender's wallet. A revert on the request step is read for WHAT reverted: a
|
|
69
|
+
* protocol error the ABI declares is `'reverted'` with the name, and only a
|
|
70
|
+
* token balance or allowance failure — or a revert nothing can decode — stays
|
|
71
|
+
* `'insufficient-balance'`. Everything else is `'failed'` and carries the
|
|
72
|
+
* original error for the consumer's crash reporter.
|
|
73
|
+
*
|
|
74
|
+
* ## Both signatures are checked before they are used
|
|
75
|
+
*
|
|
76
|
+
* A wallet is not obliged to hand back 65 bytes, and `encodeDepositData` will
|
|
77
|
+
* not catch one that does not: `defaultAbiCoder` encodes any even-length hex
|
|
78
|
+
* as `bytes`, `0x` included, so a malformed acceptance signature becomes a
|
|
79
|
+
* `depositData` blob that is broadcast, mined, and then can never be verified
|
|
80
|
+
* by the agreements service. Both personal signs are therefore length-checked
|
|
81
|
+
* where they are taken, and a bad one fails the step it was taken on rather
|
|
82
|
+
* than the one it would eventually have broken.
|
|
71
83
|
*
|
|
72
84
|
* ```ts
|
|
73
85
|
* const flow = new DepositFlow(ports);
|
|
@@ -117,6 +129,23 @@ export type DepositFailure =
|
|
|
117
129
|
| { step: DepositStep; reason: 'cancelled' }
|
|
118
130
|
| { step: DepositStep; reason: 'failed'; error: unknown }
|
|
119
131
|
| { step: 'request'; reason: 'insufficient-balance'; error: unknown }
|
|
132
|
+
| {
|
|
133
|
+
step: 'request';
|
|
134
|
+
reason: 'reverted';
|
|
135
|
+
/**
|
|
136
|
+
* The custom error the contract reverted with, exactly as the ABI
|
|
137
|
+
* declares it — `'ClearingIsPending'`, `'LendingPoolIsStopped'`,
|
|
138
|
+
* `'UserNotKycd'`, and the rest of
|
|
139
|
+
* `ILendingPoolManagerAbi` / `IKasuAllowListAbi`.
|
|
140
|
+
*
|
|
141
|
+
* Render `reverted` with generic copy and special-case only the
|
|
142
|
+
* names you have words for: a contract upgrade can add an error, and
|
|
143
|
+
* a consumer that assumed the set was closed would have nothing to
|
|
144
|
+
* show for the new one.
|
|
145
|
+
*/
|
|
146
|
+
revertError: string;
|
|
147
|
+
error: unknown;
|
|
148
|
+
}
|
|
120
149
|
| { step: 'request'; reason: 'contract-expired' };
|
|
121
150
|
|
|
122
151
|
// ---------------------------------------------------------------------------
|
|
@@ -303,6 +332,27 @@ export const CONTRACT_TTL_MS = 5 * 60 * 1000;
|
|
|
303
332
|
export const NO_SPENDER_MESSAGE =
|
|
304
333
|
'DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input';
|
|
305
334
|
|
|
335
|
+
/** What a malformed auth signature fails the generate step with. */
|
|
336
|
+
export const INVALID_AUTH_SIGNATURE_MESSAGE =
|
|
337
|
+
'DepositFlow: the wallet returned a malformed authentication signature; expected 65 bytes of 0x-prefixed hex';
|
|
338
|
+
|
|
339
|
+
/** What a malformed acceptance signature fails the confirm step with. */
|
|
340
|
+
export const INVALID_ACCEPTANCE_SIGNATURE_MESSAGE =
|
|
341
|
+
'DepositFlow: the wallet returned a malformed acceptance signature; expected 65 bytes of 0x-prefixed hex';
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Is this an EIP-191 personal sign, in the only shape the protocol accepts?
|
|
345
|
+
*
|
|
346
|
+
* 65 bytes — `r`, `s`, `v` — 0x-prefixed. Nothing downstream checks it: the
|
|
347
|
+
* ABI coder takes any even-length hex as `bytes`, so `'0x'` and a 64-byte
|
|
348
|
+
* string both encode happily into a `depositData` the agreements service then
|
|
349
|
+
* cannot verify against anything. The check has to happen where the signature
|
|
350
|
+
* is taken, which is here.
|
|
351
|
+
*/
|
|
352
|
+
function isPersonalSignature(signature: unknown): signature is string {
|
|
353
|
+
return utils.isHexString(signature, 65);
|
|
354
|
+
}
|
|
355
|
+
|
|
306
356
|
const INITIAL: DepositState = {
|
|
307
357
|
phase: 'idle',
|
|
308
358
|
step: null,
|
|
@@ -354,13 +404,32 @@ function badgeFor(
|
|
|
354
404
|
}
|
|
355
405
|
|
|
356
406
|
/**
|
|
357
|
-
* The request step
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
407
|
+
* The request step's extra outcomes, in the order the evidence supports.
|
|
408
|
+
*
|
|
409
|
+
* A revert is read for WHAT reverted before it is read as a shortfall.
|
|
410
|
+
* `requestDepositWithKyc` reverts for a whole family of declared reasons —
|
|
411
|
+
* `LendingPoolIsStopped`, `ClearingIsPending`, `UserNotKycd`, `UserBlocked`,
|
|
412
|
+
* `UserNotInAllowList`, `InvalidTranche`, `BlockExpired` — and every one of
|
|
413
|
+
* them used to arrive as `insufficient-balance`, which told a fully funded
|
|
414
|
+
* lender to top up a wallet that was never short while their pool was simply
|
|
415
|
+
* mid-clearing. So: a decoded protocol error is `reverted` and names itself.
|
|
416
|
+
*
|
|
417
|
+
* Only a token balance or allowance failure — or revert data nothing can
|
|
418
|
+
* decode, on the `UNPREDICTABLE_GAS_LIMIT` ethers raises for a failed gas
|
|
419
|
+
* estimate — is still `insufficient-balance`, which is the case that reason
|
|
420
|
+
* was named for and the one where topping up is genuinely the answer.
|
|
361
421
|
*/
|
|
362
422
|
function classifyRequest(err: unknown): DepositFailure {
|
|
363
|
-
|
|
423
|
+
const revert = decodeRevert(err);
|
|
424
|
+
if (revert?.family === 'protocol') {
|
|
425
|
+
return {
|
|
426
|
+
step: 'request',
|
|
427
|
+
reason: 'reverted',
|
|
428
|
+
revertError: revert.name,
|
|
429
|
+
error: err,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (revert !== null || isUnpredictableGas(err)) {
|
|
364
433
|
return { step: 'request', reason: 'insufficient-balance', error: err };
|
|
365
434
|
}
|
|
366
435
|
return classifyWalletFailure('request', err);
|
|
@@ -428,6 +497,18 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
|
|
|
428
497
|
const signature = await this._ports.signMessage(
|
|
429
498
|
contract.contractMessage,
|
|
430
499
|
);
|
|
500
|
+
// Checked HERE, not at the encoder: this is the signature the
|
|
501
|
+
// agreements service verifies the deposit against, and a wallet
|
|
502
|
+
// that returned something other than 65 bytes has not signed
|
|
503
|
+
// anything. Failing the confirm step names the step that produced
|
|
504
|
+
// it; letting it through would put an unverifiable blob on chain.
|
|
505
|
+
if (!isPersonalSignature(signature)) {
|
|
506
|
+
bridge.settle({
|
|
507
|
+
kind: 'failed',
|
|
508
|
+
error: new Error(INVALID_ACCEPTANCE_SIGNATURE_MESSAGE),
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
431
512
|
bridge.settle({ kind: 'accepted', signature });
|
|
432
513
|
} catch (err) {
|
|
433
514
|
// A WALLET error, and the only one this flow classifies as a
|
|
@@ -536,6 +617,18 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
|
|
|
536
617
|
);
|
|
537
618
|
return;
|
|
538
619
|
}
|
|
620
|
+
// The same EIP-191 shape as the acceptance, and the same reason to
|
|
621
|
+
// check it: kasu-backend verifies this one to decide whether to issue
|
|
622
|
+
// an agreement at all, so a malformed signature is a 4xx that says
|
|
623
|
+
// nothing about which step produced it. Named here instead.
|
|
624
|
+
if (!isPersonalSignature(signature)) {
|
|
625
|
+
this._fail(token, approvalRequired, {
|
|
626
|
+
step: 'generate',
|
|
627
|
+
reason: 'failed',
|
|
628
|
+
error: new Error(INVALID_AUTH_SIGNATURE_MESSAGE),
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
539
632
|
if (!this._store.isCurrent(token)) return;
|
|
540
633
|
|
|
541
634
|
// 3. Generate — POST the request.
|
|
@@ -620,12 +713,28 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
|
|
|
620
713
|
return;
|
|
621
714
|
}
|
|
622
715
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
716
|
+
// The acceptance signature is already known to be 65 bytes, but the
|
|
717
|
+
// encoder also reads `timestamp`, `contractVersion` and
|
|
718
|
+
// `contractType` off a response the SDK did not produce. `start()`
|
|
719
|
+
// promises it does not reject, so anything that throws in here becomes
|
|
720
|
+
// a failure the consumer can render rather than an unhandled rejection
|
|
721
|
+
// in an application that was told it never had to catch one.
|
|
722
|
+
let depositData: string;
|
|
723
|
+
try {
|
|
724
|
+
depositData = encodeDepositData({
|
|
725
|
+
signature: outcome.signature,
|
|
726
|
+
timestamp: contract.timestamp,
|
|
727
|
+
contractVersion: contract.contractVersion,
|
|
728
|
+
contractType: asContractType(contract.contractType),
|
|
729
|
+
});
|
|
730
|
+
} catch (err) {
|
|
731
|
+
this._fail(token, approvalRequired, {
|
|
732
|
+
step: 'request',
|
|
733
|
+
reason: 'failed',
|
|
734
|
+
error: err,
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
629
738
|
|
|
630
739
|
// 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
|
|
631
740
|
// unlimited allowance outlives the deposit it was granted for, and a
|
package/src/flows/index.ts
CHANGED